分布式基础篇4 —— 基础篇完结(谷粒商城)
创始人
2024-05-09 11:46:15
0

  • 分类维护
    • 一、三级分类
      • 后端实现
      • 准备工作
      • 跨域问题
      • 关闭 ESLint 检查
      • 前端实现
    • 二、分类删除
      • 前端完善分类列表
      • 后端实现——删除
      • 配置发送请求代码片段
      • 前端实现——删除
    • 三、分类增加
      • 前端实现
    • 四、分类修改
    • 五、拖拽菜单
      • 拖拽效果实现
      • 拖拽数据收集
      • 拖拽功能完成
      • 拖拽功能完善
    • 六、批量删除
  • 品牌管理
    • 一、效果优化及快速显示开关
    • 二、品牌Logo上传
      • 阿里云OSS
      • 整合阿里云OSS
      • 获取服务端签名
      • 前后端联调上传文件
    • 三、前端表单校验
    • 四、后端数据验证
      • JSR303
        • 如何使用 JSR303
        • 提取校验错误信息
        • 统一错误状态码
        • 统一异常处理
        • 分组校验
        • 自定义校验注解
    • 五、SPU&SKU
      • SPU
      • SKU
      • 基本属性【规格参数】与销售属性
      • 商品表设计分析
  • 平台属性
    • 一、属性分组
      • 组件抽取
      • 父子组件交互
      • 获取分类属性分组
        • 后端实现
        • 前端实现
      • 分组新增&级联选择器
        • JsonInclude
      • 分组修改&级联选择器显示
    • 二、品牌分类关联与级联更新
      • 增加MyBatis-Plus分页插件
      • 品牌管理模糊查询
      • 前端模块复制
      • 品牌管理&关联分类
      • 新增品牌关联分类
      • 级联更新
    • 三、规格参数
      • Object对象划分
      • 规格参数新增
      • 规格参数列表
      • 规格参数修改
        • 规格参数回显
        • 规格参数修改
    • 四、销售属性
    • 五、分组关联属性&删除关联
      • 分组关联属性
      • 删除关联
      • 查询分组未关联的属性
      • 新增分组属性关联
      • 新增属性小bug
  • 商品维护
    • 一、发布商品
      • 环境准备与测试
      • 获取分类关联的所有品牌
      • 获取分类下的所有分组&关联属性
      • BUG: 规格参数无法单选多选
      • 新增商品
        • 新增商品业务流程分析
        • 业务代码
    • 二、Spu管理
      • Spu检索
      • 规格维护
        • 小BUG
        • 获取 spu 规格
        • 修改 spu 规格
    • 三、商品管理
      • sku 检索
  • 库存系统
    • 一、整合ware服务&获取库存列表
    • 二、查询商品库存&创建采购需求
      • 模糊查询商品库存
      • 模糊查询采购需求
      • 合并采购需求
        • 获取未领取的采购单
        • 合并采购单
      • 领取采购单
      • 完成采购
  • 分布式基础篇总结

视频来源: 【Java项目《谷粒商城》Java架构师 | 微服务 | 大型电商项目】

分类维护

一、三级分类

后端实现

image-20221225180259710

查询出数据库中的所有分类:

image-20221225180536374

导入 SQL 语句:

image-20221226204455068

数据库表字段含义:

image-20221226204108521

一级分类的parent_id 为 0,一级分类的 cat_id 为二级分类的 parent_id,二级分类的 cat_id 为三级分类的 parent_id.

1、在 gulimall-product 模块下完成分类功能

2、在 CategoryEntity 实体类中,增加一个属性

用于保存每个分类的子分类

	// 分类的子级分类@TableField(exist = false)List childrenLevel;

3、CategoryController

    /*** 查询所有分类,封装成树形结构*/@RequestMapping("/list/tree")//@RequiresPermissions("product:category:list")public R list(@RequestParam Map params){List entities = categoryService.listWithTree();return R.ok().put("data", entities);}

4、CategoryService 接口

    // 查询所有分类,封装成树形结构List listWithTree();

5、CategoryServiceImpl 实现类

@Service("categoryService")
public class CategoryServiceImpl extends ServiceImpl implements CategoryService {// 查询所有分类,封装成树形结构@Overridepublic List listWithTree() {// 1、查询出所有分类List all = baseMapper.selectList(null);List level1 = all.stream().filter(categoryEntity -> { // 2、先找出所有的一级分类return categoryEntity.getParentCid() == 0;}).map(categoryEntity -> { // 3、找出每个一级分类下的所有子分类categoryEntity.setChildrenLevel(getChildrens(categoryEntity, all));return categoryEntity;}).sorted((menu1, menu2) -> {   // 4、根据 Sort 字段排序return (menu1.getSort() == null ? 0 : menu1.getSort()) - (menu2.getSort() == null ? 0 : menu2.getSort());}).collect(Collectors.toList());return level1;}/*** @description* @date 2022/12/25 19:50* @param entity 父级分类* @param all 所有分类的集合* @return java.util.List*/private List getChildrens(CategoryEntity entity, List all) {List treeList = all.stream().filter(categoryEntity -> { // 1、找出集合中分类对应的所有子分类return categoryEntity.getParentCid() == entity.getCatId();}).map(categoryEntity -> { // 2、递归查找所有的子分类categoryEntity.setChildrenLevel(getChildrens(categoryEntity, all));return categoryEntity;}).sorted((menu1, menu2) -> {return (menu1.getSort() == null ? 0 : menu1.getSort()) - (menu2.getSort() == null ? 0 : menu2.getSort());}).collect(Collectors.toList());return treeList;}
}

6、使用 PostMan 测试

image-20221225195842689

7、将 gulimall-product 注册到 Nacos,并配置网关

gulimall-product 模块配置:

image-20221226154450645

gulimall-gateway 模块配置:

注意和 admin_route 的顺序,精确路径在前,否则会交给 admin_route 服务

image-20221226154517086

准备工作

1、启动 renren-fast-vue 项目, 新增 商品系统 的 菜单

image-20221225200622693

2、新增子级菜单——分类维护

image-20221225202738513

动态生成的菜单会在数据库中sys_menu表中生成对应的路由路径

image-20221225202914101

3、路由 路径的对应规则,前缀代表页面所在的目录,后缀代表页面名称。

image-20221225201351116

那么 product-category 所对应的就是: modules/product/category.vue 页面,创建对应的页面

image-20221225203038751

4、

在前端向后端发送请求之前,我们需要先修改请求的路径,修改 /static/config/index.js 中的路径,统一由Gateway 网关进行转发

image-20221225210200347

统一向网关发送请求,就需要将 renren-fast 注册到 Nacos 注册中心

  • renren-fast 模块引入所需的依赖
    • 这里建议不要直接引入 gulimall-common 依赖,renren-fast 的版本和common的依赖有很多冲突。
    • 增加guava 这个依赖主要是因为我报了这个错误:com.google.common.collect.Sets$SetView.iterator()Lcom/google/common/collect/UnmodifiableIterator 大概由于版本冲突导致的
	com.alibaba.cloudspring-cloud-alibaba-dependencies2021.0.4.0pomimportcom.google.guavaguava30.1-jrecom.alibaba.cloudspring-cloud-starter-alibaba-nacos-discovery
  • application配置nacos注册中心地址、application.name

image-20221225220209786

  • 主启动类增加 @EnableDiscoveryClient 注解
  • 最终注册成功

image-20221225214904046

5、向网关发送请求后,会出现 404, 主要是因为我们在 index.js 中修改了请求路径为http://localhost:88/api,正确的路径应该是: localhost:8080/renren-fast/captcha

因此我们需要使用 GateWay 中的 filters 对路径进行重写

image-20221225215019144

在 Gateway 中配置:

image-20221225215939389

                - id: admin_routeuri: lb://renren-fast # 负载均衡predicates:- Path=/api/**filters:- RewritePath=/api/?(?.*), /renren-fast/$\{segment}  #路径重写

跨域问题

接着在登录时,由于俩次url不一样,因此会报出 跨域问题。

image-20221225220329058

跨域:指的是浏览器不能执行其他网站的脚本。它是由浏览器的同源策略造成的,是 浏览器对javascript施加的安全限制。

同源策略:是指协议,域名,端口都要相同,其中有一个不同都会产生跨域;

image-20221225221927822

跨域流程

image-20221225222046549

跨域文档: 跨源资源共享(CORS) - HTTP | MDN (mozilla.org)

解决跨域问题的方式

方式一:使用 Nginx 部署为同一域

image-20221225222621379

方式二:设置请求头允许跨域

image-20221225222701390

这样太麻烦,我们可以直接在网关模块中编写配置类,因为每个请求都会经过网关,这样就无需在每次请求都设置一遍请求头。

在 Gateway 模块创建 /config/CorsConfig 配置类

/*** description: 跨域配置类** @author YZG* @date 2022/12/24*/
@Configuration
public class CorsConfig {@Beanpublic CorsWebFilter corsFilter() {CorsConfiguration config = new CorsConfiguration();config.addAllowedMethod("*");config.addAllowedOrigin("*");config.addAllowedHeader("*");config.setAllowCredentials(true);UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(new PathPatternParser());source.registerCorsConfiguration("/**", config);return new CorsWebFilter(source);}
}

在 renren-fast 中也提供了一个跨域配置类,需要把这个注释掉,使用我们自己配置的跨域类

登陆成功!!~~

image-20221225223844880

关闭 ESLint 检查

VSCode在保存过程中,有可能出现以下警告,虽然不影响代码,但是看着是真烦,这是因为ESLint 语法检查太严格了,在 .eslintignore 这个文件中增加 * 忽略即可,。

image-20221225202506574

前端实现

category.vue 页面


Element-UI 参数说明:

image-20221226155758230

label 对应 展示的分类名称,对应 name,children 指定子级分类,对应 childrenLevel

image-20221226155918848

二、分类删除

前端完善分类列表

1、将以下代码放入 el-tree 标签内: 删除、增加菜单按钮

      {{ node.label }} append(data)">Append remove(node, data)">Delete

其中 data、node俩个对象属性中保存的内容:

image-20221226162416225

2、对分类菜单删除的规则:如果是三级分类,不显示 append,三级分类无法继续增加分类,如果分类没有子级分类允许 delete,可以利用 node 对象中的 level 判断是几级分类,childNode 判断是否有子级分类。

image-20221226163434764

3、为分类增加勾选框

image-20221226163858621

效果:

image-20221226163916538

后端实现——删除

对于删除功能,使用MyBatis-Plus逻辑删除,通过修改数据库中的字段达到逻辑上的删除功能。

1、在 CategoryEntity 实体类中增加逻辑删除注解

image-20221226171211041

2、

CategoryController:/*** 删除* 删除需要判断是否分类还在别的地方引用* @RequestBody: 获取请求体中的内容,只能发送 POST请求*/@RequestMapping("/delete")// @RequiresPermissions("product:category:delete")public R delete(@RequestBody Long[] catIds){categoryService.removeMenuByIds(Arrays.asList(catIds));// categoryService.removeByIds(Arrays.asList(catIds));return R.ok();}CategoryService:// 删除菜单void removeMenuByIds(List asList);CategoryServiceImpl:/*** 删除菜单* @param asList*/@Overridepublic void removeMenuByIds(List asList) {// TODO: 判断别的地方是否存在引用baseMapper.deleteBatchIds(asList);}

配置发送请求代码片段

将get、post请求封装成代码片段,方便开发

VSCode :文件 ——> 首选项 ——> 配置用户代码片段

  "http-get 请求": {"prefix": "httpget","body": ["this.\\$http({","url: this.\\$http.adornUrl(''),","method: 'get',","params: this.\\$http.adornParams({})","}).then(({data}) => {","})"],"description": "httpGET 请求"},"http-post 请求": {"prefix": "httppost","body": ["this.\\$http({","url: this.\\$http.adornUrl(''),","method: 'post',","data: this.\\$http.adornData(data, false)","}).then(({ data }) => { });"],"description": "httpPOST 请求"}

前端实现——删除

删除需要实现的功能

  • 使用ELement-UI 设置一个提示框
    • 确定:进行删除操作
    • 取消: 不进行删除操作
  • 删除完,刷新页面的时候,仍然保留在删除菜单的展示页面。

1、设置默认展开的菜单,也就是说,删除哪个菜单,删除成功刷新页面后,展示删除菜单的父级菜单

:default-expanded-keys="expandedKeys"

image-20221226175223936

在 data 中进行绑定:

      // 删除菜单的父菜单IDexpandedKeys: [],

image-20221226175307355

2、发送删除请求

删除成功后:

  • 重新刷新页面,展示菜单
  • 设置删除菜单的父级菜单ID
remove(node, data) {console.log("node:", node, "data: ", data);this.$confirm(`是否删除【${data.name}】 菜单`, "提示", {confirmButtonText: "确定",cancelButtonText: "取消",type: "warning"})//  点击确定执行 then.then(() => {var ids = [data.catId];this.$http({url: this.$http.adornUrl("/product/category/delete"),method: "post",data: this.$http.adornData(ids, false)}).then(({ data }) => {// 删除成功this.$message({type: "success",message: "删除成功!"});// 重新获取菜单this.getMenus();// 设置删除菜单的父菜单IDthis.expandedKeys = [node.parent.data.catId]});})// 点击取消删除执行catch方法.catch(() => {this.$message({type: "info",message: "已取消删除"});});}

三、分类增加

前端实现

在点击 Append 时,弹出一个对话框,在对话框输入增加分类的信息,然后增加到数据库中。

image-20221226181817826

1、增加对话框

    取 消确 定

2、在 data 中绑定数据

      category: {name: "",parentCid: "",catLevel: "",showStatus: 1,sort: 0},// 对话框dialogVisible: false,

image-20221226203821882

3、向后端发送请求

  // 打开对话框append(data) {console.log("append:", data);//   打开对话框this.dialogVisible = true;// 当前菜单的父级菜单IDthis.category.parentCid = data.catId;// 当前菜单IDthis.category.catLevel = data.parentCid * 1 + 1;},// 增加分类菜单实现addCategory() {console.log("提交的分类菜单: ", this.category);this.$http({url: this.$http.adornUrl("/product/category/save"),method: "post",data: this.$http.adornData(this.category, false)}).then(({ data }) => {this.$message({type: "success",message: "菜单保存成功!"});// 关闭对话框this.dialogVisible = false// 重新获取菜单this.getMenus();// 设置删除菜单的父菜单IDthis.expandedKeys = [this.category.parentCid];});},

后端的增加功能,逆向工程已经帮忙生成了:

image-20221226204610900

四、分类修改

思路分析:

修改操作无非就俩步:

  • 回显修改的数据
  • 修改

在页面中,修改和增加分类使用同一个对话框,如何区分是修改还是增加呢?

  • 可以使用一个标识进行判断,在进行修改、增加时为这个标识设置不同的值。

1、分类菜单中增加修改按钮

           edit(data)">Edit

2、修改对话框

  • 增加了 图标、计量单位 俩个输入框。
  • 动态修改 title 的值,如果是edit设置为 “修改菜单” ,如果是 add 设置为 “增加菜单”
  • 点击 “确定” 通过 submitData 方法判断是修改还是增加
   取 消确 定

3、data 中绑定属性

  • 对话框主要是根据 dialogType 的值判断是修改还是增加
      // 表单标题title: "",// 判断是修改还是增加dialogType: "add",category: {name: "",parentCid: "",catLevel: "",showStatus: 1,sort: 0,icon: "",productUnit: "",catId: null},

image-20221226214305468

4、submitData 方法: 用于判断提交的数据是发送修改请求还是增加请求

    //  用于判断提交的数据是发送修改请求还是增加请求submitData() {if (this.dialogType == "edit") {this.editCategory();}if (this.dialogType == "add") {this.addCategory();}},

5、edit 方法:设置对话框title、回显修改菜单的数据

在点击 “edit” 按钮时,向后端发送一次请求获取当前修改菜单的最新数据。避免由于多人操作可能造成数据不一致问题。

    // 修改edit(data) {//  设置标识为修改this.dialogType = "edit";this.title = "修改菜单";//   打开对话框this.dialogVisible = true;// 获取当前菜单的最新数据this.$http({url: this.$http.adornUrl(`/product/category/info/${data.catId}`),method: "get",params: this.$http.adornParams({})}).then(({ data }) => {console.log("当前菜单的最新数据: ", data);this.category.catId = data.data.catId;this.category.name = data.data.name;this.category.icon = data.data.icon;this.category.productUnit = data.data.productUnit;// 设置删除菜单的父菜单IDthis.expandedKeys = [data.data.parentCid];});},

获取后端返回数据时,注意返回的名称,我是修改成了 data,如果没修改默认是: category

image-20221226214742089

6、editCategory方法: 发送修改请求,完成修改操作

    // 发送修改请求editCategory() {// 解构表达式var { catId, name, icon, productUnit } = this.category;this.$http({url: this.$http.adornUrl("/product/category/update"),method: "post",data: this.$http.adornData({ catId, name, icon, productUnit }, false)}).then(({ data }) => {this.$message({type: "success",message: "菜单修改成功!"});// 关闭对话框this.dialogVisible = false;// 重新获取菜单this.getMenus();});},

7、修改 append 方法: 清空由于修改保存的数据,设置对话框标题,设置标识…

  // 增加分类菜单append(data) {this.title = "增加菜单";//   console.log("append:", data);//  设置标识为增加this.dialogType = "add";//   打开对话框this.dialogVisible = true;// 当前菜单的父级菜单IDthis.category.parentCid = data.catId;// 当前菜单IDthis.category.catLevel = data.parentCid * 1 + 1;// 清空修改数据时的表单内容this.category.catId = null;this.category.name = "";this.category.icon ="";this.category.productUnit = "";this.category.showStatus = 1;this.category.sort = 0;},

五、拖拽菜单

拖拽效果实现

思路分析:

菜单一共有三级菜单,如果超过三级菜单就不允许拖拽,分为三种情况:

  • 拖拽菜单在放置目标菜单的上方
  • 拖拽菜单在放置目标菜单的下方
  • 拖拽菜单在放置目标菜单的内部

针对第一、二种情况,我们只需要判断被拖拽菜单的深度以及目标菜单的父级菜单的层级之和是否大于3。

假设将 手机 拖拽到 大家电的上边,手机 的深度为3,大家电的父级菜单家用电器的层级为1, 加在一起为4,则不允许拖拽

这里需要了解: 层级与深度的关系

层级: 就是 catLevel 的值,几级菜单,比如: 手机是一级菜单,层级就是 1

深度: 菜单的深度,比如家用电器的深度就是3

image-20221227155438239

针对第三种情况,需要判断被拖拽菜单的深度,以及目标菜单的层级之和

假设我们将 电子书刊 拖拽到 音像 内部,电子书刊的深度是2,音像的层级也是2,加在一起层级为4,就不允许拖拽。

image-20221227151105991

被拖拽节点的深度 = 被拖拽节点的最大深度 - 被拖拽节点的层级 + 1

比如: 电子书刊的最大深度是3 - 电子书刊的层级 2 + 1 =2

电子书刊真实的深度则为 2

因此我们现在的核心 就是求出被拖拽节点的最大深度 目标菜单的层级以及目标菜单的父级菜单的层级都是现成的。

1、增加可拖拽的选项

image-20221227161645012

2、allowDrop 方法 有三个参数: draggingNode, dropNode, type,返回true允许拖拽,返回false不允许拖拽

  • draggingNode 被拖拽的当前节点(节点==菜单)
  • dropNode 放置的目标节点
  • type 放置目标节点的位置

draggingNode、dropNode这俩个对象中保存了目标菜单的层级以及目标菜单的父级菜单的层级

image-20221227161859066

3、求出被拖拽节点的最大深度

可以根据被拖拽节点 draggingNode 对象中的 childrenLevel 来求得,遍历节点中的子节点

   //  求出当前被拖拽节点的最大深度countNodeLevel(node) {// 判断是否有子节点if (node.childrenLevel != null && node.childrenLevel.length > 0) {for (let i = 0; i < node.childrenLevel.length; i++) {// 子节点的层级if (node.childrenLevel[i].catLevel > this.maxLevel) {this.maxLevel = node.childrenLevel[i].catLevel;}//  继续递归查找是否有子节点this.countNodeLevel(node.childrenLevel[i]);}}else {// 如果没有子节点,将最大深度设置为当前层级this.maxLevel = node.catLevel}},

4、allowDrop方法

 allowDrop(draggingNode, dropNode, type) {// 当前被拖拽节点最大深度this.countNodeLevel(draggingNode.data);//  当前被拖拽节点的真实深度let nodeDeep = this.maxLevel - draggingNode.data.catLevel + 1;console.log(`${draggingNode.data.name}的深度`, nodeDeep);if (type == "inner") {this.maxLevel = 0// 被拖拽节点的深度,以及目标节点的层级之和return nodeDeep + dropNode.level <= 3;} else {this.maxLevel = 0// 被拖拽菜单的深度以及目标菜单的父级菜单的层级之和return nodeDeep + dropNode.parent.level <= 3;}},

拖拽数据收集

上面实现了页面的拖拽效果,需要将修改节点的信息收集起来,并通知后端修改数据库中的信息。

首先在标签内使用 @node-drop 事件,拖拽成功后执行 handleDrop 方法。

image-20221227212556440

对于 handleDrop 有默认的四个参数

  • draggingNode 正在拖拽的节点
  • dropNode 目标节点
  • dropType 拖拽的节点放在目标节点的位置(before、after、inner)
  • ev 事件
handleDrop(draggingNode, dropNode, dropType, ev) {}

对于拖拽节点需要修改以下三个信息:

  • 当前节点的父节点ID
  • 当前节点的层级变化
  • 当前节点的最新排序

image-20221227180148872

而对于拖拽节点的位置不同,这些信息的变化又是不一样的:

1、找到当前节点的父节点ID

(1)如果当前节点在目标节点的 before/after, 那么当前节点的父节点ID就是目标节点的父节点ID

比如:如果我们将 运行商 节点放在手机通讯的前面,那么运行商父节点的ID就是手机通讯父节点 手机 的ID

image-20221227195022918

可以通过dropNode对象找到,手机的 catId=2

image-20221227195243939

(2)如果如果当前节点在目标节点的 inner, 那么当前节点的父节点ID就是目标节点的ID

比如:将手机放到运行商内部,那么手机的父节点ID就是运营商的ID

image-20221227195448655

其中 运营商 的ID也可以通过 dropNode 找到:

image-20221227195626040

2、找到当前节点以及兄弟节点的信息

(1)如果当前节点放在目标节点的 before/after,那么当前节点以及兄弟节点的信息在目标节点的父节点的子节点中。

image-20221227205325291

比如:将手机配件放在手机通讯的前面

那么手机配件以及它的兄弟节点都保存在 dropNode.parent.childNodes 里,利用这个信息就可以收集正在拖拽节点的信息。

image-20221227205735866

(2) 如果当前节点放在目标节点的 inner, 那么当前节点以及兄弟节点信息在目标节点的子节点中

image-20221227210243058

比如:将合约机放在手机通讯内部,那么合约机 以及它兄弟节点的信息保存在了 dropNode.childNodes 里

image-20221227210445058

以上的逻辑转化为代码为

    handleDrop(draggingNode, dropNode, dropType, ev) {//  1、拖拽节点的父节点IDlet pCid = 0;// 正在拖拽节点的兄弟节点let sublings = null;if(dropType == 'inner') {// 当前节点的父节点iD就是目标节点的IDpCid = dropNode.data.catId// 2、当前节点的兄弟节点sublings = dropNode.childNodes}else {// 当前节点的父节点iD就是目标节点父节点的IDpCid = dropNode.parent.data.catId// 2、当前节点的兄弟节点sublings = dropNode.parent.childNodes}}

3、当前节点的最新顺序

找到了当前正在拖拽的节点以及兄弟节点的信息,那么就需要遍历这些节点,为她们重新排序并且保存到数据库中。值得注意的是,当前正在拖拽的节点除了要设置顺序还要重新设置它的父节点ID。

转化为代码

updateNodes定义在data中,保存拖拽节点以及兄弟节点的最新顺序:

image-20221227212933235

// 3、找到兄弟节点信息后,重新遍历排序for(let i =0; i< sublings.length;i++) {// 正在遍历的节点是正在拖拽的节点if (sublings[i].data.catId == draggingNode.data.catId) {// 正在拖拽的节点不仅要设置顺序,还要设置父节点IDthis.updateNodes.push({catId:sublings[i].data.catId,sort:i,parentCid:pCid})}else {// 其他兄弟节点只需要设置节点id和顺序this.updateNodes.push({catId:sublings[i].data.catId,sort:i})}}

问题一

加入 将 电子书刊 放到 图书、音像、电子书刊的 上面

image-20221227213339393

可以看见 电子书刊 的父节点ID竟然是undefined

image-20221227213429809

这是由于,由于他放在了图书、音像、电子书刊的 上面,父节点ID = dropNode.parent.data.catId

image-20221227213536946

而 dropNode.parent.data 竟然是一个数组,没有 catId 属性,自然是 undefined 了。

image-20221227213642273

修改:此时我们只需要加一个三目运算符,当拖拽成根节点时,设置父节点ID为0

image-20221227213819833

效果:不再是 undefined 了,而是 0

image-20221227213922561

4、找到拖拽节点的层级变化

拖拽节点的父节点ID、顺序都找到了,但是他的层级变化也得更新。那么这个层级变化关系如何找到呢?

如果我们将电子书刊放到图书、音像、电子书刊的 上面, 那么电子书刊的层级由原来的 2 变成了 1,并且 电子书刊 里的子节点也发生了变化。

image-20221227215043969

电子书刊 原始的层级保存在 draggingNode 的 level 属性中,变化后的层级也可以找到,在第二步的时候,我们将拖拽节点的信息保存到了 sublings 数组中。

image-20221227220335189

找到了正在拖拽节点的层级变化,那么正在拖拽的节点还有可能有子节点,子节点的层级也会发生变化。

而子节点的层级变化保存在 sublings[i].childNode[i].level中,值得注意的是,子节点的层级是需要递归修改的。

image-20221227222601971

完整的代码

   // 拖拽成功后,收集数据发送给后端进行修改handleDrop(draggingNode, dropNode, dropType, ev) {//  1、拖拽节点的父节点IDlet pCid = 0;// 正在拖拽节点的兄弟节点let sublings = null;if (dropType == "inner") {// 当前节点的父节点iD就是目标节点的IDpCid = dropNode.data.catId;// 2、当前节点的兄弟节点sublings = dropNode.childNodes;} else {// 当前节点的父节点iD就是目标节点父节点的IDpCid =dropNode.parent.data.catId == undefined? 0: dropNode.parent.data.catId;// 2、当前节点的兄弟节点sublings = dropNode.parent.childNodes;}// 3、找到兄弟节点信息后,重新遍历排序for (let i = 0; i < sublings.length; i++) {// 正在遍历的节点是正在拖拽的节点if (sublings[i].data.catId == draggingNode.data.catId) {// 4、修改正在拖拽节点的层级// 正在拖拽节点的原始层级let catLevel = draggingNode.level;if (catLevel != sublings[i].level) {// 正在拖拽节点层级发生变化catLevel = sublings[i].level;// 修改当前节点的子节点的层级this.updateChildNodeLevel(sublings[i]);}// 正在拖拽的节点不仅要设置顺序,还要设置父节点IDthis.updateNodes.push({catId: sublings[i].data.catId,sort: i,parentCid: pCid,catLevel: catLevel});} else {// 其他兄弟节点只需要设置节点id和顺序this.updateNodes.push({ catId: sublings[i].data.catId, sort: i });}}console.log("updateNodes:", this.updateNodes);},//  修改正在拖拽节点的子节点的层级关系updateChildNodeLevel(node) {if (node.childNodes.length > 0) {// 遍历子节点for (let i = 0; i < node.childNodes.length; i++) {// 当前节点的子节点let cNode = node.childNodes[i].data;// 修改子节点的catId以及它的层级this.updateNodes.push({catId: cNode.catId,catLevel: node.childNodes[i].level});this.updateChildNodeLevel(node.childNodes[i])}}},

结果验证

手机通讯 放在 图书、音像、电子书刊 上面

image-20221227223709214

最终修改的信息:

image-20221227223840885

和数据库中的 cat_level 作对比,手机、对讲机、1111的层级由3变成 了2. 效果正确。

image-20221227223906545

拖拽功能完成

在上一步,完成了拖拽节点数据的收集,需要前端发送请求给后端对数据库完成修改

1、CategoryController

    /*** 批量修改*/@RequestMapping("/update/sort")// @RequiresPermissions("product:category:update")public R updateSort(@RequestBody CategoryEntity[] category){// categoryService.updateById(category);categoryService.updateBatchById(Arrays.asList(category));return R.ok();}

2、收集完数据后,前端发送请求

      // 发送请求this.$http({url: this.$http.adornUrl("/product/category/update/sort"),method: "post",data: this.$http.adornData(this.updateNodes, false)}).then(({ data }) => {this.$message({type: "success",message: "菜单拖拽成功!"});// 重新获取菜单this.getMenus();// 设置删除菜单的父菜单IDthis.expandedKeys = [pCid];});},

image-20221227225935739

拖拽功能完善

1、为拖拽功能增加一个开关按钮

(1)使用 El 拖拽开关

    

(2)将 draggable 设置成动态的,在 data 中声明

image-20221228152402170

image-20221228152350470

2、设置批量保存的按钮,等待所有的拖拽完成之后,在向后端发送请求

(1) 增加按钮

批量保存

(2) 点击 批量保存 时,发送请求,修改数据库

    // 批量拖拽batchSave() {// 发送请求this.$http({url: this.$http.adornUrl("/product/category/update/sort"),method: "post",data: this.$http.adornData(this.updateNodes, false)}).then(({ data }) => {this.$message({type: "success",message: "菜单拖拽成功!"});// 重新获取菜单this.getMenus();// 设置展示的菜单this.expandedKeys = this.pCid;// 清空this.updateNodes = [];this.maxLevel = 0;});},

(3) 将 pCid 改成全局属性,在data中声明,现在我们是批量拖拽,有可能需要展示多个菜单。因此父ID还有可能是多个,改成数组。

image-20221228153855039

(4) 在 handleDrop 方法中,对 pCid 赋值

image-20221228153935073

(5) 之前我们拖拽一个菜单就修改一次,菜单的层级都是最新的,而现在 由于批量拖拽,拖拽完成后同意发送请求修改,因此与可能菜单的层级发生变化。

所以在我们计算菜单最大深度时,不在使用数据库中的层级,而是使用ELement-UI帮我们封装好的层级。

image-20221228154434421

将从数据库中查询的数据都改成Element——UI帮忙封装好的

node.childrenLevel 改成 node.childNodes

node.childrenLevel[i].catLevel 改成 node.childNodes[i].level

image-20221228154515650

    //  求出当前被拖拽节点的最大深度countNodeLevel(node) {// 判断是否有子节点if (node.childNodes != null && node.childNodes.length > 0) {for (let i = 0; i < node.childNodes.length; i++) {// 子节点的层级if (node.childNodes[i].level > this.maxLevel) {this.maxLevel = node.childNodes[i].level;}//  继续递归查找是否有子节点this.countNodeLevel(node.childNodes[i]);}} else {// 如果没有子节点,将最大深度设置为当前层级this.maxLevel = node.level;}},

六、批量删除

1、增加删除按钮

批量删除

2、获取批删除的菜单

(1)为 树形菜单 增加标识

image-20221228161312455

(2)可通过 this.$refs.menuTree 获取到树形菜单,根据 getCheckedNodes 方法可获取到选择的菜单

具体的删除流程:

  batchDelete() {// 保存被删除节点的IDlet ids = [];let menuNames = [];// 获取被删除的节点let removeNodes = this.$refs.menuTree.getCheckedNodes();// 遍历节点,获取catIdfor (let i = 0; i < removeNodes.length; i++) {ids.push(removeNodes[i].catId);menuNames.push(removeNodes[i].name);}this.$confirm(`是否要批量删除【${menuNames}】 菜单`, "提示", {confirmButtonText: "确定",cancelButtonText: "取消",type: "warning"}).then(() => {this.$http({url: this.$http.adornUrl("/product/category/delete"),method: "post",data: this.$http.adornData(ids, false)}).then(({ data }) => {this.$message({type: "success",message: "批量删除成功"});this.getMenus();});}).catch(() => {this.$message({type: "info",message: "已取消删除"});});console.log("被删除的节点:", removeNodes);},

品牌管理

新增 品牌管理 菜单

image-20221228162230194

将 使用逆向工程生成好的 前端代码 直接拷贝到 product 目录下

image-20221228162253904

新创、建的菜单是没有增加和修改按钮的,需要修改权限

image-20221228162704670

一、效果优化及快速显示开关

品牌的显示状态希望使用按钮来表示是否显示,可以使用 Element-UI 提供的组件

image-20221228163528306

image-20221228163535629

(1) 使用 ELement-UI 中Table表格提供的自定义模板,template 模板里可组合其他组件使用

修改 brand.vue 页面中的 显示状态 :

        

image-20221228164158771

(2) 同样修改 brand-add-or-update.vue 中的显示状态

      

(3) 修改完显示状态,发送请求修改数据库信息

1)、修改开关标签

  • active-value 表示打开 开关 传递的值
  • inactive-value 关闭 开关 传递的值
  • @change 监听开关的变化,一有变化就执行updateshowStatus 方法。
  • scope.row 获取表格的内部信息
        

2)、updateshowStatus 方法

    //  更新品牌显示状态updateshowStatus(data) {console.log("品牌的最新信息:", data);// 解构表达式let { brandId, showStatus } = data;this.$http({url: this.$http.adornUrl("/product/brand/update"),method: "post",data: this.$http.adornData({ brandId, showStatus }, false)}).then(({ data }) => {this.$message({type: "success",message: "状态更新成功"});});},

二、品牌Logo上传

将 新增 品牌的Logo改成文件上传的样式。

image-20221228170658779

阿里云OSS

项目的文件存储使用的是阿里云OSS:OSS管理控制台 (aliyun.com)

文件的上传方式

普通上传:上传的文件需要经过服务器,通过服务器上传到OSS,当请求多的时候,这种方式给服务器造成了很大的压力,本项目中不使用此方式

image-20221228173828945

服务端签名后直传: 前端上传文件到OSS之前,后端只需要通过账号、密码生成一个签名(密钥、上传地址…),前端收到这个签名之后直接上传到 阿里云OSS,而阿里云自己会判断签名是否合法。项目中使用该方式

image-20221228172230158

创建 Bucket

image-20221228172135866image-20221228172145886

整合阿里云OSS

阿里云OSS帮助文档:OSS · alibaba/spring-cloud-alibaba Wiki (github.com)

1、创建模块 gulimall-third-party 专门管理第三方服务

image-20221228184150538

image-20221228184229766

2、POM依赖


4.0.0org.springframework.bootspring-boot-starter-parent2.1.8.RELEASE com.atguigu.gulimallgulimall-third-party0.0.1-SNAPSHOTgulimall-third-party管理第三方服务1.8Greenwich.SR3com.atguigu.gulimallgulimall-common0.0.1-SNAPSHOTcom.baomidoumybatis-plus-boot-startermysqlmysql-connector-javacom.alibaba.cloudspring-cloud-starter-alicloud-ossorg.springframework.bootspring-boot-starter-weborg.springframework.cloudspring-cloud-starter-openfeignorg.springframework.bootspring-boot-starter-testtestorg.springframework.cloudspring-cloud-dependencies${spring-cloud.version}pomimportcom.alibaba.cloudspring-cloud-alibaba-dependencies2.1.0.RELEASEpomimportorg.springframework.bootspring-boot-maven-plugin

3、bootstrap.properties

spring.cloud.nacos.config.server-addr=localhost:8848
spring.cloud.nacos.config.namespace=3670d9fb-6e4d-4f9a-bf33-5993095fbe2cspring.cloud.nacos.config.ext-config[0].data-id=oss.yaml
spring.cloud.nacos.config.ext-config[0].group=DEFAULT_GROUP
spring.cloud.nacos.config.ext-config[0].refresh=true

application.yaml


spring:cloud:nacos:discovery:server-addr: localhost:8848application:name: gulimall-third-party
server:port: 30000

Nacos配置中心创建 oss.yaml

spring:cloud:alicloud:access-key: Your Alibaba Cloud AKsecret-key: Your Alibaba Cloud SKoss:endpoint: ***.aliyuncs.com

4、主启动类

@SpringBootApplication
@EnableDiscoveryClient
public class GulimallThirdPartyApplication {public static void main(String[] args) {SpringApplication.run(GulimallThirdPartyApplication.class, args);}}

3、在使用时只需要注入 OSS 即可

@SpringBootTest
@RunWith(SpringRunner.class)
public class GulimallProductApplicationTests {@Autowiredprivate OSS ossClient;@Testpublic void test() throws FileNotFoundException {ossClient.putObject("gulimall-bucket-2022", "0d40c24b264aa511.jpg", new FileInputStream("C:\\Java\\java_notes\\其他\\project\\谷粒商城\\资料\\docs\\pics\\0d40c24b264aa511.jpg"));}
}

获取服务端签名

阿里云文档:Java (aliyun.com)

OssController :

@RestController
public class OssController {@Autowiredprivate OSS ossClient;@Value("${spring.cloud.alicloud.access-key}")private String accessId;@Value("${spring.cloud.alicloud.oss.endpoint}")private String endpoint;@Value("${spring.cloud.alicloud.oss.bucket}")private String bucket;@RequestMapping("/oss/policy")public R policy() {// 填写Host地址,格式为https://bucketname.endpoint。String host = "https://" + bucket + "." + endpoint;// 设置上传回调URL,即回调服务器地址,用于处理应用服务器与OSS之间的通信。OSS会在文件上传完成后,把文件上传信息通过此回调URL发送给应用服务器。// String callbackUrl = "https://192.168.0.0:8888";// 设置上传到OSS文件的前缀,可置空此项。置空后,文件将上传至Bucket的根目录下。String date = new SimpleDateFormat("yyyy-MM-dd").format(new Date());String dir = date + "/";Map respMap = null;try {long expireTime = 300;long expireEndTime = System.currentTimeMillis() + expireTime * 1000;Date expiration = new Date(expireEndTime);PolicyConditions policyConds = new PolicyConditions();policyConds.addConditionItem(PolicyConditions.COND_CONTENT_LENGTH_RANGE, 0, 1048576000);policyConds.addConditionItem(MatchMode.StartWith, PolicyConditions.COND_KEY, dir);String postPolicy = ossClient.generatePostPolicy(expiration, policyConds);byte[] binaryData = postPolicy.getBytes("utf-8");String encodedPolicy = BinaryUtil.toBase64String(binaryData);String postSignature = ossClient.calculatePostSignature(postPolicy);respMap = new LinkedHashMap();respMap.put("accessId", accessId);respMap.put("policy", encodedPolicy);respMap.put("signature", postSignature);respMap.put("dir", dir);respMap.put("host", host);respMap.put("expire", String.valueOf(expireEndTime / 1000));// respMap.put("expire", formatISO8601Date(expiration));// JSONObject jasonCallback = new JSONObject();// jasonCallback.put("callbackUrl", callbackUrl);// jasonCallback.put("callbackBody",//         "filename=${object}&size=${size}&mimeType=${mimeType}&height=${imageInfo.height}&width=${imageInfo.width}");// jasonCallback.put("callbackBodyType", "application/x-www-form-urlencoded");// String base64CallbackBody = BinaryUtil.toBase64String(jasonCallback.toString().getBytes());// respMap.put("callback", base64CallbackBody);//// JSONObject ja1 = JSONObject.fromObject(respMap);// // System.out.println(ja1.toString());// response.setHeader("Access-Control-Allow-Origin", "*");// response.setHeader("Access-Control-Allow-Methods", "GET, POST");// response(request, response, ja1.toString());} catch (Exception e) {// Assert.fail(e.getMessage());System.out.println(e.getMessage());}return R.ok().put("data",respMap);}
}

测试结果:

{"accessId": "LTAI5t9kg6KAfWsEzFKszdgD","policy": "eyJleHBpcmF0aW9uIjoiMjAyMi0xMi0yOFQxMTo0ODoxOC44MDhaIiwiY29uZGl0aW9ucyI6W1siY29udGVudC1sZW5ndGgtcmFuZ2UiLDAsMTA0ODU3NjAwMF0sWyJzdGFydHMtd2l0aCIsIiRrZXkiLCIyMDIyLTEyLTI4LyJdXX0=","signature": "0B9txV+OfXM+nkIkV3QOSn3thEo=","dir": "2022-12-28/","host": "https://oss-cn-hangzhou.aliyuncs.com.oss-cn-hangzhou.aliyuncs.com","expire": "1672228098"
}

配置网关:

                - id: third_party_routeuri: lb://gulimall-third-party # 负载均衡predicates:- Path=/api/thirdparty/**filters:- RewritePath=/api/thirdparty/?(?.*), /$\{segment}  #路径重写

前后端联调上传文件

/components/upload/multiUpload.vue:多文件上传



/components/upload/singleUpload.vue:单文件上传

修改文件上传地址:action



/components/upload/policy

import http from '@/utils/httpRequest.js'
export function policy() {return  new Promise((resolve,reject)=>{http({url: http.adornUrl("/thirdparty/oss/policy"),method: "get",params: http.adornParams({})}).then(({ data }) => {resolve(data);})});
}

在阿里云OSS里配置跨域规则:

image-20221228210254372

image-20221228210326433

如果配置了跨域规则还是报错 403

image-20221229154645827

并且发现向OSS发送的数据 keyId 没有

image-20221229154724551

将 singleUpload.vue、multiUpload.vue 中的 accessid 改成 accessId ,与服务端发送的签名所对应。

image-20221229154804480

三、前端表单校验

1、修改brand-add-update.vue 中的 显示状态 ,变化值改为 0,1

image-20221229160125409

2、 修改 brand.vue 中的品牌logo 显示

image-20221229161018231

        

3、对新增表单中的 检索首字母、排序 进行校验

image-20221229161938361

校验规则

  • 检索首字母:必须在a~z 或 A~Z 之间,并且只能输入一个
  • 排序:必须是大于0的整数

使用ELement-UI提供的自定义表单校验规则:组件 | Element

在 el-form表单中增加 rules 可增加校验规则

image-20221229163126213

brand-add-update.vue 中的 dataRule 中设置校验规则:

v-model.number 只能输入数字

image-20221229163635522

  firstLetter: [{validator: (rule, value, callback) => {if (value == "") {callback(new Error("检索首字母不能为空"));} else if (!/^[a-zA-Z]$/.test(value)) {callback(new Error("检索首字母必须在a~z或A~Z之间,并只能有一位"));} else {callback();}},trigger: "blur"}],sort: [{validator: (rule, value, callback) => {if (!value) {callback(new Error("排序不能为空"));} else if (!Number.isInteger(value) || value < 0) {callback(new Error("排序必须是大于0的正整数"));} else {callback();}},trigger: "blur"}]

四、后端数据验证

JSR303

JSR是Java Specification Requests的缩写,意思是Java 规范提案。是指向JCP(Java Community Process)提出新增一个标准化技术规范的正式请求。任何人都可以提交JSR,以向Java平台增添新的API和服务。JSR已成为Java界的一个重要标准。

如何使用 JSR303

javax.validation.constraints 中定义了非常多的校验注解

  • 在需要校验的属性上增加注解
  • 使用 @Valid 开启校验功

可以使用 BindingResult 提取校验错误信息 ,这个属性必须紧跟着开启校验的 JavaBean

image-20221229165631210

案例演示

1、在 BrandEntity 品牌实体类中的 name 增加校验注解。

@NotBlank:字段不能为空

message: 自定义错误信息

image-20221229170054475

2、在 Controller 层使用

@Valid : 开启校验功能

    /*** 保存* @Valid 开启校验功能* BindingResult 提取校验错误信息。必须紧跟着校验的JavaBean*/@RequestMapping("/save")// @RequiresPermissions("com.atguigu.gulimall.product:brand:save")public R save(@RequestBody @Valid BrandEntity brand){brandService.save(brand);return R.ok();}

3、使用PostMan测试

image-20221229181356008

提取校验错误信息

JSR校验的错误提示信息保存在 ValidationMessages.properties 配置文件里,包括有中文的提示信息

image-20221229182139566

也可以通过 校验注解里的 message 属性自定义错误提示信息。在 代码中可以通过BindingResult 提取出来

  /*** 保存* @Valid 开启校验功能* BindingResult 提取校验错误信息。必须紧跟着校验的JavaBean*/@RequestMapping("/save")// @RequiresPermissions("com.atguigu.gulimall.product:brand:save")public R save(@RequestBody @Valid BrandEntity brand, BindingResult result) {// 是否有错误信息if (result.hasErrors()) {// 获取所有的错误List errors = result.getFieldErrors();HashMap map = new HashMap<>();errors.forEach(item -> {// 错误信息String message = item.getDefaultMessage();// 错误的属性String field = item.getField();map.put(field, message);});return R.error(400,"数据提交不合法").put("data", map);} else {brandService.save(brand);}return R.ok();}

使用 PostMan 测试:

image-20221229184308223

统一错误状态码

使用枚举类统一设置返回的错误状态码,错误状态码的规则:

/***
* 错误码和错误信息定义类
* 1. 错误码定义规则为 5 为数字
* 2. 前两位表示业务场景,最后三位表示错误码。
* 		例如:100001。10:通用 001:系统未知异常
* 3. 维护错误码后需要维护错误描述,将他们定义为枚举形式
* 错误码列表:
* 10: 通用
* 	001:参数格式校验
* 11: 商品
* 12: 订单
* 13: 购物车
* 14: 物流
*/

将枚举类放在 common 模块

public enum BizCodeEnum {UNKNOW_EXCEPTION(10000,"未知的系统异常"),VALID_EXCEPTION(10001,"数据校验异常");private Integer code ;private String message;private BizCodeEnum(Integer code, String message) {this.code = code;this.message = message;}public Integer getCode() {return code;}public String getMessage() {return message;}
}

统一异常处理

放在 gulimall-product 模块中

@Slf4j
@RestControllerAdvice(basePackages = "com.atguigu.gulimall.product.controller") // == @RestController + ControllerAdvice
public class GuliHandleExceptionAdvice {@ExceptionHandler(MethodArgumentNotValidException.class)public R handleValidaException(MethodArgumentNotValidException e) {HashMap map = new HashMap<>();List errors = e.getBindingResult().getFieldErrors();errors.forEach(fieldError -> {map.put(fieldError.getField(),fieldError.getDefaultMessage());});log.error("错误信息:{},错误类:{}",e.getMessage(),e.getClass());return R.error(BizCodeEnum.VALID_EXCEPTION.getCode(),BizCodeEnum.VALID_EXCEPTION.getMessage()).put("data",map);}
}

使用PostMan 测试:

image-20221229191040807

分组校验

对于某些字段可能会有不同的校验规则,比如:品牌ID在修改时比如传入,而在增加时没必要传入。这时就可以使用分组校验。

image-20221229194339267

每一个校验的注解都有一个 groups 的属性, 可以进行分组。

使用步骤:

1、在 common 模块中创建AddGroup、UpdateGroup俩个接口,接口仅仅起到标识作用,使用同一接口的属性被认为是同一组。

2、在校验规则中的groups属性中使用接口区分不同的组。如果其他校验注解如果不指明分组,那么校验没有效果。比如:图中的 @NotBlank

image-20221229194943846

3、在开启校验时,使用 @Validated 注解标注使用哪个分组

  /*** 保存* @Valid 开启校验功能* BindingResult 提取校验错误信息。必须紧跟着校验的JavaBean* 使用同一异常处理捕捉娇艳异常*/@RequestMapping("/save")// @RequiresPermissions("com.atguigu.gulimall.product:brand:save")public R save(@RequestBody @Validated(value = AddGroup.class) BrandEntity brand /*,BindingResult result*/) {brandService.save(brand);return R.ok();}

4、使用 PostMan 测试

虽然name使用校验注解,但是没有设置 groups ,因此是没有效果的。

image-20221229212011221

5、对品牌实体类的所有属性进行校验

@Data
@TableName("pms_brand")
public class BrandEntity implements Serializable {private static final long serialVersionUID = 1L;/*** 品牌id*/@TableId@Null(message = "新增时不能指定ID",groups = {AddGroup.class})@NotNull(message = "修改时必须指定ID",groups = {UpdateGroup.class})private Long brandId;/*** 品牌名*/@NotBlank(message = "品牌名成不能为空",groups = {AddGroup.class,UpdateGroup.class})private String name;/*** 品牌logo地址*/@NotBlank(message = "logo地址不能为空",groups = {AddGroup.class})@URL(message = "url地址不合法",groups = {AddGroup.class,UpdateGroup.class})private String logo;/*** 介绍*/private String descript;/*** 显示状态[0-不显示;1-显示]*/private Integer showStatus;/*** 检索首字母*  使用正则校验*/@Pattern(regexp = "^[a-zA-Z]$",message = "检索首字母必须在a~z或A~Z范围内,并且只有一位",groups = {AddGroup.class,UpdateGroup.class})@NotBlank(message = "检索首字母不能为空",groups = {AddGroup.class})private String firstLetter;/*** 排序*/@Min(value = 0,message = "排序必须是一个大于0的整数",groups = {AddGroup.class,UpdateGroup.class})private Integer sort;}

自定义校验注解

对于某些字段,java提供的校验注解并不能满足需求,因此需要我们自定义校验注解。

步骤

  • 自定义一个校验注解
  • 自定义校验解析器: 实现ConstraintValidator接口
  • 将校验解析器与校验注解相关联 @Constraint(validatedBy = { })

由于校验是公共的,放在common模块下:

1、自定义一个校验注解

@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = { ListValueConstraintValidator.class}) // 使用自定义解析器,可以使用多个
public @interface ListValue {// 校验失败提示信息String message() default "{com.atguigu.common.valid.ListValue.message}";// 分组Class[] groups() default { };Class[] payload() default { };// 以上三个是固定的// 指定的值int[] vals() default {};
}

2、创建 ValidationMessages.properties 配置文件,保存校验失败提示信息。

com.atguigu.common.valid.ListValue.message=提交的数据必须是指定值

3、创建自定义校验解析器

// 泛型中的俩个属性:第一个是自定义的注解,第二个是校验的参数类型
public class ListValueConstraintValidator implements ConstraintValidator {private Set set = new HashSet<>();/** constraintAnnotation 可以获取使用注解时的指定值*  {0,1}* */@Overridepublic void initialize(ListValue constraintAnnotation) {int[] vals = constraintAnnotation.vals();for (int val : vals) {set.add(val);}}/*** @description 判断校验是否通过* @date 2022/12/29 22:06* @param value 需要校验的值* @param context* @return boolean*/@Overridepublic boolean isValid(Integer value, ConstraintValidatorContext context) {// 如果指定的值中包含需要校验的值就返回 truereturn set.contains(value);}
}

4、自定义注解创建完毕,现在就可以使用了

image-20221229221655236

5、使用PostMan 测试

image-20221229221728946

五、SPU&SKU

SPU

SPU: Standard Product Unit(标准化产品单元)

是商品信息聚合的最小单位,是一组可复用、易检索的标准化信息的集合,该集合描述了一 个产品的特性。

SKU

SKU:Stock Keeping (库存量单位)

即库存进出计量的基本单元,可以是以件,盒,托盘等为单位。SKU 这是对于大型连锁超市 DC(配送中心)物流管理的一个必要的方法。现在已经被引申为产品统一编号的简称,每 种产品均对应有唯一的 SKU 号。

例如

不同的收集类型Apple13、14、12 这些都是 SPU

而每款收集的不同规格,Apple13 64G 紫色 ,Apple14 1TB 远峰蓝 这些不同的版本都属于SKU。

像是 java 中的类 与对象,类(SPU)中定义功能,真正想要使用还得创建对象(SKU)。

image-20221230145800244

基本属性【规格参数】与销售属性

对于不同的 SPU,也就是不同的手机,他的一些基本属性都是一样的。无非就是属性的值不同。

每款手机都有 主体、基本信息、存储、屏幕.... 这些属性,而这些就属于 SPU 的基本属性,也可以叫规格参数。

image-20221230150646283

而真正决定你要购买的都是这些 销售属性 , 它决定了手机销售的库存,这些都是 SKU 属性。

image-20221230150853101

总结

每个分类下的商品共享规格参数,与销售属性。只是有些商品不一定要用这个分类下全部的 属性;

  • 同一个SPU下的不同 SKU 都共同使用同一组属性
  • 属性名确定的,但是值是每一个商品不同来决定的
  • 也就是每个手机都有主体、基本信息、存储、屏幕.... 这些属性,但是值是不同的。
  • 属性是以三级分类组织起来的
  • 规格参数中有些是可以提供检索的
  • 规格参数也是基本属性,他们具有自己的分组
  • 属性的分组也是以三级分类组织起来的

SPU决定了商品的规格参数、SKU决定了商品的销售属性!!

商品表设计分析

针对以上这些概念,分析数据库的表中的结构

gulimall-pms 数据库中的 pms_attr 表,保存了商品的属性名:

image-20221230151640614

pms_attr_group表中保存了属性的分组信息,一个组里面保存了不同的属性。

image-20221230152039131

pms_attr_attrgroup_relation 表保存了 分组 与 属性的关联关系

image-20221230152305660

pms_product_attr_value 表中保存属性的值,将属性值与属性相关联。包括表中也将属性以及属性值与商品相关联。不同的商品有不同的属性与属性值。

image-20221230152552375

举例说明

主体、基本信息、存储、屏幕 这些信息都是一个个的分组

而每个分组,比如主体里面又包含了入网型号、机型、上市日期多个属性。每个属性的属性值都是不同的。

image-20221230152250209

而每一个属性的属性值,根据商品的不同,他的值也是不同的。也就是不同的 SPU,规格参数都是不同的。

pms_spu_info表中就保存了不同的 商品(spu) 信息

image-20221230152739386

每一个商品都有不同的销售属性,也就是一个SPU对应多个SKU,SPU与SKU 的对应关系都保存在pms_sku_info表中

image-20221230152933099

每一个 SKU 的展示图片都保存在 pms_sku_images 里面

image-20221230153408680

每一个SKU的属性、属性值都是不一样的,SKU的属性名、属性值都保存在 pms_sku_sale_attr_value 表中

image-20221230153844878

举例说明

一个SPU(Apple14)对应多个SKU属性(颜色、版本…),每一个SKU属性的属性值根据SPU的不同又是不一样的。

image-20221230154114807

数据库表关联图

每一个三级分类对应不同的属性分组 , 分组&属性关联表中将属性与分组相关联。

比如: 三级分类里有主体、屏幕属性分组,主体 分组里又有 内存、像素 俩个属性

image-20221230154346434

有一个商品,他的ID(spuId)为 1,对应属性表中有俩个属性 网络、像素 ,属性值分别为: 3000万、3G;4G;5G

一个商品(spu)对应俩个 销售属性(sku):

  • id为1的sku对应的属性内存,容量,属性值为:6G、128G

  • id为2的sku对应的属性内存,容量,属性值为:4G、64G

image-20221230154639185

平台属性

一、属性分组

点击不同的分类,展示出分类所属的 属性分组

image-20221230161023274

首先将 菜单表 导入 gulimall_admin 数据库

image-20221230161222048

组件抽取

1、将三级分类封装成一个公共模块,放在 /modules/common/category.vue



2、在 /product/attrgroup.vue 中引用三级菜单

使用的是Element-UI中的Layout布局



3、attrgroup.vue 引入属性分组表格

4、效果,但是希望点击某个三级分类时,自动向数据库中查询更新表格。

image-20221230164413718

父子组件交互

三级分类在 category.vue 中,属性分组表格在 attrgroup.vue 中,点击 三级分类 时,attrgroup.vue 就需要感知到点击了哪个三级分类。需要使用Vue中的父子组件交互。

1、为三级分类的 el-tree 增加点击事件。当节点被点击时会触发 nodeClick 回调函数。并且可以传递三个参数:

  • 第一个参数 : 保存了点击的菜单对象
  • 第二个参数 : 保存了对象所对应的节点信息
  • 第三个参数 : 保存了整个组件的信息

image-20221230170137023

image-20221230165604373

2、nodeClick 回调函数。通过 this.$emit 向父组件 attrgroup 传递事件

    // 节点被点击回调nodeClick(data,node,component) {console.log("category感知到节点被点击: " ,data,node,component)// 子组件向父组件传递信息// 第一个参数: 传递的事件名称// 第二个参数: 传递的数据this.$emit("tree-node-click",data,node,component)},

3、在父组件attrgroup 使用子组件时,设置传递的事件。

image-20221230171710488

4、当子组件 触发事件后,会像父组件传递 事件,父组件接收到后会调用 nodeClick 函数

    // 感知节点被点击nodeClick(data,node,component){console.log("category父组件感知节点被点击: " , data,node,component)console.log("点击节点ID:", data.catId)},

5、控制台输出效果

image-20221230171826875

获取分类属性分组

后端实现

image-20221230205200663

思路分析

  • 如果传递catelogId, 就根据catelogId 查询对应的分组属性
    • 并且如果key不为空,不仅要根据catelogId查询,还要加上搜索关键字key。
    • 如果 key 为空,只根据 catelogId 查询即可
  • 如果没有传递 catelogId,就查询所有的分组属性

代码实现

1、AttrGroupController

    /*** 列表* 查询三级分类所对应的分组属性*/@RequestMapping("/list/{catelogId}")//@RequiresPermissions("com.atguigu.gulimall.product:attrgroup:list")public R list(@RequestParam Map params,@PathVariable(required = false) Long catelogId){// PageUtils page = attrGroupService.queryPage(params);PageUtils page = attrGroupService.queryPage(params,catelogId);return R.ok().put("page", page);}

2、AttrGroupService

PageUtils queryPage(Map params, Long catelogId);

3、AttrGroupServiceImpl

  /** 根据分类ID查询所对应的分组属性* SELECT xx FROM pms_attr_group WHERE (catelog_id = ? AND (attr_group_id = ? OR attr_group_name LIKE ?))* */@Overridepublic PageUtils queryPage(Map params, Long catelogId) {String key = (String) params.get("key");QueryWrapper wrapper = new QueryWrapper();IPage page;if (!StringUtils.isEmpty(key)) {// 如果搜索关键字不为空,带上关键字搜索分组wrapper.and(queryWrapper -> {queryWrapper.eq("attr_group_id", key).or().like("attr_group_name", key);});}if (catelogId != 0) {// 分类ID不等于0,根据分类Id查询分组属性wrapper.eq("catelog_id", catelogId);}// 如果分类ID==0,查询所有的分组属性page = this.page(new Query().getPage(params),wrapper);return new PageUtils(page);}

前端实现

修改请求发送路径,携带 catId 参数。并且在点击的时候将 catId 获取到。

image-20221230211832634

分组新增&级联选择器

在新增时选择 所属分类 应该有一个下拉框,选择现有的分类。

image-20221230213014040

1、修改 attgroup-add-or-update.vue 页面,将所属分类id 输入框改为使用级联选择器

  • :options 可选项数据源,键名可通过 Props 属性配置
  • :props 配置选项
  • filterable:提供搜索
  • placeholder 选择框默认显示的内容
        

image-20221230214432986

2、data 中定义数据。

      props: {value: 'catId',label: 'name',children: 'childrenLevel'},// 展示的菜单categorys: [],
value指定选项的值为选项对象的某个属性值string‘value’
label指定选项标签为选项对象的某个属性值string‘label’
children指定选项的子选项为选项对象的某个属性值string‘children’

3、发送请求,获取展示的菜单

    // 获取所有菜单getCategorys() {this.$http({url: this.$http.adornUrl("/product/category/list/tree"),method: "get"}).then(({ data }) => {console.log("成功获取菜单数据:", data.data);this.categorys = data.data;});},

并且 页面渲染之前调用:

  created() {this.getCategorys()},

JsonInclude

4、效果出来了,但是三级菜单之后,是一片空白。

image-20221230215059651

这是因为在后端给我们返回来的数据,三级分类就没有子级菜单了,但是有一个空的子级菜单数组。Element-UI默认这也是一个菜单选项。因此就是空白的。

image-20221230215407431

需要在没有子级菜单的时候,就不要带上 childrenLevel 的数组了。在 childrenLevel 的属性上增加 @JsonInclude 注解

image-20221230215900445

该注解有几个属性:

JsonJsonInclude.Include.ALWAYS 	//表示总是序列化所有属性
JsonJsonInclude.Include.NON_NULL //表示序列化非null属性
JsonJsonInclude.Include.NON_ABSENT //表示序列化非null或者引用类型缺省值,例如java8的Optional类,这个选中通常与Optional一起使用
JsonJsonInclude.Include.NON_EMPTY  // 表示序列化非Empty的属性,例如空的集合不会被序列化
JsonJsonInclude.Include.NON_DEFAULT //仅包含与POJO属性默认值不同的值
JsonJsonInclude.Include.CUSTOM //由{@link JsonInclude#valueFilter}指定值本身,或由{@link JsonInclude#contentFilter}指定结构化类型的内容,由过滤器对象的equals方法进行序列化,返回true则会被排除,返回false会被序列化
JsonJsonInclude.Include.USE_DEFAULTS //使用默认值

这样就正常了

image-20221230220540673

5、控制台仍然报错

image-20221230220907077

这是因为当我们选择所属分类时,默认 catelogId 是一个数组,而我们定义的 catelogId 是一个字符串

image-20221230223027347

(1)在 dataform 中增加一个 catelogIds 数组,并修改级联选择器中绑定的值

image-20221230223050311

image-20221230223134651

(2)提交表单时,就不能在从表单中获取 catelogId 了,因为我们已经修改为数组了。数组中最后的一个元素就是我们所需要的三级分类的ID

image-20221230223236009

(3)还需要修改 校验规则的属性名,否则会一直校验不通过

image-20221230223348219

分组修改&级联选择器显示

点击 分组属性 修改时。所属分类的路径并没有显示出来。

image-20221231160422593

展示分类id是一个数组,而点击修改时,后端返回来的 三级分类ID并不是一个数组类型的。

image-20221231160521996

因此我们希望在后端 返回分组信息时,也将三级分类的ID路径返回,格式是:[父id,儿子id,孙子id]

我将 catelogIds 都改成了 catelogPath

image-20221231161627233

1、在 AttrGroupEntity 实体类中增加 分类路径

	/*** 所属分类id的路径*/@TableField(exist = false)Long[] catelogPath;

2、AttrGroupController

    /*** 信息*/@RequestMapping("/info/{attrGroupId}")// @RequiresPermissions("com.atguigu.gulimall.product:attrgroup:info")public R info(@PathVariable("attrGroupId") Long attrGroupId){AttrGroupEntity attrGroup = attrGroupService.getById(attrGroupId);// 获取三级分类IDLong catelogId = attrGroup.getCatelogId();// 在返回分组信息时,希望找出分类id的路径Long[] catelogPath =  categoryService.findCatelogPath(catelogId);attrGroup.setCatelogPath(catelogPath);return R.ok().put("attrGroup", attrGroup);}

3、CategoryService

Long[] findCatelogPath(Long catelogId);

4、CategoryServiceImpl

 /*** @description 找到所属分类id的路径 [父级分类id,儿子分类id,孙子分类id] -> [2,34,225]* @date 2022/12/31 16:22* @param catelogId* @return java.lang.Long[]*/@Overridepublic Long[] findCatelogPath(Long catelogId) {ArrayList catelogPath = new ArrayList();// 找到所属分类id的路径,是一个递归操作findParentPath(catelogId,catelogPath);// 最后封装好的集合是: {225,34,2}// 将它反转一下: {2,34,225}Collections.reverse(catelogPath);// Object数组转换为Long数组会出现CastClassException,新创建一个数组。return catelogPath.toArray(new Long[catelogPath.size()]);}/** 找到所属分类id的路径* */private void findParentPath(Long catelogId,List catelogPath) {catelogPath.add(catelogId);CategoryEntity categoryEntity = this.getById(catelogId);if (categoryEntity.getParentCid() != 0) {// 如果所属分类的父id不等于0,继续递归查找findParentPath(categoryEntity.getParentCid(),catelogPath);}}

5、测试结果

image-20221231171915636

功能完善

关闭对话框时,清空选择框中的内容

image-20221231173244522

dialogClose方法

    // 关闭对话框时,将所属分类清空dialogClose() {this.dataForm.catelogPath = []},

二、品牌分类关联与级联更新

增加MyBatis-Plus分页插件

如果增加到 common 模块里,记得在主启动类里增加 @SpringBootApplication(scanBasePackages = "com.atguigu")

@Configuration
public class MyConfig {/*** 新的分页插件,一缓和二缓遵循mybatis的规则,需要设置 MybatisConfiguration#useDeprecatedExecutor = false 避免缓存出现问题(该属性会在旧插件移除后一同移除)*/@Beanpublic MybatisPlusInterceptor mybatisPlusInterceptor() {MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.H2));return interceptor;}
}

品牌管理模糊查询

BrandServiceImpl: 增加对关键字 key 的判断即可。

    @Overridepublic PageUtils queryPage(Map params) {QueryWrapper wrapper = new QueryWrapper<>();//模糊查询String key = (String) params.get("key");if (!StringUtils.isEmpty(key)) {wrapper.eq("brand_id",key).or().like("name",key);}IPage page = this.page(new Query().getPage(params),wrapper);return new PageUtils(page);}

前端模块复制

将资料提供好的 common、product 模块,替换掉自己项目中的模块。

image-20221231181852394

将项目的 CategoryEntity 里的子级分类字段 ChildrenLevel 修改成 children

其他名字尽量和老师尽量一样吧,否则太悲催了…

品牌管理&关联分类

一个品牌对应多个分类,而一个分类又可以对应多个品牌。

pms_category_brand_relation 表中保存了品牌与分类的关联关系。

image-20230102165350685

1、接口访问路径

image-20230102165513282

2、前端请求参数

image-20230102165528919

3、后端响应数据

{"msg": "success","code": 0,"data": [{"catelogId": 0,"catelogName": "string",}]
}

4、CategoryBrandRelationController

    /*** 列表* 查询品牌关联分类*/@GetMapping("catelog/list")//@RequiresPermissions("com.atguigu.gulimall.product:categorybrandrelation:list")public R catelogList(@RequestParam Long brandId){// PageUtils page = categoryBrandRelationService.queryPage(params);List data = categoryBrandRelationService.list(new QueryWrapper().eq("brand_id",brandId));return R.ok().put("data", data);}

新增品牌关联分类

1、接口路径

image-20230102170152798

2、请求参数

{"brandId":1,"catelogId":2}

3、响应数据

{"msg": "success","code": 0
}

4、 CategoryBrandRelationController

    /*** 保存* 新增品牌关联分类*/@PostMapping("/save")// @RequiresPermissions("com.atguigu.gulimall.product:categorybrandrelation:save")public R save(@RequestBody CategoryBrandRelationEntity categoryBrandRelation){// categoryBrandRelationService.save(categoryBrandRelation);categoryBrandRelationService.saveDetail(categoryBrandRelation);return R.ok();}

5、CategoryBrandRelationServiceImpl

  • 根据 brandId、catelogId 查询出品牌名,分类名。在设置关联
    @Autowiredprivate BrandDao brandDao;@Autowiredprivate CategoryDao categoryDao;// 新增品牌关联分类@Overridepublic void saveDetail(CategoryBrandRelationEntity categoryBrandRelation) {Long brandId = categoryBrandRelation.getBrandId();Long catelogId = categoryBrandRelation.getCatelogId();// 查询品牌名BrandEntity brandEntity = brandDao.selectById(brandId);// 查询分类名CategoryEntity categoryEntity = categoryDao.selectById(catelogId);// 设置关联categoryBrandRelation.setBrandName(brandEntity.getName());categoryBrandRelation.setCatelogName(categoryEntity.getName());this.save(categoryBrandRelation);}

级联更新

由于品牌与分类关联的表,单独维护了一张表,因此在修改 品牌名称 以及 分类名称 的时候,还需要更新关联表中的 品牌名与分类名。

image-20230102172459687

1、修改品牌信息时,同时更新关联表中的品牌名称

BrandController/*** 修改*/@RequestMapping("/update")// @RequiresPermissions("com.atguigu.gulimall.product:brand:update")public R update(@RequestBody @Validated(value = UpdateGroup.class) BrandEntity brand) {// brandService.updateById(brand);// 级联更新brandService.updateCascade(brand);return R.ok();}BrandServiceImpl@Autowiredprivate CategoryBrandRelationService brandRelationService;/** 修改品牌表的同时,级联更新其他与品牌相关连的表* */@Transactional // 事务注解@Overridepublic void updateCascade(BrandEntity brand) {// 更新自己this.updateById(brand);if (!StringUtils.isEmpty(brand.getName())) {// 修改 品牌分类 关联表CategoryBrandRelationEntity categoryBrandRelationEntity = new CategoryBrandRelationEntity();categoryBrandRelationEntity.setBrandName(brand.getName());// 更新关联表的 品牌名称brandRelationService.update(categoryBrandRelationEntity,new QueryWrapper().eq("brand_id", brand.getBrandId()));// TODO:更新其他表}}

2、修改分类信息时,同时更新关联表中的分类名称

CategoryController/*** 修改*/@RequestMapping("/update")// @RequiresPermissions("com.atguigu.gulimall.product:category:update")public R update(@RequestBody CategoryEntity category){// categoryService.updateById(category);// 级联更新categoryService.updateCascade(category);return R.ok();}CategoryServiceImpl/** 更新分类表的同时更新其他关联表* */@Transactional // 事务注解@Overridepublic void updateCascade(CategoryEntity category) {this.updateById(category);// 更新 关联表的 分类名if (!StringUtils.isEmpty(category.getName())) {CategoryBrandRelationEntity categoryBrandRelationEntity = new CategoryBrandRelationEntity();categoryBrandRelationEntity.setCatelogName(category.getName());categoryBrandRelationService.update(categoryBrandRelationEntity,new QueryWrapper().eq("catelog_id",category.getCatId()));}// TODO 更新其他表的数据}

三、规格参数

Object对象划分

  1. PO(persistant object) 持久对象

PO 就是对应数据库中某个表中的一条记录,多个记录可以用 PO 的集合。 PO 中应该不包 含任何对数据库的操作。

  1. DO(Domain Object)领域对象

就是从现实世界中抽象出来的有形或无形的业务实体。

  1. TO(Transfer Object) ,数据传输对象

不同的应用程序之间传输的对象

  1. DTO(Data Transfer Object)数据传输对象

这个概念来源于 J2EE 的设计模式,原来的目的是为了 EJB 的分布式应用提供粗粒度的 数据实体,以减少分布式调用的次数,从而提高分布式调用的性能和降低网络负载,但在这 里,泛指用于展示层与服务层之间的数据传输对象。

  1. VO(value object) 值对象

通常用于业务层之间的数据传递,和 PO 一样也是仅仅包含数据而已。但应是抽象出 的业务对象 , 可以和表对应 , 也可以不 , 这根据业务的需要 。用 new 关键字创建,由 GC 回收的。 View object:视图对象; 接受页面传递来的数据,封装对象 将业务处理完成的对象,封装成页面要用的数据

  1. BO(business object) 业务对象

从业务模型的角度看 , 见 UML 元件领域模型中的领域对象。封装业务逻辑的 java 对 象 , 通过调用 DAO 方法 , 结合 PO,VO 进行业务操作。business object: 业务对象 主要作 用是把业务逻辑封装为一个对象。这个对象可以包括一个或多个其它的对象。 比如一个简 历,有教育经历、工作经历、社会关系等等。 我们可以把教育经历对应一个 PO ,工作经 历对应一个 PO ,社会关系对应一个 PO 。 建立一个对应简历的 BO 对象处理简历,每 个 BO 包含这些 PO 。 这样处理业务逻辑时,我们就可以针对 BO 去处理。

  1. POJO(plain ordinary java object) 简单无规则 java 对象

传统意义的 java 对象。就是说在一些 Object/Relation Mapping 工具中,能够做到维护 数据库表记录的 persisent object 完全是一个符合 Java Bean 规范的纯 Java 对象,没有增 加别的属性和方法。我的理解就是最基本的 java Bean ,只有属性字段及 setter 和 getter 方法!。 POJO 是 DO/DTO/BO/VO 的统称。

  1. DAO(data access object) 数据访问对象

是一个 sun 的一个标准 j2ee 设计模式, 这个模式中有个接口就是 DAO ,它负持久 层的操作。为业务层提供接口。此对象用于访问数据库。通常和 PO 结合使用, DAO 中包 含了各种数据库的操作方法。通过它的方法 , 结合 PO 对数据库进行相关的操作。夹在业 务逻辑与数据库资源中间。配合 VO, 提供数据库的 CRUD 操作

规格参数新增

新增规格参数时,需要将增加的属性与属性分组相关联。

image-20230102183730569

就好比京东来说,一个属性分组下有好多个属性。就需要在新增属性时,它是属于哪个分组的。就得给他关联上。

image-20230102183535474

属性与分组的关系保存在 pms_attr_attrgroup_relation

image-20230102183958160

而在新增 规格参数 时,前端发送的请求参数多了一个 分组ID。在 AttrEntity 规格参数实体类中并没有 分组ID 属性,以前我们是在实体类中增加字段,而更多的方式是新建一个 vo对象,用于接受页面传输过来的数据。

image-20230102183917514

1、新建一个 VO对象

@Data
public class AttrVo {/*** 属性id*/private Long attrId;/*** 属性名*/private String attrName;/*** 是否需要检索[0-不需要,1-需要]*/private Integer searchType;/*** 属性图标*/private String icon;/*** 可选值列表[用逗号分隔]*/private String valueSelect;/*** 属性类型[0-销售属性,1-基本属性,2-既是销售属性又是基本属性]*/private Integer attrType;/*** 启用状态[0 - 禁用,1 - 启用]*/private Long enable;/*** 所属分类*/private Long catelogId;/*** 快速展示【是否展示在介绍上;0-否 1-是】,在sku中仍然可以调整*/private Integer showDesc;/*** 分组ID* */private Long attrGroupId;
}

2、AttrController

  /*** 保存* 新增规格参数并关联分组*/@RequestMapping("/save")// @RequiresPermissions("com.atguigu.gulimall.product:attr:save")public R save(@RequestBody AttrVo attrVo){
// attrService.save(attr);attrService.saveAttr(attrVo);return R.ok();}

3、 AttrServiceImpl

 /** 新增规格参数并关联分组 attrAttrgroupRelationService* */@Overridepublic void saveAttr(AttrVo attrVo) {// 1、保存属性基本信息AttrEntity attrEntity = new AttrEntity();// 拷贝对象BeanUtils.copyProperties(attrVo,attrEntity);this.save(attrEntity);// 2、保存关联关系AttrAttrgroupRelationEntity attrAttrgroupRelationEntity = new AttrAttrgroupRelationEntity();attrAttrgroupRelationEntity.setAttrId(attrEntity.getAttrId());attrAttrgroupRelationEntity.setAttrGroupId(attrVo.getAttrGroupId());attrAttrgroupRelationDao.insert(attrAttrgroupRelationEntity);}

规格参数列表

1、请求路径

image-20230102205838253

2、请求参数

{page: 1,//当前页码limit: 10,//每页记录数sidx: 'id',//排序字段order: 'asc/desc',//排序方式key: '华为'//检索关键字
}

3、响应数据

除了要响应 Attr 中的数据,还有 catelogName(分类名)、groupName(分组名) ,这俩个字段是 Attr 实体类中没有的,因此我们需要额外创建一个 Vo 对象。

{"msg": "success","code": 0,"page": {"totalCount": 0,"pageSize": 10,"totalPage": 0,"currPage": 1,"list": [{"attrId": 0, //属性id"attrName": "string", //属性名"attrType": 0, //属性类型,0-销售属性,1-基本属性"catelogName": "手机/数码/手机", //所属分类名字"groupName": "主体", //所属分组名字"enable": 0, //是否启用"icon": "string", //图标"searchType": 0,//是否需要检索[0-不需要,1-需要]"showDesc": 0,//是否展示在介绍上;0-否 1-是"valueSelect": "string",//可选值列表[用逗号分隔]"valueType": 0//值类型[0-为单个值,1-可以选择多个值]}]}
}

4、创建 Vo 对象

@Data
public class AttrRespVo extends  AttrVo{// catelogName/groupNameprivate String catelogName;private String groupName;
}

思路分析

  • 查询属性信息,并且都是分页查询
    • 根据分类ID查询
    • 根据 key 查询
    • 查询所有
  • 查询属性分组名称
    • 根据属性信息中的 attrId(属性ID)pms_attr_attrgroup_relation 表中查询出 attr_group_id(属性分组ID)
    • 根据 attr_group_id(属性分组ID) pms_attr_group 查询出 attr_group_name(分组名)
  • 查询所属分类名称
    • 根据属性信息中的 catelog_id(所属分类ID) 在 pms_category表中查询出 name(分类名称)

5、AttrController

/***  属性规格参数查询——关联分组名称、分类名称* @param params 封装前端请求参数* @param catelogId 分类ID* @return*///  /product/attr/base/list/{catelogId}@GetMapping("/base/list/{catelogId}")public R baseList(@RequestParam Map params,@PathVariable("catelogId")Long catelogId) {PageUtils page = attrService.queryBaseListPage(params,catelogId);return R.ok().put("page", page);}

6、AttrServiceImpl

@AutowiredAttrAttrgroupRelationDao attrAttrgroupRelationDao;@Autowiredprivate CategoryDao categoryDao;@Autowiredprivate AttrGroupDao attrGroupDao; /*- 查询属性信息,并且都是分页查询- 根据 key 查询- 根据分类ID查询- 查询所有- 查询属性分组名称- 根据属性信息中的 `attrId(属性ID)` 在 `pms_attr_attrgroup_relation` 表中查询出 `attr_group_id(属性分组ID)`- 根据 `attr_group_id(属性分组ID)` 在` pms_attr_group` 查询出 `attr_group_name(分组名)`- 查询所属分类名称- 根据属性信息中的 catelog_id(所属分类ID) 在 `pms_category`表中查询出 `name(分类名称)`* */@Overridepublic PageUtils queryBaseListPage(Map params, Long catelogId) {// 1、查询属性信息,并且都是分页查询QueryWrapper wrapper = new QueryWrapper<>();// 1.1 根据分类ID查询if (catelogId != 0) {// 根据所属分类查询wrapper.eq("catelog_id", catelogId);}// 1.2 根据 key 查询// attr_id/attr_nameString key = (String) params.get("key");if (!StringUtils.isEmpty(key)) {wrapper.and(queryWrapper -> {queryWrapper.eq("attr_id", key).or().like("attr_name", key);});}// 1.3 查询所有IPage page = this.page(new Query().getPage(params),wrapper);// 属性信息集合List records = page.getRecords();List list = records.stream().map(attrEntity -> {// 响应给浏览器的数据AttrRespVo attrRespVo = new AttrRespVo();// 先将属性的基本信息拷贝给vo对象BeanUtils.copyProperties(attrEntity, attrRespVo);// 2、查询属性分组名称// 2.1 在 `pms_attr_attrgroup_relation` 表中查询出 `attr_group_id(属性分组ID)`AttrAttrgroupRelationEntity attrgroupRelationEntity =attrAttrgroupRelationDao.selectOne(new QueryWrapper().eq("attr_id", attrEntity.getAttrId()));if (attrgroupRelationEntity != null) {// 2.2 根据 `attr_group_id(属性分组ID)` 在` pms_attr_group` 查询出 `attr_group_name(分组名)`AttrGroupEntity attrGroupEntity =attrGroupDao.selectById(attrgroupRelationEntity.getAttrGroupId());attrRespVo.setGroupName(attrGroupEntity.getAttrGroupName());}// 3、查询所属分类名称// 根据属性信息中的 catelog_id(所属分类ID) 在 `pms_category`表中查询出 `name(分类名称)`CategoryEntity categoryEntity = categoryDao.selectById(attrEntity.getCatelogId());if (categoryEntity != null) {attrRespVo.setCatelogName(categoryEntity.getName());}return attrRespVo;}).collect(Collectors.toList());PageUtils pageUtils = new PageUtils(page);pageUtils.setList(list);return pageUtils;}

规格参数修改

规格参数回显

点击 修改 回显数据

image-20230102220551645

1、 接口访问路径

image-20230102220639498

2、响应数据

需要在 AttrRespVo 中 额外再增加 catelogPath 字段

{"msg": "success","code": 0,"attr": {"attrId": 4,"attrName": "aad","searchType": 1,"valueType": 1,"icon": "qq","valueSelect": "v;q;w","attrType": 1,"enable": 1,"showDesc": 1,"attrGroupId": 1, //分组id"catelogId": 225, //分类id"catelogPath": [2, 34, 225] //分类完整路径}
}

image-20230102220943418

思路分析

  • 根据 attrId 查询 属性基本信息
  • 根据 catelogId 查询分类完整路径
  • 根据 attrId(属性ID)pms_attr_attrgroup_relation 表中查询出 attr_group_id(属性分组ID)
  • 根据 attr_group_id(属性分组ID) pms_attr_group 查询出 attr_group_name(分组名)

3、AttrController

    /*** 信息*/@RequestMapping("/info/{attrId}")// @RequiresPermissions("com.atguigu.gulimall.product:attr:info")public R info(@PathVariable("attrId") Long attrId){// AttrEntity attr = attrService.getById(attrId);AttrRespVo vo = attrService.getAttrInfo(attrId);return R.ok().put("attr", vo);}

4、AttrServiceImpl

 /** 回显修改的规格参数数据- 根据 attrId 查询 属性基本信息- 根据 catelogId 查询分类完整路径- 根据 `attrId(属性ID)` 在 `pms_attr_attrgroup_relation` 表中查询出 `attr_group_id(属性分组ID)`- 根据 `attr_group_id(属性分组ID)` 在` pms_attr_group` 查询出 `attr_group_name(分组名)`* */@Overridepublic AttrRespVo getAttrInfo(Long attrId) {AttrRespVo attrRespVo = new AttrRespVo();//  根据 attrId 查询 属性基本信息AttrEntity attrEntity = this.getById(attrId);BeanUtils.copyProperties(attrEntity, attrRespVo);// 根据 catelogId 查询分类完整路径Long[] catelogPath = categoryService.findCatelogPath(attrEntity.getCatelogId());attrRespVo.setCatelogPath(catelogPath);// 根据 `attrId(属性ID)` 在 `pms_attr_attrgroup_relation` 表中查询出 `attr_group_id(属性分组ID)`AttrAttrgroupRelationEntity attrAttrgroupRelationEntity =attrAttrgroupRelationDao.selectOne(new QueryWrapper().eq("attr_id", attrEntity.getAttrId()));if (attrAttrgroupRelationEntity != null) {attrRespVo.setAttrGroupId(attrAttrgroupRelationEntity.getAttrGroupId());// 根据 `attr_group_id(属性分组ID)` 在` pms_attr_group` 查询出 `attr_group_name(分组名)`AttrGroupEntity attrGroupEntity = attrGroupDao.selectById(attrAttrgroupRelationEntity.getAttrGroupId());if (attrGroupEntity != null) {attrRespVo.setGroupName(attrGroupEntity.getAttrGroupName());}}return attrRespVo;}

规格参数修改

1、访问路径

image-20230103151205916

2、请求参数

{"attrId": 0, //属性id"attrGroupId": 0, //属性分组id"attrName": "string",//属性名"attrType": 0, //属性类型"catelogId": 0, //分类id"enable": 0, //是否可用 "icon": "string", //图标"searchType": 0, //是否检索"showDesc": 0, //快速展示"valueSelect": "string", //可选值列表"valueType": 0 //可选值模式
}

思路分析:

  • 不仅要修改规格参数,还要修改 关联关系 中的分组ID,而修改所属分组有俩种情况:
    • 当我们没有指定所属分组时,点击 修改时指定分组,那么它其实是一个新增所属分组的操作
    • 如果指定了所属分组,修改时是一个修改所属分组操作

image-20230103151317724

3、AttrController

    /*** 修改*/@RequestMapping("/update")// @RequiresPermissions("com.atguigu.gulimall.product:attr:update")public R update(@RequestBody AttrVo attrVo){attrService.updateAttr(attrVo);return R.ok();}

4、AttrServiceImpl

 /** 修改规格参数* 1、修改规格参数* 2、修改关联分组ID* 3、判断是新增所属分组还是修改所属分组* */@Overridepublic void updateAttr(AttrVo attrVo) {// 1、修改规格参数AttrEntity attrEntity = new AttrEntity();BeanUtils.copyProperties(attrVo,attrEntity);this.updateById(attrEntity);// 2、修改关联分组IDAttrAttrgroupRelationEntity attrAttrgroupRelationEntity = new AttrAttrgroupRelationEntity();attrAttrgroupRelationEntity.setAttrId(attrVo.getAttrId());attrAttrgroupRelationEntity.setAttrGroupId(attrVo.getAttrGroupId());UpdateWrapper updateWrapper = new UpdateWrapper<>();updateWrapper.eq("attr_id",attrEntity.getAttrId());// 3、判断是新增所属分组还是修改所属分组// 从关系表中查出属性对应的分组,如果能查出来就说明是修改,查不出来就是新增if (attrAttrgroupRelationDao.selectCount(new QueryWrapper().eq("attr_id",attrVo.getAttrId())) == 0) {// 说明是新增所属分组attrAttrgroupRelationDao.insert(attrAttrgroupRelationEntity);}else {// 说明是修改所属分组attrAttrgroupRelationDao.update(attrAttrgroupRelationEntity,updateWrapper);}}

四、销售属性

1、接口路径

销售属性与规格参数唯一的区别就是,路径中带有 base 还是带有 sale。因此我们在获取列表时,只需要判断路径中带有的是哪个值即可,无需在额外创建一个方法。

销售属性:/product/attr/sale/list/{catelogId}

规格参数:/product/attr/base/list/{catelogId}

image-20230103155001350

2、请求参数

{page: 1,//当前页码limit: 10,//每页记录数sidx: 'id',//排序字段order: 'asc/desc',//排序方式key: '华为'//检索关键字
}

3、响应数据

销售属性 无需与 所属分组进行关联,因此无论是新增还是修改,如果是销售属性就不用在设置 所属分组了。

{"msg": "success","code": 0,"page": {"totalCount": 0,"pageSize": 10,"totalPage": 0,"currPage": 1,"list": [{"attrId": 0, //属性id"attrName": "string", //属性名"attrType": 0, //属性类型,0-销售属性,1-基本属性"catelogName": "手机/数码/手机", //所属分类名字"groupName": "主体", //所属分组名字"enable": 0, //是否启用"icon": "string", //图标"searchType": 0,//是否需要检索[0-不需要,1-需要]"showDesc": 0,//是否展示在介绍上;0-否 1-是"valueSelect": "string",//可选值列表[用逗号分隔]"valueType": 0//值类型[0-为单个值,1-可以选择多个值]}]}
}

思路分析

销售属性和基本属性都保存 pms_attr 表中,使用 attr_type 字段区分是哪种属性。

因此在查询时,需要增加对 attr_type 的判断

在新增、修改时,由于销售属性不用与所属分组进行相关联。因此在进行关联分组时,也需要判断是否是基本属性。只有在 新增、修改 基本属性时才会 关联分组

image-20230103160134529

4、在 common 模块中创建 常量类。区分是销售属性,还是基本属性

package com.atguigu.common.constant;/**** Author: YZG* Date: 2023/1/3 15:53* Description: */
public class ProductConstant {public enum ProductEnum{ATTR_TYPE_BASE(1,"基础属性"),ATTR_TYPE_SALE(0,"销售属性");private int code ;private String msg ;ProductEnum(int code, String msg) {this.code = code;this.msg = msg;}public int getCode() {return code;}public String getMsg() {return msg;}}
}

5、获取销售属性列表

(1) 在controller层,获取 属性类型。base/sale

image-20230103161219372

(2)在查询时,增加 属性判断 。

image-20230103161326406

(3)查询属性时,只有基本属性才能关联分组

image-20230103161521754

6、新增销售属性

AttrServiceImpl 中 saveAttr 方法

image-20230103161802152

7、修改销售属性

AttrServiceImpl 中 updateAttr 方法

image-20230103162117792

五、分组关联属性&删除关联

分组关联属性

在属性分组中,点击关联,会显示所有与分组关联的属性。

image-20230103162611568

1、访问接口路径

image-20230103162836304

2、响应数据

{"msg": "success","code": 0,"data": [{"attrId": 4,"attrName": "aad","searchType": 1,"valueType": 1,"icon": "qq","valueSelect": "v;q;w","attrType": 1,"enable": 1,"catelogId": 225,"showDesc": 1}]
}

3、AttrGroupController

       @Autowiredprivate AttrServiceImpl attrService;/*** 查询与分组关联的所有属性* */// /product/attrgroup/{attrgroupId}/attr/relation@GetMapping("/{attrgroupId}/attr/relation")public R listAttrRelation(@PathVariable("attrgroupId") Long attrgroupId){// 查询出与分组关联的所有属性List list = attrService.getAttrsWithGroup(attrgroupId);return R.ok().put("data", list);}

4、AttrGroupServiceImpl

   /*** 根据分组id查询出与分组关联的所有属性* 1、根据 attrgroupId 在关联表 pms_attr_attrgroup_relation 中查询出所对应的所有 attr_id*  一个分组可能对应多个属性* 2、根据 attr_id 在 pms_attr 表中查询出所有属性* */@Overridepublic List getAttrsWithGroup(Long attrgroupId) {QueryWrapper queryWrapper = new QueryWrapper().eq("attr_group_id", attrgroupId);// 查询出分组对应的所有属性List list = attrAttrgroupRelationDao.selectList(queryWrapper);// 根据查attr_id询出所有属性信息List allAttrEntity = list.stream().map(attrAttrgroupRelationEntity -> this.getById(attrAttrgroupRelationEntity.getAttrId())).collect(Collectors.toList());return allAttrEntity;}

删除关联

1、访问接口路径

image-20230103171336527

2、 请求参数

[{"attrId":1,"attrGroupId":2}]

3、响应数据

{"msg": "success","code": 0
}

4、创建一个 VO 对象接口参数

@Data
public class AttrRelaVo {//     [{"attrId":1,"attrGroupId":2}]private Long attrId;private Long attrGroupId;
}

5、AttrGroupController

   // /product/attrgroup/attr/relation/delete@PostMapping("/attr/relation/delete")public R deleteRela(@RequestBody AttrRelaVo[] relaVo) {// 删除与分组关联的属性attrService.deleteBatch(relaVo);return  R.ok();}

6、AttrServiceImpl

 /*** 删除与分组关联的属性* DELETE  FROM `pms_attr_attrgroup_relation` WHERE (attr_group_id=? AND attr_id=?) OR (attr_group_id=? AND attr_id=?)* */@Overridepublic void deleteBatch(AttrRelaVo[] relaVo) {// attrId,attrGroupIdList attrRelaVos = Arrays.asList(relaVo);// 将 relaVo 映射成一个个的 AttrAttrgroupRelationEntityList entities = attrRelaVos.stream().map(item -> {AttrAttrgroupRelationEntity relationEntity = new AttrAttrgroupRelationEntity();BeanUtils.copyProperties(item, relationEntity);return relationEntity;}).collect(Collectors.toList());// 删除关联关系attrAttrgroupRelationDao.deleteBatchRela(entities);}

7、AttrAttrgroupRelationDao

 void deleteBatchRela(@Param("entities") List entities);

8、AttrGroupRelationDao.xml

    DELETE  FROM `pms_attr_attrgroup_relation` WHERE( attr_id=#{item.attrId}  AND attr_group_id=#{item.attrGroupId})

查询分组未关联的属性

点击 新建关联 时,显示所有本类下所有未与分组进行关联的属性

image-20230103182309531

1、接口访问地址

image-20230103182349879

2、请求参数

{page: 1,//当前页码limit: 10,//每页记录数sidx: 'id',//排序字段order: 'asc/desc',//排序方式key: '华为'//检索关键字
}

3、响应数据

{"msg": "success","code": 0,"page": {"totalCount": 3,"pageSize": 10,"totalPage": 1,"currPage": 1,"list": [{"attrId": 1,"attrName": "aaa","searchType": 1,"valueType": 1,"icon": "aa","valueSelect": "aa;ddd;sss;aaa2","attrType": 1,"enable": 1,"catelogId": 225,"showDesc": 1}]}
}

思路分析:

查询出未关联的属性有俩个条件

  • 必须是本分类下的所有属性
  • 属性没有与其他分组相关联

逻辑步骤:

  1. pms_attr_group 表中,根据 分组ID 查询出所属的 分类ID

  2. pms_attr_group 表中, 根据分类ID查询出所有的分组

  3. 将分组的的 分组id 映射成一个集合

  4. pms_attr_attrgroup_relation 表中找出所有与 分组 相关联的属性

  5. 将所有相关联的 属性id 映射成一个集合

  6. pms_attr表中,查询本类下的所有属性,并排除相关联的属性id集合。

总体来说,就是找出所有相关联的属性,将这些属性排除即可。

4、AttrGroupController

/*** 查询所有没有与分组相关联的属性* /product/attrgroup/{attrgroupId}/noattr/relation* */
@GetMapping("/{attrgroupId}/noattr/relation")
public R listAttrNoRelation(@RequestParam Map params,@PathVariable("attrgroupId") Long attrgroupId){PageUtils page = attrService.getAttrsNoRelation(params,attrgroupId);return R.ok().put("page", page);
}

5、AttrServiceImpl


/*** 1. 在 `pms_attr_group` 表中,根据 分组ID 查询出所属的 分类ID* 2. 在 `pms_attr_group` 表中, 根据分类ID查询出所有的分组* 3. 将分组的的 分组id 映射成一个集合* 4. 在 `pms_attr_attrgroup_relation` 表中找出所有与 分组 相关联的属性* 5. 将所有相关联的 属性id 映射成一个集合* 6. 在 `pms_attr`表中,查询本类下的所有属性,并排除相关联的属性id集合。* */
@Override
public PageUtils getAttrsNoRelation(Map params, Long attrgroupId) {// 1. 在 `pms_attr_group` 表中,根据 分组ID 查询出所属的 分类IDAttrGroupEntity attrGroupEntity = attrGroupDao.selectById(attrgroupId);Long catelogId = attrGroupEntity.getCatelogId();// 2. 在 `pms_attr_group` 表中, 根据分类ID查询出所有的分组List otherGroups =attrGroupDao.selectList(new QueryWrapper().eq("catelog_id", catelogId));// 3. 将分组的的 分组id 映射成一个集合List otherGroupsIds = otherGroups.stream().map(AttrGroupEntity::getAttrGroupId).collect(Collectors.toList());// 4. 在 `pms_attr_attrgroup_relation` 表中找出所有与 分组 相关联的属性List attrAttrgroupRelationEntities= attrAttrgroupRelationDao.selectList(new QueryWrapper().in("attr_group_id",otherGroupsIds));// 5. 将所有分组相关联的 属性id 映射成一个集合List relationAttrIds = attrAttrgroupRelationEntities.stream().map(AttrAttrgroupRelationEntity::getAttrId).collect(Collectors.toList());// 6. 在 `pms_attr`表中,查询本类下的所有属性,并排除相关联的属性id集合。QueryWrapper queryWrapper = new QueryWrapper().eq("catelog_id", catelogId);if (relationAttrIds!= null && !relationAttrIds.isEmpty()) {// 有可能出现所有分组都没有关联属性的情况。queryWrapper.and((w -> {w.notIn("attr_id", relationAttrIds);}));}String key = (String) params.get("key");// 关键字搜索if (!StringUtils.isEmpty(key)) {queryWrapper.and(queryWrapper1 -> {queryWrapper.eq("attr_id",key).or().like("attr_name",key);});}// 分页查询IPage page = this.page(new Query().getPage(params),queryWrapper);// 将查询的数据封装pageUtilsPageUtils pageUtils = new PageUtils(page);return pageUtils;
}

新增分组属性关联

1、访问接口路径

image-20230103211534482

2、请求参数

[{"attrGroupId": 0, //分组id"attrId": 0, //属性id
}]

3、响应数据

{"msg": "success","code": 0
}

4、AttrGroupController

    /*** 保存分组与属性关联关系* /product/attrgroup/attr/relation*/@PostMapping("/attr/relation")public R saveAttrRelation(@RequestBody List attrVo){attrAttrgroupRelationService.saveAttrRelation(attrVo);return R.ok();}

5、AttrAttrgroupRelationServiceImpl

    /*** 保存分组与属性关联关系* 批量增加* */@Overridepublic void saveAttrRelation(List attrVo) {List attrAttrgroupRelationEntities = attrVo.stream().map(item -> {AttrAttrgroupRelationEntity attrAttrgroupRelationEntity = new AttrAttrgroupRelationEntity();BeanUtils.copyProperties(item, attrAttrgroupRelationEntity);return attrAttrgroupRelationEntity;}).collect(Collectors.toList());// 批量新增this.saveBatch(attrAttrgroupRelationEntities);}

新增属性小bug

当我们新增基本属性但是没有指定分组时,就会出现空指针异常

image-20230103214917663

image-20230103214943069

看关系表中,没有指定分组,分组ID为NULL,因此在查询所有基本属性时,并没有对 分组ID 的判断就进行了关联分组。

image-20230103215015965

AttrServiceImpl 中 queryBaseListPage: 在关联分组属性时,对 分组ID 进行不为空判断

image-20230103215357956

AttrServiceImpl 中 saveAttr方法: 增加 基本属性时,增加判断:只有分组ID不为空时,在进行分组关联

image-20230103215418581

商品维护

一、发布商品

环境准备与测试

1、将提供的 modules 模块替换掉自己项目中的模块

这里老师提供的前端代码中,有的分类Id是 catelogId,有的是 catalogId,我将前端所有的 catalogId 和数据库中的 catalogId 都换成了 catelogId

image-20230103222009940

2、调试 gulimall-member 模块。

(1) 配置文件

# 数据库配置
spring:datasource:url: jdbc:mysql://192.168.56.111:3306/gulimall_umsusername: rootpassword: rootdriver-class-name: com.mysql.jdbc.Drivercloud:nacos:config:server-addr: localhost:8848discovery:server-addr: localhost:8848application:name: gulimall-member
# MyBatis-Plus配置
mybatis-plus:mapper-locations: classpath:/mapper/**/*.xml # mapper映射文件的位置global-config:db-config:id-type: auto # 主键自增策略
server:port: 8000

(2) 主启动类

@SpringBootApplication
@EnableDiscoveryClient
public class GulimallMemberApplication {public static void main(String[] args) {SpringApplication.run(GulimallMemberApplication.class);}
}

3、 配置网关

                - id: member_routeuri: lb://gulimall-member # 负载均衡predicates:- Path=/api/member/**filters:- RewritePath=/api/?(?.*), /$\{segment}  #路径重写

获取分类关联的所有品牌

1、访问接口路径

image-20230104154843159

2、请求参数

image-20230104154857123

3、响应数据

{"msg": "success","code": 0,"data": [{"brandId": 0,"brandName": "string",}]
}

4、创建一个 VO 对象,返回页面

@Data
public class BrandVo {private Long brandId;private String brandName;
}

5、CategoryBrandRelationController

  /***  /product/categorybrandrelation/brands/list*  获取分类关联的品牌* */@GetMapping("/brands/list")public R categoryBrandRelationList(Long catId) {// 业务处理List brandEntities = categoryBrandRelationService.getBrandsByCatId(catId);// 封装页面所需要的数据List brandVos =  brandEntities.stream().map(item -> {BrandVo brandVo = new BrandVo();brandVo.setBrandId(item.getBrandId());brandVo.setBrandName(item.getName());return brandVo;}).collect(Collectors.toList());return  R.ok().put("data",brandVos);}

6、CategoryBrandRelationServiceImpl

/*** 获取分类下所有关联的品牌
* */
@Override
public List getBrandsByCatId(Long catId) {// 1、在 pms_category_brand_relation 表中查询出分类下所有的品牌idList categoryBrandRelationEntities = this.list(new QueryWrapper().eq("catelog_id", catId));List brandIds = categoryBrandRelationEntities.stream().map(CategoryBrandRelationEntity::getBrandId).collect(Collectors.toList());// 2、在品牌表中查询出所有品牌if (brandIds != null && !brandIds.isEmpty()){return brandService.listByIds(brandIds);}return  null ;
}

获取分类下的所有分组&关联属性

1、访问接口路径

image-20230104162329051

2、响应数据

{"msg": "success","code": 0,"data": [{"attrGroupId": 1,"attrGroupName": "主体","sort": 0,"descript": "主体","icon": "dd","catelogId": 225,"attrs": [{"attrId": 7,"attrName": "入网型号","searchType": 1,"valueType": 0,"icon": "xxx","valueSelect": "aaa;bb","attrType": 1,"enable": 1,"catelogId": 225,"showDesc": 1,"attrGroupId": null}, {"attrId": 8,"attrName": "上市年份","searchType": 0,"valueType": 0,"icon": "xxx","valueSelect": "2018;2019","attrType": 1,"enable": 1,"catelogId": 225,"showDesc": 0,"attrGroupId": null}]},{"attrGroupId": 2,"attrGroupName": "基本信息","sort": 0,"descript": "基本信息","icon": "xx","catelogId": 225,"attrs": [{"attrId": 11,"attrName": "机身颜色","searchType": 0,"valueType": 0,"icon": "xxx","valueSelect": "黑色;白色","attrType": 1,"enable": 1,"catelogId": 225,"showDesc": 1,"attrGroupId": null}]}]
}

3、创建 VO 对象,返回前端

@Data
public class AttrGroupWithAttrsVo {/*** 分组id*/private Long attrGroupId;/*** 组名*/private String attrGroupName;/*** 排序*/private Integer sort;/*** 描述*/private String descript;/*** 组图标*/private String icon;/*** 所属分类id*/private Long catelogId;/*** 分组下的所有属性* */private List attrs;
}

4、AttrGroupController

    /*** 获取分类下的所有分组,以及每个分组的所有属性* /product/attrgroup/{catelogId}/withattr* */@GetMapping("{catelogId}/withattr")public R attrgroupWithAttrs(@PathVariable("catelogId") Long catelogId){List attrGroupWithAttrsVos =  attrGroupService.getAttrGroupWithAttrsWithCatelogId(catelogId);return R.ok().put("data",attrGroupWithAttrsVos);}

5、AttrGroupServiceImpl

   /*** 获取分类下的所有分组* 获取每个分组下的所有属性* */@Overridepublic List getAttrGroupWithAttrsWithCatelogId(Long catelogId) {// 1、获取分类下的所有分组List attrGroupEntities = this.list(new QueryWrapper().eq("catelog_id", catelogId));if (attrGroupEntities != null && !attrGroupEntities.isEmpty()) {List attrGroupWithAttrsVoList = attrGroupEntities.stream().map(item -> {AttrGroupWithAttrsVo attrGroupWithAttrsVo = new AttrGroupWithAttrsVo();BeanUtils.copyProperties(item, attrGroupWithAttrsVo);// 2、获取每个分组下的所有属性List attrs = attrService.getAttrsRelation(attrGroupWithAttrsVo.getAttrGroupId());attrGroupWithAttrsVo.setAttrs(attrs);return attrGroupWithAttrsVo;}).collect(Collectors.toList());return attrGroupWithAttrsVoList;}return null;}

BUG: 规格参数无法单选多选

做到这里,突然发现规格参数的值类型无法选择单选还是多选。

image-20230104172813411

这是因为在 pms_attr表中少了一个 value_type字段。

image-20230104172846662

在 AttrEntity和AttrVo 中也增加上字段。

	/*** 1:表示可选多个值* 0:表示可选单个值* */private Integer valueType;

新增商品

在线JSON格式转换,以及在线生成实体类:

在线JSON校验格式化工具(Be JSON)

1、在线生成JavaBean实体类,将代码下载并拷贝到 vo 包下

image-20230104215757464

生成的 Vo

AttrVo
@Data  
public class Attr {private Long attrId;private String attrName;private String attrValue;
}BaseAttrs
@Data
public class BaseAttrs {private Long attrId;private String attrValues;private int showDesc;}
Bounds
@Data
public class Bounds {private BigDecimal buyBounds;private BigDecimal growBounds;}
Images
@Data
public class Images {private String imgUrl;private int defaultImg;}MemberPrice
@Data
public class MemberPrice {private Long id;private String name;private BigDecimal price;}Skus
@Data
public class Skus {private List attr;private String skuName;private BigDecimal price;private String skuTitle;private String skuSubtitle;private List images;private List descar;private Integer fullCount;private BigDecimal discount;private int countStatus;private BigDecimal fullPrice;private BigDecimal reducePrice;private int priceStatus;private List memberPrice;}
SpuSaveVo
@Data
public class SpuSaveVo {/*** 商品名称* */private String spuName;/*** 商品描述* */private String spuDescription;/*** 分类ID* */private Long catelogId;/*** 品牌ID* */private Long brandId;/*** 手机重量* */private BigDecimal weight;/*** 上架状态[0 - 下架,1 - 上架]* */private int publishStatus;/*** 商品介绍* */private List decript;/*** 商品图集* */private List images;/*** 商品积分* */private Bounds bounds;/*** 基本属性* */private List baseAttrs;/*** 销售属性* */private List skus;}

新增商品业务流程分析

1、保存商品的基本信息: gulimall_pms 数据库中的 pms_spu_info 表

image-20230104222042006

2、保存商品的介绍信息: gulimall_pms 数据库中的 pms_spu_info_desc 表

image-20230104222227582

3、保存商品的图片集: gulimall_pms 数据库中的 pms_spu_images 表

image-20230104222318465

4、保存商品的基本属性: gulimall_pms 数据库中的 pms_product_attr_value 表

image-20230104222755700

5、保存商品的积分: gulimall_sms 数据库中的 sms_spu_bounds 表

image-20230104222723765

6、保存当前 spu 对应的所有 sku 信息:

(1)sku 的基本信息: gulimall_pms 数据库中的 pms_sku_info 表

image-20230104223142747

(2) sku 的图片信息: gulimall_pms 数据库中的 pms_sku_images 表

image-20230104223305169

(3) sku 的销售属性信息: gulimall_pms 数据库中的 pms_sku_sale_attr_value表image-20230104223547522

(4) 保存商品的优惠、满减等信息。gulimall_sms 数据库中的 sms_sku_ladder、sms_sku_full_reduction、sms_member_price 表

image-20230104224321173


业务代码

一、 product 服务需要调用 coupon 服务,之间传输的数据称为 to 对象。

需要在 common 模块额外创建一个专门保存 to 对象的包。

保存商品积分的TO:

@Data
public class SpuBoundsTo {private Long spuId;private BigDecimal buyBounds;private BigDecimal growBounds;
}

保存商品优惠信息的TO:

@Data
public class SkuReductionTo {private Long skuId;private Integer fullCount;private BigDecimal discount;private int countStatus;private BigDecimal fullPrice;private BigDecimal reducePrice;private int priceStatus;// 依赖于 MemberPrice 类,也拷贝到 to包下private List memberPrice;
}

二、由于商品保存功能需要跨数据库完成,需要使用 OpenFeign 远程调用 gulimall-coupon 模块:

  1. 确保调用者与被调用者都注册到了Nacos服务中心。(省略)
  2. gulimall-product 中的主启动类增加 @EnableFeignClients 注解 (省略)
  3. gulimall-product 创建 接口,声明 gulimall-coupon 中的方法
@FeignClient("gulimall-coupon")
public interface CouponFeignService {/*** OpenFeign远程调用流程:* 1、 调用 CouponFeignService.saveSpuBounds(spuBoundsTo) 方法*      (1)  @RequestBody 将 spuBoundsTo 这个对象转换 json*      (2) 在 Nacos服务中心找到gulimall-coupon服务,并向 coupon/spubounds/save 发送请求,*          并把 json 保存在请求体中*      (3) 对方接受到请求,(@RequestBody SpuBoundsEntity spuBounds)*          @RequestBody 会将 json 数据转换为 SpuBoundsEntity* 只要 json 中的字段名与 SpuBoundsEntity中的字段名保持一致,可以自动封装的。* *//**  保存商品积分信息* */@PostMapping("coupon/spubounds/save")R saveSpuBounds(@RequestBody SpuBoundsTo spuBoundsTo);/** 保存商品优惠信息* */@PostMapping("coupon/skufullreduction/saveinfo")R saveSkuReduction(@RequestBody SkuReductionTo skuReductionTo);
}

三、具体实现的代码

1、SpuInfoServiceImpl

      @Autowiredprivate SpuInfoDescServiceImpl spuInfoDescService;@Autowiredprivate SpuImagesService spuImagesService;@Autowiredprivate AttrService attrService;@Autowiredprivate ProductAttrValueService productAttrValueService;@Autowiredprivate SkuInfoService skuInfoService;@Autowiredprivate SkuImagesService skuImagesService;@Autowiredprivate SkuSaleAttrValueService skuSaleAttrValueService;@Autowiredprivate CouponFeignService couponFeignService;/*** 新增商品* TODO: 需要完善功能* */@Override@Transactionalpublic void saveSpuInfo(SpuSaveVo spuSaveVo) {// 1、保存商品的基本信息: gulimall_pms 数据库中的 pms_spu_info 表SpuInfoEntity spuInfoEntity = new SpuInfoEntity();BeanUtils.copyProperties(spuSaveVo,spuInfoEntity);spuInfoEntity.setCreateTime(new Date());spuInfoEntity.setUpdateTime(new Date());this.saveSpuInfoBase(spuInfoEntity);// 2、保存商品的介绍信息: gulimall_pms 数据库中的 pms_spu_info_desc 表List decript = spuSaveVo.getDecript();SpuInfoDescEntity spuInfoDescEntity = new SpuInfoDescEntity();spuInfoDescEntity.setSpuId(spuInfoEntity.getId());// join方法: 拼接集合中的每一个属性,最终返回一个字符串spuInfoDescEntity.setDecript(String.join(",",decript));spuInfoDescService.saveSpuInfoDesc(spuInfoDescEntity);// 3、保存商品的图片集: gulimall_pms 数据库中的 pms_spu_images 表List images = spuSaveVo.getImages();spuImagesService.saveSpuInfoImages(spuInfoEntity.getId(),images);// 4、保存商品的基本属性: gulimall_pms 数据库中的 pms_product_attr_value 表List baseAttrs = spuSaveVo.getBaseAttrs();List productAttrValueEntities = baseAttrs.stream().map(attr -> {ProductAttrValueEntity valueEntity = new ProductAttrValueEntity();valueEntity.setSpuId(spuInfoEntity.getId());valueEntity.setAttrId(attr.getAttrId());// 查询属性名字AttrEntity attrEntity = attrService.getById(attr.getAttrId());valueEntity.setAttrName(attrEntity.getAttrName());valueEntity.setAttrValue(attr.getAttrValues());valueEntity.setQuickShow(attr.getShowDesc());return valueEntity;}).collect(Collectors.toList());productAttrValueService.saveProductAttr(productAttrValueEntities);// 5、保存商品的积分: gulimall_sms 数据库中的 sms_spu_bounds 表 【远程调用gulimall-coupon】Bounds bounds = spuSaveVo.getBounds();SpuBoundsTo spuBoundsTo = new SpuBoundsTo();BeanUtils.copyProperties(bounds,spuBoundsTo);spuBoundsTo.setSpuId(spuInfoEntity.getId());R r1 = couponFeignService.saveSpuBounds(spuBoundsTo);if (r1.getCode() != 0) {log.error("远程保存商品积分信息失败!!");}// 6、保存当前 spu 对应的所有 sku 信息:List skus = spuSaveVo.getSkus();if (skus != null && skus.size() > 0) {skus.forEach(sku -> {// 找到sku的默认图片List skuImages = sku.getImages();String defaultImage = "";for (Images skuImage : skuImages) {if (skuImage.getDefaultImg() == 1 ) {defaultImage = skuImage.getImgUrl();}}// (1)sku 的基本信息: gulimall_pms 数据库中的 pms_sku_info 表//    private String skuName;//     private BigDecimal price;//     private String skuTitle;//     private String skuSubtitle;SkuInfoEntity skuInfoEntity = new SkuInfoEntity();BeanUtils.copyProperties(sku,skuInfoEntity);skuInfoEntity.setSpuId(spuInfoEntity.getId());skuInfoEntity.setBrandId(spuInfoEntity.getBrandId());skuInfoEntity.setCatelogId(spuInfoEntity.getCatelogId());skuInfoEntity.setSaleCount(0L);skuInfoEntity.setSkuDefaultImg(defaultImage);skuInfoService.save(skuInfoEntity);// (2) sku 的图片信息: gulimall_pms 数据库中的 pms_sku_images 表List imagesEntities = skuImages.stream().map(img -> {SkuImagesEntity skuImagesEntity = new SkuImagesEntity();skuImagesEntity.setSkuId(skuInfoEntity.getSkuId());skuImagesEntity.setImgUrl(img.getImgUrl());skuImagesEntity.setDefaultImg(img.getDefaultImg());return skuImagesEntity;//    图片地址可能会null,过滤以下}).filter(item -> !StringUtils.isEmpty(item.getImgUrl())).collect(Collectors.toList());skuImagesService.saveBatch(imagesEntities);// (3) sku 的销售属性信息: gulimall_pms 数据库中的 pms_sku_sale_attr_value表 skuSaleAttrValueServiceList skuAttrs = sku.getAttr();List attrValueEntities = skuAttrs.stream().map(skuAttr -> {SkuSaleAttrValueEntity skuSaleAttrValueEntity = new SkuSaleAttrValueEntity();skuSaleAttrValueEntity.setSkuId(skuInfoEntity.getSkuId());BeanUtils.copyProperties(skuAttr, skuSaleAttrValueEntity);return skuSaleAttrValueEntity;}).collect(Collectors.toList());skuSaleAttrValueService.saveBatch(attrValueEntities);// (4) 保存商品的优惠、满减等信息。gulimall_sms 数据库中的 sms_sku_ladder、sms_sku_full_reduction、sms_member_price 表SkuReductionTo skuReductionTo = new SkuReductionTo();BeanUtils.copyProperties(sku,skuReductionTo);skuReductionTo.setSkuId(skuInfoEntity.getSkuId());R r = couponFeignService.saveSkuReduction(skuReductionTo);// 在 R 返回类中增加 getCode方法if (r.getCode() != 0) {log.error("远程保存商品优惠信息失败!!");}});}}

2、gulimall-coupon模块下的: SkuFullReductionServiceImpl,用来保存优惠信息

 /*** 保存商品优惠信息* */@Override@Transactionalpublic void saveSkuReduction(SkuReductionTo reductionTo) {// (4) 保存商品的优惠、满减等信息。gulimall_sms 数据库中的 sms_sku_ladder、sms_sku_full_reduction、sms_member_price 表// sms_sku_ladderSkuLadderEntity skuLadderEntity = new SkuLadderEntity();skuLadderEntity.setFullCount(reductionTo.getFullCount());skuLadderEntity.setDiscount(reductionTo.getDiscount());skuLadderEntity.setSkuId(reductionTo.getSkuId());skuLadderEntity.setAddOther(reductionTo.getCountStatus());// 如果有打折活动再去保存if (skuLadderEntity.getFullCount() > 0) {skuLadderService.save(skuLadderEntity);}//sms_sku_full_reductionSkuFullReductionEntity skuFullReductionEntity = new SkuFullReductionEntity();BeanUtils.copyProperties(reductionTo,skuFullReductionEntity);skuFullReductionEntity.setAddOther(reductionTo.getCountStatus());// 如果有满减活动再去保存if (skuFullReductionEntity.getFullPrice().compareTo(new BigDecimal("0")) == 1) {this.save(skuFullReductionEntity);}// sms_member_priceList memberPrice = reductionTo.getMemberPrice();List collect = memberPrice.stream().map(item -> {MemberPriceEntity memberPriceEntity = new MemberPriceEntity();memberPriceEntity.setMemberPrice(item.getPrice());memberPriceEntity.setMemberLevelId(item.getId());memberPriceEntity.setMemberLevelName(item.getName());memberPriceEntity.setSkuId(reductionTo.getSkuId());memberPriceEntity.setAddOther(reductionTo.getCountStatus());return memberPriceEntity;}).filter(item -> item.getMemberPrice().compareTo(new BigDecimal("0")) == 1).collect(Collectors.toList());memberPriceService.saveBatch(collect);}

二、Spu管理

Spu检索

1、接口访问路径

image-20230106184705695

2、请求参数

{page: 1,//当前页码limit: 10,//每页记录数sidx: 'id',//排序字段order: 'asc/desc',//排序方式key: '华为',//检索关键字catelogId: 6,//三级分类idbrandId: 1,//品牌id status: 0,//商品状态
}

3、响应数据

{"msg": "success","code": 0,"page": {"totalCount": 0,"pageSize": 10,"totalPage": 0,"currPage": 1,"list": [{"brandId": 0, //品牌id"brandName": "品牌名字","catelogId": 0, //分类id"catalogName": "分类名字","createTime": "2019-11-13T16:07:32.877Z", //创建时间"id": 0, //商品id"publishStatus": 0, //发布状态"spuDescription": "string", //商品描述"spuName": "string", //商品名字"updateTime": "2019-11-13T16:07:32.877Z", //更新时间"weight": 0 //重量}]}
}

4、SpuInfoController

    /*** 列表* SPU检索 : /product/spuinfo/list*/@RequestMapping("/list")//@RequiresPermissions("com.atguigu.gulimall.product:spuinfo:list")public R list(@RequestParam Map params){PageUtils page = spuInfoService.queryPageByCondition(params);return R.ok().put("page", page);}

5、SpuInfoServiceImpl

    /*** Spu检索查询* */@Overridepublic PageUtils queryPageByCondition(Map params) {//   key: '华为',//检索关键字//    catelogId: 6,//三级分类id//    brandId: 1,//品牌id//    status: 0,//商品状态QueryWrapper wrapper = new QueryWrapper<>();String key = (String) params.get("key");String catelogId = (String) params.get("catelogId");String brandId = (String) params.get("brandId");String status = (String) params.get("status");wrapper.eq( !StringUtils.isEmpty(key),"id", key).or().like(!StringUtils.isEmpty(key), "spu_name", key).eq(!StringUtils.isEmpty(catelogId) && !"0".equals(catelogId),"catelog_id", catelogId).eq(!StringUtils.isEmpty(brandId) && !"0".equals(brandId),"brand_id", brandId).eq(!StringUtils.isEmpty(status),"publish_status", status);IPage page = this.page(new Query().getPage(params),wrapper);return new PageUtils(page);}

规格维护

小BUG

点击 Spu 管理中的 规格 ,很多兄弟显示 404,找不到页面。

解决方法:

1、在 gulimall_admin 数据库中执行以下sql语句

INSERT INTO sys_menu (menu_id, parent_id, NAME, url, perms, TYPE, icon, order_num) VALUES (76, 37, '规格维护', 'product/attrupdate', '', 2, 'log', 0);

2、在前端 /src/router/index.js 里找到 mainRoutes 。在 children 中增加路由

    { path: '/product-attrupdate', component: _import('modules/product/attrupdate'), name: 'attr-update', meta: { title: '规格维护', isTab: true } }

获取 spu 规格

1、接口路径

GET /product/attr/base/listforspu/{spuId}

2、响应数据

{"msg": "success","code": 0,"data": [{"id": 43,"spuId": 11,"attrId": 7,"attrName": "入网型号","attrValue": "LIO-AL00","attrSort": null,"quickShow": 1}]
}

3、AttrController

    /*** spu规格参数维护——获取spu规格* GET /product/attr/base/listforspu/{spuId}*/@GetMapping("/base/listforspu/{spuId}")//@RequiresPermissions("com.atguigu.gulimall.product:attr:list")public R listForSpu(@PathVariable("spuId") Long  spuId) {List productAttrValueEntity = productAttrValueService.listBaseAttrForSpu(spuId);return R.ok().put("data", productAttrValueEntity);}

4、ProductAttrValueServiceImpl

    /*** 获取Spu规格参数* */@Overridepublic List listBaseAttrForSpu(Long spuId) {return this.list(new QueryWrapper().eq("spu_id",spuId));}

修改 spu 规格

1、访问接口路径

POST
/product/attr/update/{spuId}

2、请求参数

[{"attrId": 7,"attrName": "入网型号","attrValue": "LIO-AL00","quickShow": 1
}, {"attrId": 14,"attrName": "机身材质工艺","attrValue": "玻璃","quickShow": 0
}, {"attrId": 16,"attrName": "CPU型号","attrValue": "HUAWEI Kirin 980","quickShow": 1
}]

3、AttrController

    /*** spu规格参数维护——修改spu规格* /product/attr/update/{spuId}*/@PostMapping("/update/{spuId}")// @RequiresPermissions("com.atguigu.gulimall.product:attr:update")public R update(@PathVariable("spuId") Long spuId, @RequestBody List spuAttrList) {productAttrValueService.updateBaseAttrForSpu(spuId,spuAttrList);return R.ok();}

4、ProductAttrValueServiceImpl: 修改 spu 规格参数时,前端会将我们修改的所有属性值都返回过来,无论是空值还是修改的值。因此在修改spu规格时。只需要删除所有的 spu 规格参数,然后再批量保存即可。

    /*** 修改 spu 规格参数* */@Overridepublic void updateBaseAttrForSpu(Long spuId, List spuAttrList) {// 批量删除this.baseMapper.delete(new QueryWrapper().eq("spu_id",spuId));List collect = spuAttrList.stream().map(item -> {item.setSpuId(spuId);return item;}).collect(Collectors.toList());// 批量修改this.saveBatch(collect);}

三、商品管理

sku 检索

1、接口访问路径

image-20230106205830832

2、请求参数

{
page: 1,//当前页码
limit: 10,//每页记录数
sidx: 'id',//排序字段
order: 'asc/desc',//排序方式
key: '华为',//检索关键字
catelogId: 0,
brandId: 0,
min: 0,
max: 0
}

3、响应数据

{"msg": "success","code": 0,"page": {"totalCount": 26,"pageSize": 10,"totalPage": 3,"currPage": 1,"list": [{"skuId": 1,"spuId": 11,"skuName": "华为 HUAWEI Mate 30 Pro 星河银 8GB+256GB","skuDesc": null,"catalogId": 225,"brandId": 9,"skuDefaultImg": "https://gulimall-hello.oss-cn-beijing.aliyuncs.com/2019-11-26/60e65a44-f943-4ed5-87c8-8cf90f403018_d511faab82abb34b.jpg","skuTitle": "华为 HUAWEI Mate 30 Pro 星河银 8GB+256GB麒麟990旗舰芯片OLED环幕屏双4000万徕卡电影四摄4G全网通手机","skuSubtitle": "【现货抢购!享白条12期免息!】麒麟990,OLED环幕屏双4000万徕卡电影四摄;Mate30系列享12期免息》","price": 6299.0000,"saleCount": 0}]}
}

4、SkuInfoController

    /*** sku 检索* /product/skuinfo/list*/@RequestMapping("/list")//@RequiresPermissions("com.atguigu.gulimall.product:skuinfo:list")public R list(@RequestParam Map params){PageUtils page = skuInfoService.queryPageByCondition(params);return R.ok().put("page", page);}

5、SkuInfoServiceImpl

 /*** sku检索* */@Overridepublic PageUtils queryPageByCondition(Map params) {//key: '华为',//检索关键字// catelogId: 0,// brandId: 0,// min: 0,// max: 0QueryWrapper queryWrapper = new QueryWrapper<>();String key = (String) params.get("key");String catelogId = (String) params.get("catelogId");String brandId = (String) params.get("brandId");String min = (String) params.get("min");String max = (String) params.get("max");queryWrapper.eq(!StringUtils.isEmpty(key), "sku_id", key).or().like(!StringUtils.isEmpty(key), "sku_name", key).eq(!StringUtils.isEmpty(catelogId) && !"0".equals(catelogId),"catelog_id", catelogId).eq(!StringUtils.isEmpty(brandId) && !"0".equals(brandId),"brand_id", brandId).ge(!StringUtils.isEmpty(min),"price", min).le(!StringUtils.isEmpty(max) && new BigDecimal(max).compareTo(new BigDecimal("0")) == 1,"price", max);IPage page = this.page(new Query().getPage(params),queryWrapper);return new PageUtils(page);}

库存系统

一、整合ware服务&获取库存列表

1、配置文件中配置nacos注册中心地址。以及服务名

2、主启动类增加 @EnableDiscoveryClient 注解

3、Gateway网关配置路由

                - id: ware_routeuri: lb://gulimall-ware # 负载均衡predicates:- Path=/api/ware/**filters:- RewritePath=/api/?(?.*), /$\{segment}  #路径重写

4、模糊查询

访问路径: /ware/wareinfo/list

    @Overridepublic PageUtils queryPage(Map params) {QueryWrapper queryWrapper = new QueryWrapper<>();String key = (String) params.get("key");if (!StringUtils.isEmpty(key)) {queryWrapper.eq("id",key).or().like("name",key).or().like("address",key).like("areacode",key);}IPage page = this.page(new Query().getPage(params),queryWrapper);return new PageUtils(page);}

二、查询商品库存&创建采购需求

模糊查询商品库存

1、访问接口路径

GET  /ware/waresku/list

2、请求参数

{page: 1,//当前页码limit: 10,//每页记录数sidx: 'id',//排序字段order: 'asc/desc',//排序方式wareId: 123,//仓库idskuId: 123//商品id
}

3、模糊查询: WareSkuServiceImpl

  @Overridepublic PageUtils queryPage(Map params) {QueryWrapper queryWrapper = new QueryWrapper<>();//   wareId: 123,//仓库id//    skuId: 123//商品idString wareId = (String) params.get("wareId");String skuId = (String) params.get("skuId");queryWrapper.eq(!StringUtils.isEmpty(wareId),"ware_id",wareId).eq(!StringUtils.isEmpty(skuId),"sku_id",skuId);IPage page = this.page(new Query().getPage(params),queryWrapper);return new PageUtils(page);}

模糊查询采购需求

1、接口访问路径

GET /ware/purchasedetail/list

2、请求参数

{page: 1,//当前页码limit: 10,//每页记录数sidx: 'id',//排序字段order: 'asc/desc',//排序方式key: '华为',//检索关键字status: 0,//状态    wareId: 1,//仓库id
}

3、模糊查询: PurchaseDetailServiceImpl

 @Overridepublic PageUtils queryPage(Map params) {QueryWrapper queryWrapper = new QueryWrapper<>();//   key: '华为',//检索关键字//    status: 0,//状态//    wareId: 1,//仓库idString key = (String) params.get("key");String status = (String) params.get("status");String wareId = (String) params.get("wareId");// purchase_id sku_id status ware_idqueryWrapper.eq(!StringUtils.isEmpty(key),"purchase_id",key).or().eq(!StringUtils.isEmpty(key),"sku_id",key).eq(!StringUtils.isEmpty(status),"status",status).eq(!StringUtils.isEmpty(wareId),"ware_id",wareId);IPage page = this.page(new Query().getPage(params),queryWrapper);return new PageUtils(page);}

合并采购需求

合并采购单流程

image-20230106221906031

获取未领取的采购单

image-20230106222301254

采购单有五种状态,只有未领取的采购单才能合并。下面就查询未被领取的采购单,在页面中显示。

image-20230106222601610

1、PurchaseController

/*** 查询未领取的采购单*   /ware/purchase/unreceive/list*/
@RequestMapping("/unreceive/list")
//@RequiresPermissions("ware:purchase:list")
public R unreceiveList(@RequestParam Map params){PageUtils page = purchaseService.queryPageByunreceive(params);return R.ok().put("page", page);
}

2、PurchaseServiceImpl

   /*** 查询未领取的采购单* */@Overridepublic PageUtils queryPageByunreceive(Map params) {IPage page = this.page(new Query().getPage(params),new QueryWrapper().eq("status",0).or().eq("status",1));return new PageUtils(page);}

合并采购单

当我们选择合并到的采购单 会携带采购单的 id,将采购的商品增加到原有的采购单即可

image-20230106224020579

当没选择采购单时,就需要新创建一个采购单进行合并。

image-20230106224138676

1、访问接口路径

POST  /ware/purchase/merge

2、 请求参数

{purchaseId: 1, //整单iditems:[1,2,3,4] //合并项集合
}

3、合并采购单只需要修改 wms_purchase_detail 表中的 采购单id 以及 status 状态

image-20230107133324067

4、创建枚举类,表示采购单、采购需求的几种状态

public class WareConstant {// 采购单状态public enum PurchaseStatusEnum{CREATED(0,"新建"),ASSIGNEE(1,"已分配"),RECEIVE(2,"已领取"),FINISHED(3,"已完成"),HASERROR(4,"有异常");private int code ;private String msg ;PurchaseStatusEnum(int code, String msg) {this.code = code;this.msg = msg;}public int getCode() {return code;}public String getMsg() {return msg;}}// 采购需求状态public enum PurchaseDetailStatusEnum{CREATED(0,"新建"),ASSIGNEE(1,"已分配"),BUYING(2,"正在采购"),FINISHED(3,"已完成"),HASERROR(4,"采购失败");private int code ;private String msg ;PurchaseDetailStatusEnum(int code, String msg) {this.code = code;this.msg = msg;}public int getCode() {return code;}public String getMsg() {return msg;}}
}

5、PurchaseController

    /*** 合并采购单* /ware/purchase/merge*/@PostMapping("merge")public R merge(@RequestBody MergeVo mergeVo) {purchaseService.mergePurchase(mergeVo);return R.ok();}

6、PurchaseServiceImpl

/*** 合并采购单* */@Override@Transactionalpublic void mergePurchase(MergeVo mergeVo) {Long purchaseId = mergeVo.getPurchaseId();// 新建采购单if (purchaseId == null) {PurchaseEntity purchaseEntity = new PurchaseEntity();purchaseEntity.setCreateTime(new Date());purchaseEntity.setUpdateTime(new Date());purchaseEntity.setStatus(WareConstant.PurchaseStatusEnum.CREATED.getCode());this.save(purchaseEntity);// 获取新的采购单idpurchaseId = purchaseEntity.getId();}// 确认采购单状态,只有0或者1才能合并PurchaseEntity purchase = this.getById(purchaseId);Integer status = purchase.getStatus();if (status == WareConstant.PurchaseStatusEnum.CREATED.getCode() ||status == WareConstant.PurchaseStatusEnum.ASSIGNEE.getCode()) {// 合并采购单List items = mergeVo.getItems();Long finalPurchaseId = purchaseId;List collect = items.stream().map(item -> {PurchaseDetailEntity purchaseDetailEntity = new PurchaseDetailEntity();// 1、设置采购需求的采购单purchaseDetailEntity.setPurchaseId(finalPurchaseId);// 2、设置采购需求的状态为 已分配purchaseDetailEntity.setStatus(WareConstant.PurchaseDetailStatusEnum.ASSIGNEE.getCode());purchaseDetailEntity.setId(item);return purchaseDetailEntity;}).collect(Collectors.toList());// 批量修改purchaseDetailService.updateBatchById(collect);// 同时更新采购单的修改时间PurchaseEntity purchaseEntity = new PurchaseEntity();purchaseEntity.setUpdateTime(new Date());purchaseEntity.setId(purchaseId);this.updateById(purchaseEntity);}}

领取采购单

该接口功能对接员工系统,在员工系统中会展示可领取的采购单。因此使用 postMan 模拟员工系统发送请求领取采购单。

员工领取采购单的要求:

  • 只有采购单为新建、已分配状态才能领取
  • 修改采购单状态为已领取
  • 修改采购需求的状态为正在采购

1、访问接口路径

POST /ware/purchase/received

2、请求参数

[1,2,3,4]//采购单id

3、PurchaseController

    /*** 领取采购单* /ware/purchase/merge*/@PostMapping("received")public R received(@RequestBody List purchaseIds) {purchaseService.receivedPurchase(purchaseIds);return R.ok();}

4、PurchaseServiceImpl

  /*** 领取采购单* */@Override@Transactionalpublic void receivedPurchase(List purchaseIds) {// - 判断采购单状态// (1) 根据 purchaseId 查询出所有的采购单// (2) 过滤掉采购单状态不为 0 或者 1 ,剩下的就是可领取的采购单List unReceivePurchases = purchaseIds.stream().map(this::getById).filter(entity -> entity.getStatus() == WareConstant.PurchaseStatusEnum.CREATED.getCode() ||entity.getStatus() == WareConstant.PurchaseStatusEnum.ASSIGNEE.getCode()).collect(Collectors.toList());// - 修改采购单状态为已领取List purchaseEntityList = unReceivePurchases.stream().map(item -> {PurchaseEntity purchaseEntity = new PurchaseEntity();purchaseEntity.setId(item.getId());// 设置采购单状态为已领取purchaseEntity.setStatus(WareConstant.PurchaseStatusEnum.RECEIVE.getCode());return purchaseEntity;}).collect(Collectors.toList());this.updateBatchById(purchaseEntityList);// - 修改采购需求的状态为正在采购unReceivePurchases.forEach(item -> {// 查询出采购单中所有的采购需求List purchaseDetailEntityList =purchaseDetailService.list(new QueryWrapper().eq("purchase_id", item.getId()));// 修改每一个采购需求中的statusList collect = purchaseDetailEntityList.stream().map(entity -> {PurchaseDetailEntity purchaseDetailEntity = new PurchaseDetailEntity();BeanUtils.copyProperties(entity, purchaseDetailEntity);// 修改采购需求的状态为正在采购purchaseDetailEntity.setStatus(WareConstant.PurchaseDetailStatusEnum.BUYING.getCode());return purchaseDetailEntity;}).collect(Collectors.toList());purchaseDetailService.updateBatchById(collect);});}

完成采购

1、接口访问路径

POST /ware/purchase/done

2、请求参数

{id: 123,//采购单iditems: [{itemId:1,status:4,reason:""}] //完成/失败的需求详情
}

思路分析:

  1. 设置采购项的状态

    1. 采购项的状态根据请求参数中的 status 决定的。status=3:完成采购,status=4:采购失败
  2. 设置采购单的状态,采购单的状态是根据采购项的状态决定的

    1. 如果所有的采购项都采购成功,那么采购单的状态是 FINISHED
    2. 如果有一个采购项没有采购成功,那么采购单的状态是 HASERROR
  3. 设置库存

    1. 将采购成功的采购项增加到库存当中

      1. 如果库存中没有这个采购项,就新建一个采购项的库存
      2. 如果库存中有这个采购项,就修改库存中采购项的数量

3、根据请求参数创建Vo对象

@Data
public class PurchaseDoneVo {//    id: 123,//采购单id//    items: [{itemId:1,status:4,reason:""}] //完成/失败的需求详情private Long id ;private List items ;
}@Data
public class PurchaseDoneItemVo {//    itemId:1,status:4,reason:""private Long itemId;private Integer status;private String reason;
}

4、PurchaseController

    /*** 完成采购*  POST /ware/purchase/done* */@PostMapping("done")public R done(@RequestBody PurchaseDoneVo purchaseDoneVo) {purchaseService.donePurchase(purchaseDoneVo);return R.ok();}

5、PurchaseServiceImpl

 /*** 完成采购* */@Override@Transactionalpublic void donePurchase(PurchaseDoneVo purchaseDoneVo) {Long purchaseId = purchaseDoneVo.getId();// 采购项集合List items = purchaseDoneVo.getItems();// 保存采购项ArrayList purchaseDetailEntities = new ArrayList<>();boolean isError = true;for (PurchaseDoneItemVo item : items) {PurchaseDetailEntity purchaseDetailEntity = new PurchaseDetailEntity();purchaseDetailEntity.setId(item.getItemId());// 设置采购项状态purchaseDetailEntity.setStatus(item.getStatus());purchaseDetailEntities.add(purchaseDetailEntity);if (item.getStatus() == WareConstant.PurchaseDetailStatusEnum.HASERROR.getCode()) {// 如果有采购项失败isError = false;}else{// 3、采购成功的采购项,设置库存PurchaseDetailEntity detailEntity = purchaseDetailService.getById(item.getItemId());wareSkuService.addStock(detailEntity.getSkuId(),detailEntity.getWareId(),detailEntity.getSkuNum());}}PurchaseEntity purchaseEntity = new PurchaseEntity();purchaseEntity.setId(purchaseId);purchaseEntity.setStatus(isError ? WareConstant.PurchaseStatusEnum.FINISHED.getCode() : WareConstant.PurchaseStatusEnum.HASERROR.getCode());purchaseEntity.setUpdateTime(new Date());// 2、更新采购单状态this.updateById(purchaseEntity);// 1、批量更新采购项的状态purchaseDetailService.updateBatchById(purchaseDetailEntities);}

6、WareSkuServiceImpl

 /** 设置库存* */@Overridepublic void addStock(Long skuId, Long wareId, Integer skuNum) {QueryWrapper queryWrapper = new QueryWrapper().eq("sku_id",skuId).eq("ware_id",wareId);WareSkuEntity entity = this.baseMapper.selectOne(queryWrapper);if (entity == null) {// 没有对应的采购项库存,就新增WareSkuEntity wareSkuEntity = new WareSkuEntity();wareSkuEntity.setSkuId(skuId);wareSkuEntity.setWareId(wareId);wareSkuEntity.setStock(skuNum);// TODO:设置skuName,需要远程调用wareSkuEntity.setSkuName("");this.baseMapper.insert(wareSkuEntity);}else {// 说明有与之对应的采购项库存,新增库存// 没有对应的采购项库存,就新增WareSkuEntity wareSkuEntity = new WareSkuEntity();wareSkuEntity.setStock(entity.getStock() + skuNum);wareSkuEntity.setId(entity.getId());this.baseMapper.updateById(wareSkuEntity);}}

分布式基础篇总结

1、分布式基础概念

​ • 微服务、注册中心、配置中心、远程调用、Feign、网关

2、基础开发

​ • SpringBoot2.0、SpringCloud、Mybatis-Plus、Vue组件化、阿里云对象存储

3、环境

​ • Vagrant、Linux、Docker、MySQL、Redis、逆向工程&人人开源

4、开发规范

​ • 数据校验JSR303、全局异常处理、全局统一返回、全局跨域处理

​ • 枚举状态、业务状态码、VO与TO与PO划分、逻辑删除

​ • Lombok:@Data、@Slf4j

相关内容

热门资讯

【NI Multisim 14...   目录 序言 一、工具栏 🍊1.“标准”工具栏 🍊 2.视图工具...
银河麒麟V10SP1高级服务器... 银河麒麟高级服务器操作系统简介: 银河麒麟高级服务器操作系统V10是针对企业级关键业务...
不能访问光猫的的管理页面 光猫是现代家庭宽带网络的重要组成部分,它可以提供高速稳定的网络连接。但是,有时候我们会遇到不能访问光...
AWSECS:访问外部网络时出... 如果您在AWS ECS中部署了应用程序,并且该应用程序需要访问外部网络,但是无法正常访问,可能是因为...
Android|无法访问或保存... 这个问题可能是由于权限设置不正确导致的。您需要在应用程序清单文件中添加以下代码来请求适当的权限:此外...
北信源内网安全管理卸载 北信源内网安全管理是一款网络安全管理软件,主要用于保护内网安全。在日常使用过程中,卸载该软件是一种常...
AWSElasticBeans... 在Dockerfile中手动配置nginx反向代理。例如,在Dockerfile中添加以下代码:FR...
AsusVivobook无法开... 首先,我们可以尝试重置BIOS(Basic Input/Output System)来解决这个问题。...
ASM贪吃蛇游戏-解决错误的问... 要解决ASM贪吃蛇游戏中的错误问题,你可以按照以下步骤进行:首先,确定错误的具体表现和问题所在。在贪...
月入8000+的steam搬砖... 大家好,我是阿阳 今天要给大家介绍的是 steam 游戏搬砖项目,目前...