M5(项目)-01-尚硅谷谷粒商城项目分布式基础篇开发文档
分布式基础篇
一、环境搭建
- 各种开发软件的安装
虚拟机: docker,mysql,redis
主机: Maven, idea(后端),VsCode(前端),git,NodeJS
版本和安装过程略
- 快速搭建基本的前后端框架
(1). 前端(vscode):下载并导入人人开源后台管理系统vue端脚手架工程renren-fast-vue
坑点1:使用vscode导入renren-fast-vue工程时一定要注意自身NodeJS版本是否匹配package.json中node-sass的版本,如果不匹配则加载不出登录页面。我的NodeJS版本是v12.18.2,package.json中**“node-sass”: “^4.14.1”**
(2). 后端(idea):下载并导入人人开源后台管理系统renren-fast和逆向工程renren-generator快速生成dao,entity,service,controller基本CRUD代码以及mapper映射文件。
在创建各个微服务模块前先创建一个公共模块gulimall-common,用以承载每一个微服务公共的依赖,bean,工具类等,各模块只需要在pom.xml文件中依赖这个公共模块就可以大大简化代码。由于要使用renren-fast,大部分依赖和工具类以及bean都要参考renren-fast,从renren-fast中抽取出需要用到的部分。
搭建好公共模块后再创建各微服务模块,不要忘记在父项目gulimall的pom.xml中引用各module。不同的微服务模块使用逆向工程要更改renren-generator模块application.yml中url的表名以及generator.properties文件中的模块名和表前缀,最后分别启动模块测试接口是否可以正常访问。
bug和tip:
①提示-在使用renren-generator快速生成时一定要看清下方共有几页,保证所有表都在一页里展示,这样全选后生成的代码才不会有缺失。
②bug1-各模块配置yml文件时由于粗心使得datasource中的url缩进错误(删除了url:冒号后的空格)使得运行后抛出异常。总结:yml文件一定要注意格式问题
③bug2-由于本人使用的VMware,在开关机后发现centos7的ip一直在变动,主机访问起来非常麻烦。就百度CentOS7设置固定IP地址将IP锁死。但是过了一会后发现xshell和sqlyog都不能正常连接虚拟机,打开VMware->编辑->虚拟网络编辑器中发现VMnet8中子网IP和NAT设置中网关IP都有问题(检查前三个数字和自己固定的Ip是否一致),修改后问题解决。[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3HruWPLj-1646456434649)(C:\Users\ck\AppData\Roaming\Typora\typora-user-images\1644492852234.png)]
- 使用SpringCloud Alibaba以及SpringCloud搭建微服务组件
![KP%I78%XQ_D[KRJNY_]ZPK](D:\life\qq\2073412335\FileRecv\MobileFile\Image\K
P%I78%XQ_D[KRJNY_]ZPK.png)
3.1 使用SpringCloud Alibaba-Nacos配置注册中心(服务发现/注册)
①服务发现是每个微服务都需要包含的功能,所以在common模块pom.xml中创建springcloud alibaba的依赖管理来控制版本并在dependency中依赖spring-cloud-starter-alibaba-nacos-discovery
②其次,在各个微服务模块的yml文件中配置服务发现的服务端地址,默认是8848端口,spring.cloud.nacos.discovery.server-addr:127.0.0.1:8848和spring.application.name服务名。并在启动类上使用注解**@EnableDiscoveryClient标识。在下载(快速下载地址http://8090top.cn-sh2.ufileos.com/centos/nacos-server-1.3.1.zip)并双击nacos-server的bin目录下的startup.cmd启动nacos服务端后,再启动微服务,访问127.0.0.1:8848/nacos**用账密nacos登陆后查看微服务是否被注册到注册中心
3.2 使用SpringCloud-Feign远程调用其它微服务-member会员模块调用coupon优惠券模块的demo
远程调用别的服务的步骤(此处测试调用coupon优惠券服务)
* 1.pom.xml文件中引入spring-cloud-starter-openfeign,使该服务具有远程调用其它微服务的能力
* 2.创建一个Feign包(盛放远程调用服务的接口)并在包下编写一个接口,使用注解@FeignClient("远程服务名")告诉springcloud该接口需要远程调用的服务,且接口的每个方法告知调用该服务的哪个请求即都是对应该服务的控制器方法签名,注意请求路径完整
* 3.开启远程调用其它服务的功能,在启动类上使用注解@EnableFeignClients(basePackages = "feign包的全包名"),这样一旦启动该服务则会自动扫描feign包下使用@FeignClient注解的接口
* 4.在自身控制层写一个远程调用其它服务的方法,注入feign包下需要调用其它服务的接口,在该控制器方法中可以使用注入的接口调用目标方法来获取远程调用其它服务中目标请求返回的数据。demo如下图
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jk9eBANp-1646456434651)(C:\Users\ck\AppData\Roaming\Typora\typora-user-images\1644591989939.png)]
3.3 使用SpringCloud Alibaba-Nacos实现动态配置管理
①导入依赖。nacos-server兼顾了注册中心和配置中心,每个微服务都需要动态配置管理,则统一在common模块依赖spring-cloud-starter-alibaba-nacos-config
②在服务的resources下创建bootstrap.properties文件,该文件执行优先级高于application.properties。并配置Nacos Config元数据:服务名和配置中心地址
spring.application.name=gulimall-coupon
spring.cloud.nacos.config.server-addr=127.0.0.1:8848
③在nacos-server的配置列表中新建配置,Data ID是配置文件的名字(默认是服务名.properties),选好配置格式properties并填写需要动态管理的配置信息后发布。
④在用到配置文件的控制器(Controller层)中加上@RefreshScope注解,这样在nacos-server配置中心的配置列表里点编辑就可以动态修改配置文件后发布。注意:如果配置中心和applicaiton.properties都配置了相同项的配置,则优先配置中心的值。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iXabPHQA-1646456434652)(C:\Users\ck\AppData\Roaming\Typora\typora-user-images\1646384322154.png)]
补充几个概念:
命名空间-隔离配置的作用。添加不同的命名空间,思路一–应用于不同的环境需求,如prop生产环境,test测试环境和dev开发环境,默认是public,且public下的配置默认优先读取。思路二–应用于不同微服务的配置隔离。如果需要声明优先读取的命名空间则需要在bootstrap.properties中定义spring.cloud.nacos.config.namespace=命名空间ID/命名空间名称(新版nacos填写名称即可,旧版需要填写命名空间的ID)
配置集-所有的配置的集合
配置集ID-Data ID,作用上类似于文件名,官方文档命名规范是服务名-环境名.yml/properties
配置分组-默认所有的配置集都属于DEFAULT_GROUP。根据需求可以新增不同的GROUP,如双11,618等分组。在特殊时期可以修改bootstrap.properties中spring.cloud.nacos.config.group的值来指定分组。
综上-命名空间和配置分组结合使用可以区分生产环境和微服务。官方建议使用命名空间区分生产环境,而通过配置分组来区分微服务。当然,也可以给每一个微服务都声明一个以微服务名为一个命名空间,并在配置分组下定义不同生产环境dev,prod等即使用命名空间区分微服务而配置分组区分生产环境。
如果有配置文件内容过大需要拆分的需求,可以在bootstrap.properties中使用spring.cloud.nacos.config.ext-config[索引0开始].data-id=拆分配置文件名,spring.cloud.nacos.config.ext-config[索引0开始].group=拆分配置文件分组,spring.cloud.nacos.config.ext-config[索引0开始].refresh=true/false是否自动刷新
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Xp4N4sKG-1646456434653)(C:\Users\ck\AppData\Roaming\Typora\typora-user-images\1644648737804.png)]
3.4 使用SpringCloud-GateWay作为API网关(Webflux编程模式)
①先创建gulimall-gateway模块,添加gateway模块并在pom文件中引入gulimall-common公共依赖模块
注意–公共模块中有引入Mybatis-plus则配置文件中必须定义数据源,但网关模块不需要此处功能,可以通过给启动类注解添加exclude属性排除掉跟数据库有关的自动配置@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})。当然也可以不引入common模块,手动在pom文件中添加需要的依赖也行。
②启动类上添加**@EnableDiscoveryClient注解用于开启发现服务的功能并在application.properties中定义服务注册中心地址spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848**,应用名以及端口号。
# 应用名称
spring.application.name=gulimall-gateway
spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848
server.port=88
添加配置中心配置文件bootstrap.properties,定义应用名/服务名,配置中心地址和命名空间等信息。
spring.application.name=gulimall-gateway
spring.cloud.nacos.config.server-addr=127.0.0.1:8848
spring.cloud.nacos.config.namespace=dev
spring.cloud.nacos.config.group=DEFAULT_GROUP
二、基础篇数据库表设计及说明
1.gulimall_pms库-商品模块
属性表pms_attr
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6PhRQsqP-1646456434654)(C:\Users\ck\AppData\Roaming\Typora\typora-user-images\1646389682443.png)]
用于存放商品的规格参数|基本属性(attr_type=1)和销售属性(attr_type=0),兼有分类Id字段即catelog_id
属性分组表pms_attr_group
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nOMckmuD-1646456434654)(C:\Users\ck\AppData\Roaming\Typora\typora-user-images\1646389624761.png)]
用于存放属性的分组信息,兼有分类Id字段即catelog_id。属性分组和属性的关系是一对多,即一个属性分组可以有很多所属属性但是一个属性只有唯一的一个属性分组。
属性及属性分组关联表pms_attr_attrgroup_relation
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-J7RuAeUl-1646456434655)(C:\Users\ck\AppData\Roaming\Typora\typora-user-images\1646389998312.png)]
用于存放属性和属性分组的关联关系,设计中间表的初衷是不使用外键。
分类表pms_category
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dX6fDcTO-1646456434656)(C:\Users\ck\AppData\Roaming\Typora\typora-user-images\1646390279742.png)]
用于存放所有的分类信息。设计的分类信息是以三层父子树形结构存放
品牌表pms_brand
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-V2GxDmOj-1646456434656)(C:\Users\ck\AppData\Roaming\Typora\typora-user-images\1646390543905.png)]
用于存放所有的品牌信息
分类及品牌关联表pms_category_brand_relation
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lJf8PL31-1646456434657)(C:\Users\ck\AppData\Roaming\Typora\typora-user-images\1646390633545.png)]
用于存放品牌和和分类的关联关系,分类和品牌是一对多的关系,即一个分类有多个所属品牌但一个品牌只有一个所属分类。此外还冗余设计了品牌名brand_name和分类名catelog_name,减少了查库的需求和压力但是有可能面对品牌名和分类名失真的情况需要在更新品牌名和分类名时级联更新中间表信息。
标准化产品单元Spu主体信息表pms_spu_info
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-M2shhzqp-1646456434658)(C:\Users\ck\AppData\Roaming\Typora\typora-user-images\1646391410662.png)]
用于存放spu主体信息,兼有spu所属的分类Id和品牌Id。
Spu图集表pms_spu_images
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cR61j2lz-1646456434658)(C:\Users\ck\AppData\Roaming\Typora\typora-user-images\1646391687373.png)]
用于存放spu的商品图片
Spu简介图片表pms_spu_info_desc
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OjrummwX-1646456434659)(C:\Users\ck\AppData\Roaming\Typora\typora-user-images\1646391807415.png)]
用于存放spu的简介图片
Spu属性及属性值表pms_spu_attr_value
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XE9DdiHd-1646456434660)(C:\Users\ck\AppData\Roaming\Typora\typora-user-images\1646395030716.png)]
用于接收spu时实际设置的规格参数|基本属性的值,以及设置是否快速展示。
库存量单位Sku主体信息表pms_sku_info
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1GRRIbze-1646456434660)(C:\Users\ck\AppData\Roaming\Typora\typora-user-images\1646391960684.png)]
用于存放发行商品的主体信息,兼有了sku_id字段。标准化产品单元spu和库存量单位sku是一对多的关系,即一个spu可以有多个对应的sku,但一个sku只有唯一对应的一个spu。
Sku发行商品图集表pms_sku_images
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1meeS0bs-1646456434661)(C:\Users\ck\AppData\Roaming\Typora\typora-user-images\1646394151229.png)]
用于存放发行商品的图片集,img_url字段存放的图片链接,default_img字段1或0表示是否是默认展示图片
一个sku_id即一个sku商品可以对应多个图片但一个图片只能对应一个sku_id
Sku发行商品属性表pms_sku_sale_attr_value
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-T1lvfrDY-1646456434661)(C:\Users\ck\AppData\Roaming\Typora\typora-user-images\1646395377653.png)]
用于接收设置发行商品时实际设置的销售属性的值。
2.gulimall_sms库-优惠券模块
sku满减信息表sms_sku_full_reduciton
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1t5m0pCQ-1646456434662)(C:\Users\ck\AppData\Roaming\Typora\typora-user-images\1646397192476.png)]
用于存放sku的满减信息
sku折扣表sms_sku_ladder
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rd67gnn4-1646456434662)(C:\Users\ck\AppData\Roaming\Typora\typora-user-images\1646397310063.png)]
用于存放sku的打折信息
sku会员价表sms_member_price
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TFAo4eCI-1646456434663)(C:\Users\ck\AppData\Roaming\Typora\typora-user-images\1646397448583.png)]
用于存放会员的sku价格信息
spu积分表sms_spu_bounds
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vGCTX74O-1646456434663)(C:\Users\ck\AppData\Roaming\Typora\typora-user-images\1646397539290.png)]
用于存放spu的成长积分和金币获取量
3.gulimall_wms库-库存模块
仓库维护表wms_ware_info
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XHBzLb36-1646456434664)(C:\Users\ck\AppData\Roaming\Typora\typora-user-images\1646455311978.png)]
三、业务实现
1. 商品模块-三级菜单 - pms_category表的CRUD
1.1 给pms_category商品分类表插入数据 略
1.2 修改gulimall-product模块CategoryController控制器方法list()
将原先请求路径/list改为/list/tree,将获取到的分类数据以父子的树形结构返回
/**
* 查出所有分类以及子分类,以树形结构组装起来
*/
@RequestMapping("/list/tree")
//@RequiresPermissions("product:category:list")
public R list(){
//list()方法能查到所有分类但是我们需要查到所有分类并用将父子分类以树形结构组装起来 categoryService.list();
//所以要自定义一个方法listWithTree()
List<CategoryEntity> entities = categoryService.listWithTree();
return R.ok().put("data", entities);
}
1.3 创建CategoryService接口方法listWithTree()并在实现类CategoryServiceImpl中实现
@Override
public List<CategoryEntity> listWithTree() {
//1.查出所有分类
/*自定义方法listWithTree()需要用categoryDao查询所有分类,可以@Autowire手动注入,
但由于该实现类CategoryServiceImpl继承了ServiceImpl,并将CategoryDao传入
所以此时baseMapper指向的就是CategoryDao。使用baseMapper一样可以调用selectList()方法
查询所有分类*/
List<CategoryEntity> entities = baseMapper.selectList(null);
//2.组装成父子树形结构
//2.1 找到所有的一级分类 - 特点 父分类id为0即parent_cid=0
//用流的方式根据需求过滤不满足需求的集合数据并收集到一个新的集合中
/*未在CategoryEntity自定义children属性递归查询子分类数据
List<CategoryEntity> level1Menus = entities.stream().filter((categoryEntity -> {
return categoryEntity.getParentCid() == 0;
})).collect(Collectors.toList());*/
//定义了children属性 有设置children属性和排序的需求 用到map()和sort()
List<CategoryEntity> level1Menus = entities.stream().filter((categoryEntity -> {
return categoryEntity.getParentCid() == 0;
})).map(menu -> {
//menu是遍历到的一级菜单项
menu.setChildren(getChildrens(menu,entities));
return menu;
}).sorted((menu1,menu2)->{
//这里sort是Interger类型 有可能为null出现空指针异常 使用三元表达式排除这种情况
return (menu1.getSort()==null?0:menu1.getSort()) - (menu2.getSort()==null?0:menu2.getSort());
}).collect(Collectors.toList());
return level1Menus;
}
//递归查找所有菜单的子菜单 root是当前菜单 all是需要所有过滤的菜单集合-即查询到的所有分类标签数据
private List<CategoryEntity> getChildrens(CategoryEntity root, List<CategoryEntity> all) {
List<CategoryEntity> children = all.stream().filter(categoryEntity -> {
//过滤思路-查看当前菜单的分类id即cat_id是否等于待过滤元素的父菜单id即parent_cid
//return categoryEntity.getParentCid() == root.getCatId();
//不能使用 == 判断,因为id是Long类型-包装类,如果id超过127则结果错误(包装类缓存池)
return categoryEntity.getParentCid().equals(root.getCatId());
}).map(categoryEntity -> {
//找到子菜单
categoryEntity.setChildren(getChildrens(categoryEntity, all));
return categoryEntity;
}).sorted((menu1, menu2) -> {
//这里sort是Interger类型 有可能为null出现空指针异常 使用三元表达式排除这种情况
return (menu1.getSort()==null?0:menu1.getSort()) - (menu2.getSort()==null?0:menu2.getSort());
}).collect(Collectors.toList());
return children;
}
开启服务后测试接口返回数据是否正确。
1.4 在vscode的终端中使用npm run dev启动renren-fast-vue打开快速开发平台并创建商品系统目录以及分类维护菜单
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EUZ4eRps-1646456434664)(C:\Users\ck\AppData\Roaming\Typora\typora-user-images\1644764717248.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EqSFsoC8-1646456434665)(C:\Users\ck\AppData\Roaming\Typora\typora-user-images\1644764827247.png)]
参考系统管理目录下其它菜单的设置编写好菜单URL在对应的src/views/modules下创建product文件夹并在该文件下创建category.vue文件 由于路由配置都被平台快速配置好了 这样在点击商品系统的分类维护后就会自动渲染category.vue模块。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-O0lUn0CX-1646456434665)(C:\Users\ck\AppData\Roaming\Typora\typora-user-images\1644765013224.png)]
1.5 参考其它模块如role.vue编写category.vue文件,添加getMenus()方法用以获取后台分类数据信息并在钩子函数created()中调用
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-F9k3pQU7-1646456434666)(C:\Users\ck\AppData\Roaming\Typora\typora-user-images\1644813949127.png)]
点击开发平台的分类维护菜单发送请求,查看Network发现发送的请求地址有问题,应该发给的是localhost:10001/product/category/list/tree,实际却是如下
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yMo3LpSu-1646456434666)(C:\Users\ck\AppData\Roaming\Typora\typora-user-images\1644814101472.png)]
所以应该修改static\config\index.js文件中的基准路径baseUrl,(注意:经前端发送的请求都添加一个请求前缀api )将所有请求都发给网关,让网关去处理
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-POWzDjsb-1646456434667)(C:\Users\ck\AppData\Roaming\Typora\typora-user-images\1644814471993.png)]
再重新刷新页面查看请求地址发现验证码请求直接给了网关,而验证码是renren-fast服务的功能,所以需要将renren-fast服务添加到服务注册中心。
1.6 将renren-fast服务添加到注册中心
给renren-fast服务添加公共模块gulimall-common(服务注册到注册中心)并修改yml文件添加服务名和注册中心地址,再给启动类加上服务发现注解**@EnableDiscoveryClient**
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-K9y2kKsV-1646456434667)(C:\Users\ck\AppData\Roaming\Typora\typora-user-images\1644814582667.png)]
1.7 修改gulimall-gateway的配置文件,将所有前端发送的请求暂时都负载均衡到renren-fast服务,具体模块发送的请求后期细化
再修改网关的application.yml文件,将所有前端发送的请求都负载均衡到renren-fast服务
断言定义的是所有的前端请求/api/**,过滤器是重写了路径 去掉了/api前端项目前缀并添加了/renren-fast的项目名地址
spring:
cloud:
gateway:
routes:
- id: admin_route
uri: lb://renren-fast
predicates:
- Path=/api/**
filters:
- RewritePath=/api/(?<segment>.*),/renren-fast/$\{segment}
## 前端项目 请求前缀/api
## 发送的请求http://localhost:88/api/captcha.jpg 不加路径重写的实际请求是http://localhost:8080/api/captcha.jpg
## 需要转成http://localhost:8080/renren-fast/captcha.jpg /renren-fast是renren-fast模块的应用名访问时需要添加的项目名地址
## server.servlet.context-path=/renren-fast
此处网关的路径重写注意网关版本
3.x的gateway路径重写- RewritePath=/api/?(?<segment>.*),/renren-fast\{segment}
2.x的gateway路径重写- RewritePath=/api/(?<segment>.*), /renren-fast/$\{segment}
1.8 解决跨域问题-通过添加配置类给容器返回CorsWebFilter从而给预检请求添加响应头实现请求跨域
填写完验证码点击登陆时出现了跨域问题
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JEDwFJpN-1646456434668)(C:\Users\ck\AppData\Roaming\Typora\typora-user-images\1644819190993.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AcvKdjXy-1646456434668)(C:\Users\ck\AppData\Roaming\Typora\typora-user-images\1644819858499.png)]
解决办法:①采用代理服务器nginx ②写跨域配置类,让服务器响应预检请求OPTIONS允许本次请求跨域。本项目采用②
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VoGZLbCA-1646456434669)(C:\Users\ck\AppData\Roaming\Typora\typora-user-images\1644819977068.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-G2ZPTzPT-1646456434669)(C:\Users\ck\AppData\Roaming\Typora\typora-user-images\1644820154233.png)]
给gulimall-gateway服务下创建config包以及配置类,将springboot提供的CorsWebFilter配置(该过滤器配置类作用是配置预检请求的响应头)后存入容器
注意创建UrlBasedCorsConfigurationSource时使用reactive包,另外由于renren-fast项目也设置了跨域处理见renren-fast/src/main/java/io/renren/config/CorsConfig.java,将代码屏蔽后重新运行网关和renrenfast看跨域问题是否解决
@Configuration
public class GulimallCorsConfiguration {
@Bean
public CorsWebFilter corsWebFilter(){
//创建CorsWebFilter对象需要传入CorsConfigurationSource配置源
//而CorsConfigurationSource是一个接口,实现类是reactive包下的UrlBasedCorsConfigurationSource
// org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource 响应式编程
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
//注册跨域配置 参1设置需要跨域的请求 参2传入跨域配置类
CorsConfiguration corsConfiguration = new CorsConfiguration();
corsConfiguration.addAllowedHeader("*");//允许哪些请求头跨域
corsConfiguration.addAllowedMethod("*");//允许哪些请求方式跨域
corsConfiguration.addAllowedOrigin("*");//允许哪些请求来源跨域
corsConfiguration.setAllowCredentials(true);//允许携带cookie跨域
source.registerCorsConfiguration("/**",corsConfiguration);
return new CorsWebFilter(source);
}
}
1.9 细化网关的routes配置,给product商品服务模块发送的请求提前(精确路由放在粗略路由之前)
跨域问题解决后,再在gulimall-gateway的applicaiton.yml里给网关添加前端发送/api/product/商品服务的请求时的路由配置 此处注意精确路由需要在粗略路由前面,否则/api/product/**的请求就会被/api/捕获,路由到错误服务中
spring:
cloud:
gateway:
routes:
- id: product_route
uri: lb://gulimall-product
predicates:
- Path=/api/product/**
filters:
- RewritePath=/api/(?<segment>.*),/$\{segment}
- id: admin_route
uri: lb://renren-fast
predicates:
- Path=/api/**
filters:
- RewritePath=/api/(?<segment>.*),/renren-fast/$\{segment}
## 前端项目 请求前缀/api
## 发送的请求http://localhost:88/api/captcha.jpg 不加路径重写的实际请求是http://localhost:8080/api/captcha.jpg
## 需要转成http://localhost:8080/renren-fast/captcha.jpg /renren-fast是renren-fast模块的应用名访问时需要添加的项目名地址
## server.servlet.context-path=/renren-fast
## 细化前端项目发的请求 如商品服务/api/product 另外精确路由需要在粗略路由前面
## 前端分类维护功能发送请求http://localhost:88/api/product/category/list/tree
## 后端需要接收的请求是http://localhost:10001/product/category/list/tree 去掉api前缀就行
1.10 校验前端是否正常获取到后端发送的分类数据,导入树形控件,绑定标签展示值和标签的子树
配置后重启项目检查是否可以获取到正确的三级分类数据,之后进入VScode,修改发送ajax请求响应成功后的函数,解构响应数据中的{data},这样data.data就是后端分类并组装好的树形分类数据。
<script>
export default {
data() {
return {
menus: [],
defaultProps: {
//label:哪个属性是作为标签的值需要展示出来,children:哪个属性需要作为标签的子树
children: "children",
label: "name",
},
};
},
methods: {
handleNodeClick(menus) {
console.log(menus);
},
getMenus() {
this.$http({
url: this.$http.adornUrl("/product/category/list/tree"),
method: "get",
}).then( ({data}) => {
//{data}解构 将响应的对象中data解构出来 data.data就是查询到的数据集合
console.log("成功获取到菜单数据。。", data.data);
this.menus = data.data;
});
},
},
created() {
this.getMenus();
},
};
</script>
再根据elementUI官方文档的树形控件使用说明在template中导入
<div>
<el-tree
:data="menus"
:props="defaultProps"
@node-click="handleNodeClick"
></el-tree>
</div>
并在data中修改defaultProps的label和children值,具体见上面
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aAYa4Q2j-1646456434670)(C:\Users\ck\AppData\Roaming\Typora\typora-user-images\1644847950427.png)]
1.11 给树形控件添加增删按钮
给树形控件加入append添加和delete删除选项 并给el-tree配置属性:expand-on-click-node="false"点击后不会自动展开折叠和可以选中的框show-checkbox(不用赋值true,不然报错)以及node-key="catId"每个树节点用来作为唯一标识的属性,并删除之前的点击事件,并在methods中定义append&delete方法
<el-tree
:data="menus"
:props="defaultProps"
:expand-on-click-node="false"
show-checkbox
node-key="catId"
>
<span class="custom-tree-node" slot-scope="{ node, data }">
<span>{{ node.label }}</span>
<span>
<el-button v-if="node.level<=2" type="text" size="mini" @click="() => append(data)">
Append
</el-button>
<el-button v-if="node.childNodes.length==0" type="text" size="mini" @click="() => remove(node, data)">
Delete
</el-button>
</span>
</span>
</el-tree>
//树形控件追加标签 data是从数据库获取到节点的真正内容/数据
//节点id 父节点cid 所有子节点children等
append(data) {
console.log("append:",data)
},
//树形空间移除标签 node是当前节点数据(是否被选中checked,是否展开enpended,所处层级level等),
//data同append
remove(node, data) {
console.log("remove:",node,data)
},
另外由于限定三级分类,所以只有在1,2级节点时才可以追加分类标签并且只有在没有子节点的时候可以删除当前节点,所以在append和delete按钮的属性中添加v-if控制展示的场景
1.12 在后端CategoryController类中修改逆向生成的delete方法,添加业务层接口方法以及业务层实现类方法实现,期间整合MP实现逻辑删除。
CategoryController类
/**
* 删除-
* @RequestBody获取请求体 必须发送post请求
* SpringMVC 自动将请求体的数据(json形式存储)转化为对应的对象
*/
@RequestMapping("/delete")
//@RequiresPermissions("product:category:delete")
public R delete(@RequestBody Long[] catIds){
System.out.println("待删除菜单id:"+catIds);
//categoryService.removeByIds(Arrays.asList(catIds));
//1.检查当前删除的菜单是否被别的地方引用
categoryService.removeMenuByIds(Arrays.asList(catIds));
return R.ok();
}
CategoryService接口类
public interface CategoryService extends IService<CategoryEntity> {
PageUtils queryPage(Map<String, Object> params);
List<CategoryEntity> listWithTree();
//自定义删除菜单方法
void removeMenuByIds(List<Long> asList);
}
CategoryServiceImpl接口类
@Override
public void removeMenuByIds(List<Long> asList) {
//TODO 调用批量删除之前检查菜单是否被其它地方引用
//批量删除方法-使用了逻辑删除
baseMapper.deleteBatchIds(asList);
}
由于后续引用地方不确定 使用TODO将此处加入待办事项可以在IDEA下方TODO栏查看待办事项。
此外SpringBoot整合MP实现逻辑删除需要如下步骤:
2、逻辑删除
* 1)配置全局的逻辑删除规则(可以省略)
* mybatis-plus:
* global-config:
* db-config:
* logic-delete-field: flag # 全局逻辑删除的实体字段名(since 3.3.0,配置后可以忽略不配置步骤2)
* logic-delete-value: 1 # 逻辑已删除值(默认为 1)
* logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)
* 2)注册逻辑删除组件MP 3.1.1开始不需要此步骤--现在查看官网已经看不到这步要求(可以省略)
* 3)实体类字段加上逻辑删除注解@TableLogic 且@TableLogic(value = "1",delval = "0")可以设置逻辑删除与不删除的值
* 如果与默认全局配置冲突的话
有效配置就是给实体类字段加上逻辑删除注解@TableLogic
/**
* 是否显示 表中定义[0-不显示,1显示]
* 而MP 默认0显示1不显示 所以要该TableLogic的属性值
*/
@TableLogic(value = "1",delval = "0")
private Integer showStatus;
1.13 后端调整完毕后进入前端 修改remove()删除菜单的方法并加入ElmentUI的确认消息弹框和消息提示组件
由于renren-fast-vue对发送ajax请求做了封装 此处将发送httpget和httppost请求的模版写入全局代码片段方便后续快速使用。
"http-get请求": {
"prefix": "httpget",
"body": [
"this.\\$http({",
"url:this.\\$http.adornUrl(''),",
"method:'get',",
"params:this.\\$http.adornParams({})",
"}).then(({data})=>{",
"})"
],
"description": "renren-fast-vue - httpGet请求"
},
"http-post请求": {
"prefix": "httppost",
"body": [
"this.\\$http({",
"url:this.\\$http.adornUrl(''),",
"method:'post',",
"data:this.\\$http.adornData(data,false)",
"}).then(({data})=>{",
"})"
],
"description": "renren-fast-vue - httpPost请求"
},
remove()方法最终编写如下,需要留意的点如下:
①使用反引号包裹的文本内可以使用差值表达式${}获取值 不需要拼串 见确认删除弹框的消息提示部分回显菜单名
②确认删除后需要保持删除前的菜单结构则需要给el-tree控件动态绑定**:default-expanded-keys=“expandedKey”**,动态绑定的属性值expandedKey以数组形式存储,注意要在data()中定义且初值赋空。这样只需要在删除成功后刷新菜单时赋值需要删除菜单的父节点id就可以保持删除前的菜单结构
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tDH6pKqR-1646456434670)(C:\Users\ck\AppData\Roaming\Typora\typora-user-images\1644920004890.png)]
//树形空间移除标签 node是当前节点数据(是否被选中checked,是否展开enpended,所处层级level等),
//data同append
remove(node, data) {
console.log("remove:", node, data);
//将当前节点数据data中的cartId拼成一个数组
var ids = [data.catId];
//使用elementUI messageBox消息弹框 确认是否删除 使用反引号可以在文本内使用差值表达式,不用拼串 `${data.name}`
this.$confirm(
`此操作将永久删除菜单【${data.name}】, 是否继续?`,
"提示",
{
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
}
)
.then(() => {
//确认删除,发送ajax请求
this.$http({
url: this.$http.adornUrl("/product/category/delete"),
method: "post",
data: this.$http.adornData(ids, false),
}).then(({ data }) => {
//使用elementUI的message消息提示组件
this.$message({
message: "成功删除菜单",
type: "success",
});
//刷新菜单
this.getMenus();
//设置父节点id 保持删除前展开结构
this.expandedKey = [node.parent.data.catId];
});
})
.catch(() => {
//取消删除不做任何处理,但必须保留否则会报错
});
1.14 修改增加菜单 后端部分不需要修改 根据catId获取菜单信息的方法将存入自定义响应类R的键名改为"data" 主要是前端部分要添加模态框|对话框 以及事件处理函数等
template部分代码改造 菜单加edit按钮 添加修改新增菜单的对话框
注意点:①对话框弹出后 默认点击模态框外会关闭 需要给close-on-click-modal设为false 但这样设置close-on-click-modal="false"会报错传参错误 需要设置成动态绑定:close-on-click-modal=“false”
<div>
<el-tree
:data="menus"
:props="defaultProps"
:expand-on-click-node="false"
show-checkbox
node-key="catId"
:default-expanded-keys="expandedKey"
>
<span class="custom-tree-node" slot-scope="{ node, data }">
<span>{{ node.label }}</span>
<span>
<el-button
v-if="node.level <= 2"
type="text"
size="mini"
@click="() => append(data)"
>
Append
</el-button>
<el-button type="text" size="mini" @click="() => edit(data)">
Edit
</el-button>
<el-button
v-if="node.childNodes.length == 0"
type="text"
size="mini"
@click="() => remove(node, data)"
>
Delete
</el-button>
</span>
</span>
</el-tree>
<el-dialog :title="dialogType" :visible.sync="dialogVisible" width="30%" :close-on-click-modal="false">
<el-form :model="category">
<el-form-item label="分类名称">
<el-input v-model="category.name" autocomplete="off"></el-input>
</el-form-item>
<el-form-item label="图标">
<el-input v-model="category.icon" autocomplete="off"></el-input>
</el-form-item>
<el-form-item label="计量单位">
<el-input v-model="category.productUnit" autocomplete="off"></el-input>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="dialogVisible = false">取 消</el-button>
<el-button type="primary" @click="submitData">确 定</el-button>
</div>
</el-dialog>
</div>
script部分代码 编写和改造 注意点如下:
①对话框复用标记dialogType
②字符串转数字 字符串*1 this.category.catLevel = data.catLevel * 1 + 1;
③对象解构的应用 - 将有用的对象属性结构出来 如解构category将需要发送的属性解构出来
var { catId, name, icon, productUnit } = this.category;
④在修改菜单回显数据时 重新发请求获取了一下最新的数据 稍微避免了修改过程时其它管理员修改完毕同条信息导致回显数据失真的情况 但是没有完全解决这个问题 可以考虑后续使用乐观锁解决这种情况
⑤修改和新增复用同一对话框会出现先点击修改弹出对话框category赋值后再取消修改 随后点击新增会回显之前修改菜单获取到的信息这种情况 使用要在append(data)时将其它信息设为初始
export default {
data() {
return {
menus: [],
//expandedKey 菜单默认展开的结构状态 传入父节点id
expandedKey: [],
//dialogVisible控制对话框/模态框是否展示 默认不展示
dialogVisible: false,
//修改新增复用对话框的依据 edit|append
dialogType: "",
//对话框内表单绑定的数据对象 其中菜单ID-catId是对话框修改新增复用的依据
category: {
name: "",
parentCid: 0,
catLevel: 0,
showStatus: 1,
sort: 0,
icon: "",
productUnit: "",
catId: null,
},
defaultProps: {
//label:哪个属性是作为标签的值需要展示出来,children:哪个属性需要作为标签的子树
children: "children",
label: "name",
},
};
},
methods: {
//获取分类数据
getMenus() {
this.$http({
url: this.$http.adornUrl("/product/category/list/tree"),
method: "get",
}).then(({ data }) => {
//{data}解构 将响应的对象中data解构出来 data.data就是查询到的数据集合
console.log("成功获取到菜单数据。。", data.data);
this.menus = data.data;
});
},
//追加标签 点击append触发的事件 data是从数据库获取到节点的真正内容/数据
//节点id 父节点cid 所有子节点children等
append(data) {
//console.log("append:", data);
//设定对话框类型
this.dialogType = "append";
//点击添加标签后,对话框展示
this.dialogVisible = true;
//给追加标签绑定的数据对象赋值
this.category.parentCid = data.catId; //父标签id
this.category.catLevel = data.catLevel * 1 + 1; //分类级别 当前点击的节点级别 + 1 乘1是将字符串转换成数字
//将其它属性设为初始 防止出现先点击修改弹出对话框category赋值后再取消修改 随后点击新增会回显之前修改菜单获取到的信息这种情况
this.category.catId = null;
this.category.name = "";
this.category.icon = "";
this.category.productUnit = "";
this.category.sort = 0;
this.category.showStatus = 1;
},
//移除标签 点击remove触发的事件 node是当前节点数据(是否被选中checked,是否展开enpended,所处层级level等),
//data同append
remove(node, data) {
//console.log("remove:", node, data);
//将当前节点数据data中的cartId拼成一个数组
var ids = [data.catId];
//使用elementUI messageBox消息弹框 确认是否删除 使用反引号可以在文本内使用差值表达式,不用拼串 `${data.name}`
this.$confirm(`此操作将永久删除菜单【${data.name}】, 是否继续?`, "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
})
.then(() => {
//确认删除,发送ajax请求
this.$http({
url: this.$http.adornUrl("/product/category/delete"),
method: "post",
data: this.$http.adornData(ids, false),
}).then(({ data }) => {
//使用elementUI的message消息提示组件
this.$message({
message: "成功删除菜单",
type: "success",
});
//刷新菜单
this.getMenus();
//设置父节点id 保持删除前展开结构
this.expandedKey = [node.parent.data.catId];
});
})
.catch(() => {
//取消删除不做任何处理,但必须保留否则会报错
});
},
//修改菜单 点击edit按钮触发的事件
edit(data) {
//console.log("当前要修改的数据", data);
//修改对话框类型
this.dialogType = "edit";
//打开对话框
this.dialogVisible = true;
//回显数据 发送请求重新获取最新数据 防止数据在更新期间被其它管理员更改
//也没有完全避免这个问题 后续可以加乐观锁
this.$http({
url: this.$http.adornUrl(`/product/category/info/${data.catId}`),
method: "get",
}).then(({ data }) => {
console.log("data.data:",data.data);//注意data.data下才是需要的菜单信息
//请求成功 修改数据 层级level等属性不改 后期做拖拽功能直接改结构
this.category.name = data.data.name;
this.category.catId = data.data.catId;
this.category.icon = data.data.icon;
this.category.productUnit = data.data.productUnit;
//为了修改后展示父菜单 所以有回显父菜单id的需求
this.category.parentCid = data.data.parentCid;
});
},
submitData() {
if (this.dialogType == "append") {
//调用添加标签的方法
this.appendCategory();
}
if (this.dialogType == "edit") {
//调用修改标签的方法
this.editCategory();
}
},
//对话框确认按钮点击-修改标签发送请求到后台
editCategory() {
//将前端收集到的category数据发送给后端,由于有些没有修改的内容不需要发给后端
//所以要解构category将需要发送的解构出来
var { catId, name, icon, productUnit } = this.category;
//键值名相同可以省略
//var data = {catId,name,icon,productUnit};//懒得多声明一个变量可以直接将结构的结果放进请求体里
this.$http({
url: this.$http.adornUrl("/product/category/update"),
method: "post",
data: this.$http.adornData({ catId, name, icon, productUnit }, false),
}).then(({ data }) => {
//后台响应成功
//关闭对话框
this.dialogVisible = false;
//提示友好信息
this.$message({
message: "菜单修改成功",
type: "success",
});
//刷新信息
this.getMenus();
//展示原先结构
this.expandedKey = [this.category.parentCid];
});
},
//对话框确认按钮点击-追加标签发送请求到后台
appendCategory() {
//console.log("提交的三级分类数据:", this.category);
//将前端收集到的category数据发送给后端
this.$http({
url: this.$http.adornUrl("/product/category/save"),
method: "post",
data: this.$http.adornData(this.category, false),
}).then(({ data }) => {
//后台响应成功
//关闭对话框
this.dialogVisible = false;
//提示友好信息
this.$message({
message: "菜单保存成功",
type: "success",
});
//刷新信息
this.getMenus();
//展示原先结构
this.expandedKey = [this.category.parentCid];
});
},
},
created() {
this.getMenus();
},
};
1.15 增加节点拖拽功能
调试成功后增加节点拖拽功能即实现通过拖拽节点从而改变父子关系和层级结构的功能。只需要给el-tree添加属性draggable就可以拖拽节点,但会出现超出设置的三层菜单的层级限制,需要额外添加属性**:allow-drap=“allowDrop”,并在method中定义方法allowDrop(draggingNode,dropNode,type),其中draggingNode表示当前节点,dropNode是目标节点,type有三种情况:‘prev’,'inner’和’next’分别表示放置在目标节点前,目标节点中和目标节点后,allowDrop返回的标记决定是否能够放置。另外额外提取了一个countNodeLevel(node)**方法用来递归调用计算当前节点的子节点最大层数。具体方法如下:
这个功能需要注意的是整个实现思想
①允许拖拽后放置的依据:当前被拖动的节点的深度+目标节点层级不能大于设置的菜单最大层级3
当前节点的深度通俗来说就是当前节点和子节点以及子节点的子节点全部加起来总共有几层菜单
当前节点的深度(待拖动节点加上子节点有几层) = 当前节点的子节点的最大层级maxLevel - 当前节点所处层级catlevel + 1
②计算当前节点的子节点最大层级的方法countNodeLevel(node)使用了递归
//判断节点是否可以被拖拽 其中draggingNode表示当前节点,dropNode是目标节点,type有三种情况:
//'prev','inner'和'next'分别表示放置在目标节点前,目标节点中和目标节点后
allowDrop(draggingNode, dropNode, type) {
//判断依据 当前被拖动的节点的深度+目标节点层级不能大于3
//当前节点的深度(待拖动节点加上子节点有几层) = 当前节点的子节点的最大层级maxLevel - 当前节点所处层级catlevel + 1
//console.log("allowDrop", draggingNode, dropNode, type);
//1.计算当前节点的深度 即 当前节点的子节点的最大层级maxLevel - 当前节点所处层级catlevel + 1
//1.1 求出当前节点的子节点最大层级 值更新在this.maxlevel中
// draggingNode.data中是Node节点中从后台数据库获取到的静态信息,这里不使用 因为没有更新层级改变此处数据有可能失真
this.countNodeLevel(draggingNode);
//console.log("当前节点的子节点最大层级",this.maxLevel)
//1.2 计算深度
let deep = this.maxLevel - draggingNode.level + 1;
//console.log("当前拖拽节点深度",deep)
//2 判断是否可以拖动 拖动到目标节点内或者前后两种情况
if (type == "inner") {
//拖动到目标节点内 只需要当前节点深度+目标节点层级<=3即可
let isDrag = deep + dropNode.level <= 3;
console.log(`拖拽类型${type}: 当前节点的子节点最大层级:${this.maxLevel}--当前节点层级:${draggingNode.level}
--当前节点深度:${deep}--目标节点层级:${dropNode.level}--是否允许拖拽:${isDrag}`);
//判断完毕给data中的maxLevel赋初值
this.maxLevel = 0;
//this.updateNodes = [];
return isDrag;
} else {
//拖动到目标节点前或后 只需要判断当前节点深度+目标节点父节点层级<=3即可
let isDrag = deep + dropNode.parent.level <= 3;
console.log(`拖拽类型${type}: 当前节点的子节点最大层级:${this.maxLevel}--当前节点层级:${draggingNode.level}
--当前节点深度:${deep}--目标节点父节点层级:${dropNode.parent.level}--是否允许拖拽:${isDrag}`);
//判断完毕给data中的maxLevels赋初值
this.maxLevel = 0;
//this.updateNodes = [];
return isDrag;
}
},
//计算当前节点的子节点最大层数
countNodeLevel(node) {
//console.log("当前节点信息",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 {
//没有子节点 将maxLevel设置为当前节点层级 为了正确计算当前节点深度
//console.log("无子节点的maxlevel设置",node.level)
this.maxLevel = node.level;
}
},
以上方法实现后,在拖拽菜单后el-tree会自动计算出拖拽后应处于的层级level和子节点childNodes,但从数据库中获取到的静态数据data里的节点信息还没更新,先通过拖拽菜单成功后触发的事件函数**handleDrop(draggingNode, dropNode, dropType, ev)**来处理拖拽后的新节点数据并将拖拽后的新节点数据封装为节点对象数组updateNodes: []传入后端从而更新数据库中的节点数据。
注意**更新思想:**当前拖拽节点最新的父节点id|当前拖拽节点最新的顺序 - 遍历兄弟节点数组|当前拖拽节点最新的层级
//拖拽菜单成功后触发的事件函数
//draggingNode当前正拖拽的节点 dropNode目标节点|参考节点
//dropType拖拽到参考节点的哪个位置 ev事件对象
handleDrop(draggingNode, dropNode, dropType, ev) {
//console.log("tree drop: ", draggingNode, dropNode, dropType);
//1.当前拖拽节点最新的父节点id 根据方式判断
let pCid = 0;
let siblings = null;
if (dropType == "before" || dropType == "after") {
//父id应该是兄弟节点|目标节点的父id
//pCid = dropNode.parent.data.catId;
//这里避免一个小bug 如果移动到第一个一级菜单之前 由于之前一级菜单的父节点没有数据
//所以移动后pCid会变成undefined 这里加个三元判断
pCid =
dropNode.parent.data.catId == undefined
? 0
: dropNode.parent.data.catId;
//当前拖拽节点的兄弟节点就是目标节点的父节点的子节点 - 注意childNodes是拖拽后自动改变后的新值
//不同于data中后台获取到的children静态值
siblings = dropNode.parent.childNodes;
} else {
//inner
//父Id就是目标节点的id
pCid = dropNode.data.catId;
//当前拖拽节点的兄弟节点就是目标节点的子节点
siblings = dropNode.childNodes;
}
//给全局pCid赋值
this.pCid.push(pCid);
//2.当前拖拽节点最新的顺序 - 遍历兄弟节点数组
//3.当前拖拽节点最新的层级
for (let i = 0; i < siblings.length; i++) {
//遍历到当前拖拽节点
if (siblings[i].data.catId == draggingNode.data.catId) {
//将节点信息push到updateNodes中 除了排序改变还要将父id 以及层级(视情况而定)
//判断层级是否发生变化 这里判断使用的siblings[i].level是会随拖拽后自动变化的 - 也就是目标值|正确值
//而draggingNode.data.catLevel是数据库中存的静态数据 如果二者不相等则需要封装
let catLevel = draggingNode.data.catLevel;
if (siblings[i].level != catLevel) {
//当前拖拽节点层级改变
catLevel = siblings[i].level;
//当前节点子节点层级改变 将当前遍历到的拖拽节点传入参数 其childNodes是子节点 抽成一个方法
this.updateChildrenNodeLevel(siblings[i]);
}
this.updateNodes.push({
catId: siblings[i].data.catId,
sort: i,
parentCid: pCid,
catLevel: catLevel,
});
} else {
//遍历到其它节点
this.updateNodes.push({ catId: siblings[i].data.catId, sort: i });
}
}
//打印最新整理好的updateNodes
console.log("updateNodes:", this.updateNodes);
},
//拖拽后层级改变,当前拖拽节点的子节点层级改变
updateChildrenNodeLevel(node) {
//遍历
for (let i = 0; i < node.childNodes.length; i++) {
//let cNode = node.childNodes[i].data;//遍历到当前子节点存储的后端节点数据
//cNode.catId = cNode.catId;//待更新的id
//cNode.catLevel = node.childNodes[i].level//待更新的后端catLevel层级
//console.log("待更新的子节点id",node.childNodes[i].data.catId)
//console.log("待更新的子节点后端catLevel层级",node.childNodes[i].level)
this.updateNodes.push({
catId: node.childNodes[i].data.catId,
catLevel: node.childNodes[i].level,
});
//递归调用
this.updateChildrenNodeLevel(node.childNodes[i]);
}
},
为了多次拖拽后统和数据改动一次传入后端从而减少与数据库交互次数,定义一个保存批量拖拽的按钮点击后向后端发送数据更新请求,另外也定义一个清除节点更新数组的重置按钮
//批量拖拽后向后台提交最新节点信息
batchSave() {
this.$http({
url: this.$http.adornUrl("/product/category/update/sort"),
method: "post",
data: this.$http.adornData(this.updateNodes, false),
}).then(({ data }) => {
//响应成功发送友好信息
this.$message({
message: "菜单结构已修改",
type: "success",
});
//刷新菜单
this.getMenus();
//设置默认展开的菜单s
this.expandedKey = this.pCid;
//置为初值
this.updateNodes = [];
//this.pCid = [];
});
},
//取消批量拖拽
cancelBatchDrag() {
//刷新菜单
this.getMenus();
//设置默认展开的菜单s
this.expandedKey = this.pCid;
//置为初值
this.updateNodes = [];
this.pCid = [];
},
},
也别忘了给后端增加批量更新节点的方法com.atguigu.gulimall.product.controller.CategoryController#updateSort
/**
* 自定义批量修改方法 用于拖拽时的更新需求
* category是前端收集到的待更新节点数组由SpringMVC自动映射为List<CategoryEntity>
*/
@RequestMapping("/update/sort")
//@RequiresPermissions("product:category:update")
public R updateSort(@RequestBody List<CategoryEntity> category){
categoryService.updateBatchById(category);
return R.ok();
}
1.16 批量删除
引入批量删除按钮el-button 点击后将选中的节点id组成的数组发送给后端批量删除
主要注意这里用到el-tree中Tree组件内部定义的方法,使用组件内定义的方法需要先给该组件定义一个引用标识ref属性,此外调用方式是:this.$refs.ref标识.组件内方法
<el-button
type="primary"
size="mini"
round
@click="batchDelete"
>批量删除</el-button
>
//批量删除菜单
//这里用到el-tree中Tree组件内部定义的方法getCheckedMenus()该方法返回所有被选择的节点组成的数组
//使用组件内定义的方法需要先给该组件定义一个引用标识ref属性,此外调用方式是:this.$refs.ref标识.组件内方法
//其中this.$refs是拿到当前所有的组件 而.ref标识就是拿到树形控件
batchDelete(){
//let checkedNodes = this.$refs.treeMenu.getCheckedNodes();
//console.log("所有被选中的节点:",checkedNodes)
//这里使用map将this.$refs.treeMenu.getCheckedNodes()获取到的选中节点对象映射为只有其catId构成的数组
let checkedNodesId = this.$refs.treeMenu.getCheckedNodes().map(node => node.catId);
let checkedNodesName = this.$refs.treeMenu.getCheckedNodes().map(node => node.name);
let checkedNodesNameSlice = [];
if(checkedNodesName.length > 5){
checkedNodesNameSlice = checkedNodesName.slice(0,5)
}
//console.log("截取名",checkedNodesNameSlice)
//console.log("所有被选中的节点catId:",checkedNodesId)
//使用elementUI messageBox消息弹框 确认是否删除 使用反引号可以在文本内使用差值表达式,不用拼串 `${data.name}`
this.$confirm(`是否批量删除以下菜单【${checkedNodesName.length <= 5?checkedNodesName:checkedNodesNameSlice}】${checkedNodesNameSlice.length == 0?"":"等"}?`, "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
})
.then(() => {
//确认删除,发送ajax请求
this.$http({
url: this.$http.adornUrl("/product/category/delete"),
method: "post",
data: this.$http.adornData(checkedNodesId, false),
}).then(({ data }) => {
//使用elementUI的message消息提示组件
this.$message({
message: "菜单批量删除成功",
type: "success",
});
//刷新菜单
this.getMenus();
});
})
.catch(() => {
//取消删除不做任何处理,但必须保留否则会报错
});
},
2.商品模块-品牌服务 - pms_brand表的CRUD
先导-基本的CRUD需要的vue文件renren-generator已经逆向生成好了,只需要先在快速开放平台创建好需要实现的菜单及服务 配置好菜单URL
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0ZEqK51A-1646456434671)(C:\Users\ck\AppData\Roaming\Typora\typora-user-images\1645283243066.png)]
然后在前端项目文件对应的目录下复制粘贴生成好的vue文件即可,如商品模块的品牌服务
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EjxvbHsE-1646456434671)(C:\Users\ck\AppData\Roaming\Typora\typora-user-images\1645283073314.png)]
导入后重启前端项目,打开新添加的菜单发现只有查询功能,没有新增功能,这是renren-fast-vue写好的权限校验,只有管理身份打开才能有新增删除等权限,这里开发阶段先将该权限校验的代码做一下修改,权限校验方法是在src\utils\index.js中导出
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mYTRy09c-1646456434672)(C:\Users\ck\AppData\Roaming\Typora\typora-user-images\1645283540711.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OsyTfAmj-1646456434672)(C:\Users\ck\AppData\Roaming\Typora\typora-user-images\1645283562486.png)]
以上修改完毕后就要按需对逆向生成的代码调整
2.1 修改品牌showStatus的存取类型input->switch并添加@change监听状态改变及时改变数据库数据
tips:
①由于showStatus品牌状态打算使用switch开关组件那么在el-table中就需要参考自定义列模版的使用方法。
在el-table的每一列el-table-column下自定义列模版需要使用template标签且必须定义属性slot-scope=“scope”,在template内就可以使用其它需要组合使用的组件,且在其它组件中可以通过scope.row获取当前行的数据。以下是showStatus列的改造
②后端数据库存放的show_status列用0|1绑定是否打开关闭,而switch默认true和fasle控制开关,所以手动设置el-switch的active-value和inactive-value值与数据库映射。切记此处1和0是数字,那么必须加上冒号:,否则会将1,0当字符串判定
<el-table-column
prop="showStatus"
header-align="center"
align="center"
label="显示状态"
>
<template slot-scope="scope">
<el-switch
v-model="scope.row.showStatus"
@change="updateBrandStatus(scope.row)"
:active-value="1"
:inactive-value="0"
></el-switch>
</template>
</el-table-column>
以及状态改变后触发的事件处理函数-改变后台数据 ①结构行数据rowData ②后台showStatus用0和1存放,这里用三元将true|fasle 转化为1|0
//品牌展示开关被改变后触发事件
updateBrandStatus(rowData) {
//console.log("status改变后最新rowData", rowData);
//将brandId和showStatus解构出来
let {brandId,showStatus} = rowData;
this.$http({
url: this.$http.adornUrl("/product/brand/update"),
method: "post",
//注意showStatus后台是0,1接收 这里使用三元转换数据
data: this.$http.adornData({brandId:brandId,showStatus:showStatus?1:0}, false),
}).then(({ data }) => {
//响应成功
this.$message({
message:"状态改变成功",
type:"success"
})
});
},
2.2 品牌logo的文件上传功能
先导-对象存储服务OSS(object storage service)使用必要性
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IbfH5AIS-1646456434673)(C:\Users\ck\AppData\Roaming\Typora\typora-user-images\1645368614646.png)]
阿里云对象存储服务的使用步骤 这里注意版本问题 新版本springboot有变化
使用aliyun对象存储OSS的步骤
* 1)导入依赖
* <dependency>
* <groupId>com.alibaba.cloud</groupId>
* <artifactId>spring-cloud-starter-alicloud-oss</artifactId>
* <version>2.2.0.RELEASE</version>
* </dependency>
* 2)在yml文件中配置阿里云openAPI的endpoint access-key secret-key
* 3)注入OSSClient 通过OSSClient.putObject传入bucket名 自定义云端文件名含后缀以及本地文件流FileinputStream参数
* 就可以将文件上传给阿里云的bucket列表
创建一个新模块gulimall-third-party用来盛放第三方的一些服务,将阿里云对象存储OSS放入其中
采用服务端签名直传的方式将图片存入阿里云OSS即在服务端通过Java代码完成签名(并且设置上传回调),然后通过表单直传数据到OSS。步骤如下
①pom.xml导入OSS依赖
②编辑application.yml文件
配置注册中心地址 应用名 端口名以及阿里云oss服务三属性access-key sercret-key及oss.endpoint 和自定义的云存储空间名bucket。
spring:
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
alicloud:
access-key: LTAI5tPPeS9KtR539wrrL8WG
secret-key: bFeVRHpecpHt1ASHG5blyrrjL3df20
oss:
endpoint: oss-cn-hangzhou.aliyuncs.com
bucket: gulimall-chenk
application:
name: gulimall-third-party
server:
port: 30000
③创建并编辑bootstrap.properties文件
设置配置中心地址 命名空间 并加载配置中心的配置
spring.cloud.nacos.config.server-addr=127.0.0.1:8848
spring.cloud.nacos.config.namespace=0bcdfaec-accd-44e9-8d4f-fd92b49bcd10
#加载配置中心配置oss.yml oss.endpoint access-key sercret-key
spring.cloud.nacos.config.extension-configs[0].data-id=oss.yml
spring.cloud.nacos.config.extension-configs[0].group=DEFAULT_GROUP
spring.cloud.nacos.config.extension-configs[0].refresh=true
④创建controller包并编写OSScontroller接口方法policy
注意点tips
a.注入使用**@Autowired和@Resource区别**
b.读取配置文件中属性**@Value("${属性名}")**
@RestController
public class OssController {
//自动注入OssClient
//方式一:@Autowired注解要注入OSS接口
//如果注入OSSClient会报错Field ossClient in com.atguigu.gulimall.thirdparty.controller
// .OssController required a bean of type 'com.aliyun.oss.OSSClient' that could not be found.
// @Autowired
// OSS ossClient;
//方式二:@Resource注解 直接注入OSSClient即可 因为使用@Resource注解可以按名称找到
@Resource
OSSClient ossClient;
//从配置文件中获取endpoint bucket等属性 其中bucket是自定义的一个属性
@Value("${spring.cloud.alicloud.oss.endpoint}")
private String endpoint;
@Value("${spring.cloud.alicloud.oss.bucket}")
private String bucket;
@Value("${spring.cloud.alicloud.access-key}")
private String accessId;
// @Value("${spring.cloud.alicloud.secret-key}")
// private String accessKey;
/**
*
* @return Map<String, String> respMap包装为R
* accessid-
* policy-策略
* signature-签名 阿里云验证依据
* dir-上传文件时的目录前缀
* host-上传到的主机地址
* expire-过期时间
* respMap值为{"accessid":"LTAI5tPPeS9KtR539wrrL8WG","policy":"eyJleHBpcmF0aW9uIjoiMjAyMi0wMi0yMVQwMzoxNjozNy4wNjNaIiwi
* Y29uZGl0aW9ucyI6W1siY29udGVudC1sZW5ndGgtcmFuZ2UiLDAsMTA0ODU3NjAwMF0sWyJzdGFydHMtd2l0aCIsIiRrZXkiLCIyMDIyL
* TAyLTIxLyJdXX0=","signature":"+VkovFEcgQxoCkoOhzyBBlB6MG4=","dir":"2022-02-21/",
* "host":"https://gulimall-chenk.oss-cn-hangzhou.aliyuncs.com","expire":"1645413397"}
*/
@RequestMapping("/oss/policy")
public R policy(){
//对象存储服务最终访问路径应为 存储空间名.endpoint/自定义文件包含后缀
// https://gulimall-chenk.oss-cn-hangzhou.aliyuncs.com/testup.jpg
//从配置文件中获取endpoint bucket等属性
//String accessId = "<yourAccessKeyId>"; // 请填写您的AccessKeyId。
//String accessKey = "<yourAccessKeySecret>"; // 请填写您的AccessKeySecret。
//String endpoint = "oss-cn-hangzhou.aliyuncs.com"; // 请填写您的 endpoint。
//String bucket = "bucket-name"; // 请填写您的 bucketname 。
String host = "https://" + bucket + "." + endpoint; // host的格式为 bucketname.endpoint
//上传回调暂时不用注释掉 callbackUrl为上传回调服务器的URL,请将下面的IP和Port配置为您自己的真实信息。
//String callbackUrl = "http://88.88.88.88:8888";
//文件的目录前缀这里设定每天的图片收集到一个文件夹中
String currentDate = new SimpleDateFormat("yyyy-MM-dd").format(new Date());
String dir = currentDate; // 用户上传文件时指定的前缀。拼接与否看前端获取时是否要拼接
Map<String, String> respMap = null;
// 创建OSSClient实例。 这里自动注入省去
//OSS ossClient = new OSSClientBuilder().build(endpoint, accessId, accessKey);
try {
long expireTime = 30;
long expireEndTime = System.currentTimeMillis() + expireTime * 1000;
Date expiration = new Date(expireEndTime);
// PostObject请求最大可支持的文件大小为5 GB,即CONTENT_LENGTH_RANGE为5*1024*1024*1024。
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<String, String>();
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());
} finally {
ossClient.shutdown();
}
return R.ok().put("data",respMap);
}
}
⑤给网关的配置文件添加新的路由地址-此处注意网关版本 不同版本路径重写方式不同
将前端发送的/api/thirdparty/的所有请求负载均衡到gulimall-third-party服务并重写路径将前端项目前缀/api/thirdparty去掉
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BJORMl9k-1646456434673)(C:\Users\ck\AppData\Roaming\Typora\typora-user-images\1645415256045.png)]
⑥联调前端
前端使用upload组件来实现文件上传,将已经封装好的文件上传组件(单文件上传和多文件上传)以及抽取好的获取后端签名方法的policy.js放入src\components下
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jiZUQdad-1646456434674)(C:\Users\ck\AppData\Roaming\Typora\typora-user-images\1645415839753.png)]
修改el-upload中action属性值为Bucket 域名 =“gulimall-chenk.oss-cn-hangzhou.aliyuncs.com”
然后遵循以下步骤正确导入组件
①在<script>中导入组件 @表示src/
import SingleUpload from"@/components/upload/singleUpload";
②在<tempalte>中需要导入组件的地方通过 <组件名(允许驼峰式命名调用)></组件名>导入组件
注意绑定属性否则不回显
<el-form-item label="品牌logo地址" prop="logo">
<!-- <el-input v-model="dataForm.logo" placeholder="品牌logo地址"></el-input> -->
<single-upload v-model="dataForm.logo"></single-upload>
</el-form-item>
③在<script>默认导出中componets属性中注册组件
components: { SingleUpload:SingleUpload },
导入后重启项目,点击新增和上传文件 进入调试模式查看NetWordk发现出现了跨域问题(自己服务器和阿里云服务器)
其次注意后端返回数据的层级-单文件上传时在beforeUpload(file)方法中后端响应的response数据要存在data键且data键对应的值是封装好的协议签名等数据,换言之后端获取到的签名等数据封装为一个respMap后要再封装一次,键名为"data"值为respMap 即return R.ok().put(“data”,respMap);
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gw61iCeS-1646456434675)(C:\Users\ck\AppData\Roaming\Typora\typora-user-images\1645427689632.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZgsziXdA-1646456434675)(C:\Users\ck\AppData\Roaming\Typora\typora-user-images\1645427813244.png)]
创建OSS当前bucket的跨域规则
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CGgiaoJk-1646456434675)(C:\Users\ck\AppData\Roaming\Typora\typora-user-images\1645426726643.png)]
完成后打开新增或修改即可成功上传文件,再将brand.vue组件的品牌logo列插入自定义模版,将logo地址赋给img的src。这样既可以回显图片而非Logo地址
<el-table-column
prop="logo"
header-align="center"
align="center"
label="品牌logo地址"
>
<template slot-scope="scope">
<img :src="scope.row.logo" style="width: 100px; height: 80px">
</template>
</el-table-column>
2.3 自定义校验规则的使用-前端校验
elementUI中Form 组件提供了表单验证的功能,只需要通过 rules
属性传入约定的验证规则,并将 Form-Item 的 prop
属性设置为需校验的字段名即可
此外还提供了自定义校验规则的方法,只需在校验规则中给自定义校验器validator绑定用于校验的函数即可使用自定义规则校验表单。以下是首字母检索字段和排序字段属性的自定义校验规则
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 == null) {
callback(new Error("排序字段必须填写"));
} else if (!Number.isInteger(value)) {
callback(new Error("排序字段必须为大于等于0的整数,越小排序优先级越高"));
}else if (value < 0){
callback(new Error("排序字段必须为大于等于0的整数"));
}else{
callback();
}
},
trigger: "blur",
},
],
2.4 JSR303-后端校验
使用JSR303校验-高版本要手动导入依赖validation-api和springboot整合validation的启动类
* <dependency>
* <groupId>javax.validation</groupId>
* <artifactId>validation-api</artifactId>
* <version>2.0.1.Final</version>
* </dependency>
*
* <dependency>
* <groupId>org.springframework.boot</groupId>
* <artifactId>spring-boot-starter-validation</artifactId>
* <version>2.6.3</version>
* </dependency>
* 1)给Bean的字段前(如BrandEntity)添加校验规则的注解@NotBlank等(详见javax.validation.constraints),
* 并添加自定义message信息,如@NotBlank(message = "品牌名必须提交"),不添加则使用默认提示信息
* 此外如果有一些其它需求可以使用@Pattern并传入regexp值匹配自定义校验规则(正则),如
* @Pattern(regexp = "/^[a-zA-Z]$/",message = "检索首字母必须是一个字母")
* private String firstLetter;
* 2)在Controller层的控制方法中需要校验Bean的方法参数前加校验注解@Valid开启校验,如public R save
* (@Valid @RequestBody BrandEntity brand)
* 3)如不想使用默认的校验结果,则可以在待校验Bean参数后紧跟一个BindingResult result,该变量封装的校验结果
* 如public R save(@Valid @RequestBody BrandEntity brand, BindingResult result)
4) 分组校验-根据应用场景的不同有不同的校验需求 如添加品牌和修改品牌对Bean的字段限定要求不同
* ①首先要创建不同的分组接口来做区分应用场景的依据。这里在common模块创建如下包体
* com.atguigu.common.validator.group并先创建AddGroup组和UpdateGroup组区分添加和修改场景
* ②给Bean实体类的字段上添加注解时赋予groups属性值,如groups={AddGroup.class,UpdateGroup.class}表示注解
* 的校验规则生效场景
* ③使用@Validated({生效场景标记接口类.class})并传入分组接口字节码文件替代@Valid注解开启分组校验
5) 自定义校验注解 略 参考 P69视频
①、编写一个自定义的校验注解
②、编写一个自定义的校验器constraintValidator
③、关联自定义的校验器和自定义的校验注解
*@Documented
*Constraint(validatedBy = { ListValueConstraintValidator.cLass【可以指定多个不同的校验器
@Target({METHOD,FIELD,ANNOTATION_TYPE, CoNSTRUCTOR,PARAMETER,TYPE_uSE })
@Retention(RUNTIME)
public @interface ListVaLue i
以保存品牌的save方法为例,改造后代码如下(参考去掉注释的部分即可)
总结来说就是
①实体类Entity中使用javax.validation.constraints下的注解给字段加校验规则。有分组校验需求传入groups属性值,值为生效分组标记接口字节码文件数组。
②在接收对应实体类的参数前加**@Valid注解开启校验。有分组校验需求使用使用@Validated({生效场景标记接口类.class})**
③如果有处理异常信息的需求即不使用默认异常信息封装格式 在接收对应实体类的参数后紧接BindingResult接收封装好的异常信息,一般都是异常集中处理,这里了解一下即可。
/**
* 保存
*/
@RequestMapping("/save")
//@RequiresPermissions("product:brand:save")
public R save(@Valid @RequestBody BrandEntity brand/*, BindingResult result 统一处理异常这里不单独接收*/){
/* 这里不做异常信息的处理 统一交由com.atguigu.gulimall.product.exception.GulimallExceptionControllerAdvice
if (result.hasErrors()){
//1创建一个收纳错误的map集合
HashMap<String, String> map = new HashMap<>();
//2遍历错误集合提取错误字段和提示信息并存入map中
result.getFieldErrors().forEach((error)->{
String message = error.getDefaultMessage();
String field = error.getField();
map.put(field,message);
});
//3封装进R后返回
return R.error(400,"提交的数据不合法").put("data",map);
}else {
//校验结果不出错
brandService.save(brand);
return R.ok();
}*/
brandService.save(brand);
return R.ok();
}
2.5 自定义注解的基本实现步骤
这里自定义一个校验注解@ListValue{vals = {允许传入的值数组}}
①编写一个自定义校验注解
/**
* 自定义校验注解-只允许输入注解传递的值
* 必须填写三个属性
* message() 出错后错误信息寻找
* groups() 支持分组校验
* payload() 自定义一些负载信息
* 且要标记如下注解
* @Constraint指定校验器 这里需要创建一个ListValConstraintValidator
* @Target注解可以标识的位置
* @Retention校验注解可以在运行时获取
*
*/
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = { })
public @interface ListValue {
//默认错误信息默认为 ValidationMessages.properties 中的 com.atguigu.common.validator.ListValue.message值
//在resoureces下创建一个ValidationMessages.properties并给com.atguigu.common.validator.ListValue.message赋值
String message() default "{com.atguigu.common.validator.ListValue.message}";
Class<?>[] groups() default { };
Class<? extends Payload>[] payload() default { };
//指定传入值
int[] vals() default {};
}
②指定报错信息或创建一个ValidationMessages.properties文件并定义报错信息
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Rw3vrVc2-1646456434676)(C:\Users\ck\AppData\Roaming\Typora\typora-user-images\1645596687988.png)]
③编写一个自定义校验器ConstraintValidator
/**
* 自定义注解ListVal的校验器
* 实现ConstraintValidator<A,T>
* A传入校验的注解 T传入可以标记的类型
* 并实现两个方法
*/
public class ListValConstraintValidator implements ConstraintValidator<ListValue,Integer> {
//自定一个Set集合
private Set<Integer> set = new HashSet<>();
//初始化方法
@Override
public void initialize(ListValue constraintAnnotation) {
//获取详细信息 从ListValue定义的vals()方法获取指定值
int[] vals = constraintAnnotation.vals();
//遍历vals 并添加到set中
for (int val : vals) {
set.add(val);
}
}
//判断是否校验成功
//其中value是注解获取到的需要校验的值
@Override
public boolean isValid(Integer value, ConstraintValidatorContext context) {
//判断依据 如果set中不包含带校验值value返回false否则true
return set.contains(value);
}
}
④关联自定义的校验器和自定义的校验注解
在自定义注解类中指定注解@Constraint的validatedBy值
//指定校验器 与自定义校验器产生关联
@Constraint(validatedBy = { ListValConstraintValidator.class })
2.6 异常集中处理和自定义错误和错误信息枚举类
随着业务深入给前端返回的状态码需要有一定的规划由此在common模块定义错误和错误信息枚举类com.atguigu.common.exception.BizCodeEnume。后续再做补充
package com.atguigu.common.exception;
/**
* 错误码和错误信息定义类
* 1.错误码定义规则为5位数字
* 2.前两位表示业务场景,后三位表示错误原因|异常类型。如10001其中
* 10表示通用,001表示参数格式校验异常
* 3.维护错误码需要错误信息描述,将其定义为枚举类型
* 4.错误码列表
* 10:通用
* 001:参数格式校验
* 11:商品服务
* 12:订单
* 13:购物车
* 14:物流
*/
public enum BizCodeEnume {
UNKNOW_EXCEPTION(10000,"系统未知异常"),
VALID_EXCEPTION(10001,"参数格式校验失败");
private int code;
private String msg;
BizCodeEnume(int code,String msg){
this.code = code;
this.msg = msg;
}
public int getCode() {
return code;
}
public String getMsg() {
return msg;
}
}
随后再在com.atguigu.gulimall.product.exception.GulimallExceptionControllerAdvice定义异常处理类,集中处理异常
注意点tips:
①标准异常处理类要使用**@ControllerAdvice并配置基础扫描包,另外由于都返回JSON格式数据要用到@ResponseBody**,所以可以使用复合注解@RestControllerAdvice(basePackages = “”)
②使用日志打印异常要用注解**@Slf4j**,并在方法体中使用log.error(“数据校验出现异常{},异常类型{}”,e.getMessage(),e.getClass()); 可以通过这种方式精确经常类
③执行异常处理方法的依据是通过匹配**@ExceptionHandler(value = 异常类.class)**注解的value值
如果有精确匹配到的就优先执行,如果没有精确匹配到就执行能匹配包含指定异常类的处理方法。
/**
* 集中处理所有异常
*/
@Slf4j
//@ResponseBody
//@ControllerAdvice(basePackages = "com.atguigu.gulimall.product.controller")
//复合注解
@RestControllerAdvice(basePackages = "com.atguigu.gulimall.product.controller")
public class GulimallExceptionControllerAdvice {
//数据校验异常
@ExceptionHandler(value = MethodArgumentNotValidException.class)
public R handleValidException(MethodArgumentNotValidException e){
log.error("数据校验出现异常{},异常类型{}",e.getMessage(),e.getClass());
//获取数据校验的错误结果集
BindingResult result = e.getBindingResult();
//创建自定义错误集合
HashMap<String, String> errorMap = new HashMap<>();
//遍历错误结果集,取出错误信息和错误字段放入自定义错误集合
result.getFieldErrors().forEach((error)->{
errorMap.put(error.getField(),error.getDefaultMessage());
});
return R.error(BizCodeEnume.VALID_EXCEPTION.getCode(),BizCodeEnume.VALID_EXCEPTION.getMsg()).put("data",errorMap);
}
//其它任意异常
@ExceptionHandler(value = Throwable.class)
public R handleException(Throwable throwable){
return R.error(BizCodeEnume.UNKNOW_EXCEPTION.getCode(),BizCodeEnume.UNKNOW_EXCEPTION.getMsg());
}
}
2.6 品牌表pms_brand的查询业务补充-模糊查询
前端
- 表头上添加模糊查询的el-input框绑定一个模糊查询v-model="dataForm.key"值,添加一个查询按钮摁下后发送get请求到后端,将绑定的模糊查询key值也携带进请求参数params中。
后端
- 改造brandServiceImpl中的queryPage方法,增加模糊查询条件
//改造queryPage方法
@Override
public PageUtils queryPage(Map<String, Object> params) {
//1.从请求参数中获模糊查询的key
String key = (String) params.get("key");
//2.创建检索条
QueryWrapper<BrandEntity> wrapper = new QueryWrapper<>();
//3.判断key是否有值
if (!StringUtils.isEmpty(key)){
//有值 拼接模糊检索条件
wrapper.eq("brand_id",key).or().like("name",key);
}
//4.构造分页信息
IPage<BrandEntity> page = this.page(
new Query<BrandEntity>().getPage(params),
wrapper
);
//5.返回包装后的分页信息
return new PageUtils(page);
}
2.7 品牌关联分类表pms_category_brand_relation的CRUD
2.7.1 查询
前端
- 在brand.vue组件中添加操作el-button关联分类按钮,并添加按下按钮后弹出的关联表的模态框组件。在模态框中添加所需组件等 略
后端
- 在CategoryBrandRelationController中添加控制器方法cateloglist获取品牌关联的所有分类的信息,
并在业务层实现
CategoryBrandRelationController
/**
* 获取品牌关联的所有分类的信息
*/
//@RequestMapping(value = "/catelog/list",method = RequestMethod.GET)
@GetMapping("/catelog/list")
//@RequiresPermissions("product:categorybrandrelation:list")
public R cateloglist(@RequestParam("brandId") Long brandId){
//查询当前表pms_category_brand_relation的当前品牌brandId的所以信息
List<CategoryBrandRelationEntity> data = categoryBrandRelationService.getRelationCatlog(brandId);
return R.ok().put("data", data);
}
CategoryBrandRelationServiceImpl
//获取当前品牌关联的分类信息
@Override
public List<CategoryBrandRelationEntity> getRelationCatlog(Long brandId) {
return this.list(new QueryWrapper<CategoryBrandRelationEntity>().eq("brand_id", brandId));
}
2.7.2 新增关联分类
前端
- 点击新增后向后端发送请求 请求参数是brandId和catelogId 不包含品牌名和分类名
后端
-
改造逆向生成的save()方法。前端只传递了品牌ID和分类ID所以不能使用自动生成的方法因为不会保存品牌名和分类名。
注意pms_category_brand_relation表之所以还冗余设计了品牌名和分类名字段是为了降低联表查询的次数,减少对数据库性能的损耗
2.7.3 查询关联分类获取到的品牌名和分类名失真问题
在新增关联分类时查询到了对应的品牌名和分类名并存入了关联表中,所以获取关联信息时都是查的之前保存的信息,假如后续品牌名和分类名|标签名有改动那么这个信息就会失真。
所以为了保证这些冗余信息一致性,需要改造之前的品牌和标签|分类的修改业务。
品牌
通过调用renren-fast封装好的业务层方法和数据库交互
BrandController
/**
* 修改
* 改造 由于其它地方引用了品牌名 所以不止要改此处的品牌名
* 也要改造其它地方引用的品牌名
*/
@RequestMapping("/update")
//@RequiresPermissions("product:brand:update")
public R update(@Validated(UpdateGroup.class) @RequestBody BrandEntity brand){
//brandService.updateById(brand);
brandService.updateDetail(brand);
return R.ok();
}
BrandServiceImpl 接口略 标记为事务方法
//修改品牌的细节 也要改引用品牌名的地方
//标记为事务方法 要么都修改成功要么都不改 使用前提放入容器的MP配置类用
//@EnableTransactionManagement//开启事务
@Transactional
@Override
public void updateDetail(BrandEntity brand) {
//保证冗余字段的一致性
//1.修改自身
this.updateById(brand);
//2.查看品牌名是否为空 不为空则有改动引用的需求
if (!StringUtils.isEmpty(brand.getName())){
//3.同步更新其它关联表的引用
//3.1 品牌标签关联表引用 注入CategoryBrandRelationService
//定义一个根据品牌ID和品牌名修改品牌信息的方法
categoryBrandRelationService.updateBrand(brand.getBrandId(),brand.getName());
//TODO 更新其它引用brand的表数据
}
}
CategoryBrandRelationService 接口略
//根据品牌ID和品牌名更新数据
//用于品牌名更改后更新表中之前保存的品牌名
@Override
public void updateBrand(Long brandId, String name) {
//构造待更新的Bean|Entity
CategoryBrandRelationEntity entity = new CategoryBrandRelationEntity();
entity.setBrandName(name);
entity.setBrandId(brandId);
//条件更新 所有品牌id是brandId的CategoryBrandRelationEntity都更新为entity
this.update(entity,new UpdateWrapper<CategoryBrandRelationEntity>().eq("brand_id",brandId));
}
标签|分类
通过Dao自定义更新方法及对应mapper文件SQL语句和数据库交互数据
CategoryController#update
/**
* 修改
* 改造 自定义级联更新方法 保证冗余字段一致性
*/
@RequestMapping("/update")
//@RequiresPermissions("product:category:update")
public R update(@RequestBody CategoryEntity category){
//categoryService.updateById(category);
categoryService.updateCascader(category);
return R.ok();
}
CategoryServiceImpl#updateCascader 服务层接口略 标记为事务方法
/**
* 自定义级联更新 确保冗余字段一致性即其它引用了标签名的地方更新同步
* @param category
*/
@Transactional
@Override
public void updateCascader(CategoryEntity category) {
this.updateById(category);
//品牌关联标签表服务接口引用 注入
categoryBrandRelationService.updateCategory(category.getCatId(),category.getName());
}
CategoryBrandRelationDao
@Mapper
public interface CategoryBrandRelationDao extends BaseMapper<CategoryBrandRelationEntity> {
//自定义根据标签Id和标签名更新的方法 写mapper语句
//推荐给参数取名 mapper文件中获取方便 #{起的名}
void updateCategory(@Param("catId") Long catId,@Param("name") String name);
}
mapper/product/CategoryBrandRelationDao.xml
<!--自定义统一修改标签名和标签ID 条件是标签ID取等-->
<update id="updateCategory">
UPDATE `pms_category_brand_relation` SET `catelog_name` = #{name} WHERE `catelog_id` = #{catId}
</update>
3.商品模块-其它服务
先导-几个基础概念
①三级分类
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-h3onFEAu-1646456434677)(C:\Users\ck\AppData\Roaming\Typora\typora-user-images\1645516857184.png)]
一级分类查出二级分类数据,二级分类中查询出三级分类数据
对应数据表pms-category
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0rgFctXN-1646456434677)(C:\Users\ck\AppData\Roaming\Typora\typora-user-images\1645516960236.png)]
②SPU和SKU
SPU:Standard Product Unit (标准化产品单元)
SKU: Stock KeepingUnit (库存量单位)
举例来说:
IPhoneX 是 SPU,MI8 是 SPU
IPhoneX 64G 黑曜石 是 SKU
MIX8 + 64G 是 SKU
③基本属性(规格参数)与销售属性
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YxVSrb8K-1646456434677)(C:\Users\ck\AppData\Roaming\Typora\typora-user-images\1645517112292.png)]
【属性分组-规格参数-销售属性-三级分类】关联关系
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-A8X4ql9t-1646456434678)(C:\Users\ck\AppData\Roaming\Typora\typora-user-images\1645518457098.png)]
[SPU-SKU-属性表]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5KplUSQl-1646456434678)(C:\Users\ck\AppData\Roaming\Typora\typora-user-images\1645518493569.png)]
此部分开始前端直接引入设计好的vue文件,并在SQLyog下的gulimall_admin库运行sys_menu.sql创建好所需的菜单和目录。
⑤Object划分
- PO (persistant object) 持久化对象
po 就是对应数据库中某一个表的一条记录,多个记录可以用 PO 的集合,PO 中应该不包含任何对数据库到操作
- DO ( Domain Object) 领域对象
就是从现实世界抽象出来的有形或无形的业务实体
- TO (Transfer Object) 数据传输对象
不同的应用程序之间传输的对象
- DTO (Data Transfer Object) 数据传输对象
这个概念来源于 J2EE 的设计模式,原来的目的是为了 EJB的分布式应用提供粗粒度的数据实体,以减少分布式调用的次数,从而提高分数调用的性能和降低网络负载,但在这里,泛指用于展示层与服务层之间的数据传输对象
- VO(value object) 值对象
通常用于业务层之间的数据传递,和 PO 一样也是仅仅包含数据而已,但应是抽象出的业务对象,可以和表对应,也可以不,这根据业务的需要,用 new 关键字创建,由 GC 回收
view Object 视图对象
接受页面传递来的数据,封装对象,封装页面需要用的数据
- BO(business object) 业务对象
从业务模型的角度看,见 UML 原件领域模型中的领域对象,封装业务逻辑的, java 对象,通过调用 DAO 方法,结合 PO VO,进行业务操作,business object 业务对象,主要作用是把业务逻辑封装成一个对象,这个对象包括一个或多个对象,比如一个简历,有教育经历,工作经历,社会关系等等,我们可以把教育经历对应一个 PO 、工作经验对应一个 PO、 社会关系对应一个 PO, 建立一个对应简历的的 BO 对象处理简历,每 个 BO 包含这些 PO ,这样处理业务逻辑时,我们就可以针对 BO 去处理
- POJO ( plain ordinary java object) 简单无规则 java 对象
传统意义的 java 对象,就是说一些 Object/Relation Mapping 工具中,能够做到维护数据库表记录的 persisent object 完全是一个符合 Java Bean 规范的 纯 java 对象,没有增加别的属性和方法,我们的理解就是最基本的 Java bean 只有属性字段 setter 和 getter 方法
POJO 时是 DO/DTO/BO/VO 的统称
- DAO(data access object) 数据访问对象
是一个 sun 的一个标准 j2ee 设计模式,这个模式有个接口就是 DAO ,他负持久层的操作,为业务层提供接口,此对象用于访问数据库,通常和 PO 结合使用,DAO 中包含了各种数据库的操作方法,通过它的方法,结合 PO 对数据库进行相关操作,夹在业务逻辑与数据库资源中间,配合VO 提供数据库的 CRUD 功能
3.1 属性分组表pms_attr_group的CRUD
3.1.1 属性分组表的查询(前端:组件的抽离与使用 父子组件传递信息 后端:改造根据id获取属性分组信息集合的方法)
前端
-
将三级分类组件category.vue去掉不需要的功能只保留三级分类组件的基本查询功能后抽成一个公共模块common下的vue文件
-
在product下创建attgroup.vue文件。
- 通过el-row和el-col组件将布局页面分为两部分,三级分类树形结构占6而增伤改查功能占18(共24份),
el-row中可以配置
gutter
属性(如:gutter=20)来指定每一栏之间的间隔,默认间隔为 0- 在左边页面引入category.vue组件(3步:script导入-默认导出里componets注册-引用位置通过组件名小写作为标签)
- 父子组件传递数据
过程:子组件给父组件传递数据,事件机制即子组件给父组件发送一个事件,并携带上数据。// this.$emit(“事件名”,携带的数据…)。而父组件在引用标签中定义**@事件名=“事件处理函数”**,来获取到子组件接收子组件传递的数据。
具体来说查阅树形组件文档发现Events事件node-click事件(节点被点击后触发事件)比较符合我们的业务需求,在子组件category.vue的树形控件定义**@node-click=“nodeclick”**,并在方法中将事件以及数据发送给父组件。
//子组件category节点被点击后的事件处理函数 //data-数据库中真正封装的节点信息 //node-当前节点数据 elementUI树形控件封装好的 //component-整个树形组件 //通过this.$emit("tree-node-click", data, node, component);给父组件传递事件并携带数据 nodeclick(data, node, component) { //console.log("子组件category的节点被点击", data, node, component); //向父组件发送事件; this.$emit("tree-node-click", data, node, component); }
在父组件引用子组件时配置子组件传递的事件以及对应的事件处理函数
<category @tree-node-click="treenodeclick"></category>
//感知树节点被点击 treenodeclick(data, node, component) { //console.log("感知到子组件的树节点被点击,返回的数据是:",data,node,component); //三级节点被点击后修改catId if (node.level == 3) { this.catId = data.catId; this.getDataList(); //重新查询 } },
后端
- 在AttrGroupController下修改逆向生成的获取属性分组数据集的方法list()
/**
* 列表
* 修改原方法 加入路径参数catelogId 三级菜单id
* 并新增attrGroupService.queryPage(params,catelogId)方法
*/
@RequestMapping("/list/{catelogId}")
//@RequiresPermissions("product:attrgroup:list")
public R list(@RequestParam Map<String, Object> params,@PathVariable("catelogId") Long catelogId){
// 注释掉原来的方法 增加一个参数
// PageUtils page = attrGroupService.queryPage(params);
PageUtils page = attrGroupService.queryPage(params,catelogId);
return R.ok().put("page", page);
}
- 在AttrGroupService接口定义queryPage(params,catelogId)并在AttrGroupServiceImpl实现
注意点tips:
①renren-fast这里封装了分页和条件查询,参考其它方法模仿改造
②由于有模糊查询的需求所以要将key从请求参数params中获取到。此外拼接查询条件时注意如果拼接的查询条件有扩号,如select * from pms_attr_group where catelog_id = ? and (attr_group_id = key or attr_group_name like ‘%key%’)。那么需要使用函数式编程将括号内的条件拼接成一个对象后返回。
@Override
public PageUtils queryPage(Map<String, Object> params, Long catelogId) {
//①先从params获取到检索关键字key 看pms_attr_group表中各字段是否精确匹配或模糊匹配
String key = (String) params.get("key");
//②创建查询条件 泛型中实体类对应表
QueryWrapper<AttrGroupEntity> wrapper = new QueryWrapper<AttrGroupEntity>();
//③如果有模糊查询 将模糊查询拼接进条件中
if (!StringUtils.isEmpty(key)){
//key不为空 拼接查询条件 由于and拼接的是一个括号 所以要使用函数式编程返回一个对象
wrapper.and((obj)->{
obj.eq("attr_group_id",key).or().like("attr_group_name",key);
});
}
//④视情况生成分页信息并返回
//catelogId == 0则查询所有
if (catelogId == 0){
//this.page(IPage分页信息,QueryWrapper查询条件)
IPage<AttrGroupEntity> page = this.page(
new Query<AttrGroupEntity>().getPage(params),
wrapper
);
return new PageUtils(page);
}else {
//最终要发送的查询语句应为
//select * from pms_attr_group where catelog_id = ? and (attr_group_id = key or attr_group_name like '%key%')
//拼接
wrapper.eq("catelog_id",catelogId);
//③查询分页数据并返回
IPage<AttrGroupEntity> page = this.page(
new Query<AttrGroupEntity>().getPage(params),
wrapper
);
return new PageUtils(page);
}
}
postman测试结果
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-l9qIJsWY-1646456434679)(C:\Users\ck\AppData\Roaming\Typora\typora-user-images\1645539410611.png)]
3.1.2 属性分组表的增改(前端:级联组件使用 后端:修改CategoryEntity中自定义属性Children的返回情况免除Children空返回后级联展示异常)
前端
-
在attr-add-or-update.vue组件的所选分组列使用级联选择器Cascader组件。参考文档使用该组件主要需要配置3个属性v-model=“paths” :options="categorys"和 :props=“setting”。v-model选中项绑定值(通常是一个数组,这里应该是级联的父子catId数组),options绑定可选项数据源,键名可通过
Props
属性配置,props配置选项绑定一个json且要给value(指定选项的值为级联组件动态绑定的值这里用的catId),label(展示名绑定的属性)和children(子选项关联的值)绑定值 -
后端新增节点路径后修改v-model的绑定值后新增和修改正常回显。之后为了防止点修改后路径数组存入数据
但取消修改导致路径数组没有清空从而引起新增的错误回显,查看dialog的回调函数监听关闭事件**@closed**="dialogClose"从而每次关闭都清空完整路径this.catelogPath = [];
-
级联选择器el-cascader添加filterable则可以通过输入关键字快速查找。placeholder="试试搜索:手机"属性可以设置默认文字提示
后端
- 修改CategoryEntity中自定义属性Children的返回情况免除Children空返回后级联展示异常。使用的**@JsonInclude注解**
//如果children集合为空则不返回这个属性
@JsonInclude(JsonInclude.Include.NON_EMPTY)
//自定义的属性,存放子分类,表中查询不到加注解@TableField(exist = false)
@TableField(exist = false)
private List<CategoryEntity> children;
- 通过翻阅文档发现级联组件使用中v-model需要一个节点的父子catId数组即完整catId路径才能正常绑定数据以及修改时的回显,所以需要给后端的AttrGroupEntity定义一个新的自定义属性catelogPath用来存放完整路径并改造控制层方法,并给业务层新增获取完整路径的方法
AttrGroupEntity
/**
* 自定义属性catelogPath
* 用于保存完整路径 用于级联v-model绑定
*/
@TableField(exist = false)
private Long[] catelogPath;
AttrGroupController
/**
* 信息
* 改造-将自定义属性catelogPath
* 通过注入CategoryService的findCatelogPath获取到完整路径
*/
@RequestMapping("/info/{attrGroupId}")
//@RequiresPermissions("product:attrgroup:info")
public R info(@PathVariable("attrGroupId") Long attrGroupId){
AttrGroupEntity attrGroup = attrGroupService.getById(attrGroupId);
//获取当前标签catelogId
Long catelogId = attrGroup.getCatelogId();
//通过CategoryService的findCatelogPath获取到完整路径,这里需要自己完成
Long[] catelogPath = categoryService.findCatelogPath(catelogId);
//将完整路径放进去
attrGroup.setCatelogPath(catelogPath);
return R.ok().put("attrGroup", attrGroup);
}
CategoryServiceImpl 接口中略
/**
* 自定义根据当前catId获取该ID的完整展开路径,即[父,子,孙]用于基本属性功能的级联组件使用
* @return
*/
@Override
public Long[] findCatelogPath(Long catelogId) {
ArrayList<Long> paths = new ArrayList<>();
//总的来说就是根据当前catId查询完整信息查看是否有父节点
//如果有继续查询 直到没有父节点 这里将其抽成一个递归方法实现
//也可以使用迭代
List<Long> parentPath = findParentPath(catelogId,paths);
//逆序
Collections.reverse(parentPath);
//转换成数组
return (Long[]) parentPath.toArray(new Long[parentPath.size()]);
}
private List<Long> findParentPath(Long catelogId, ArrayList<Long> paths) {
//1.收集当前节点
paths.add(catelogId);
//2.查询当前catId详细信息
CategoryEntity category = this.getById(catelogId);
//3.查看是否有父节点 有则递归
if (category.getParentCid() != 0){
findParentPath(category.getParentCid(),paths);
}
return paths;
}
- 由于使用了MP的分页插件,所以需要在product模块创建config包以及MybatisConfig给容器中放入分页PaginationInterceptor 注意版本 高版本的分页Bean不一样
/**
* MybatisPlus的配置类--新版本可能要更换返回的分页Bean
* MybatisPlusInterceptor
*/
@Configuration
@EnableTransactionManagement//开启事务
@MapperScan("com.atguigu.gulimall.product.dao")//扫描Mapper接口
public class MybatisConfig {
//引入分页插件
@Bean
public PaginationInterceptor paginationInterceptor(){
PaginationInterceptor paginationInterceptor = new PaginationInterceptor();
//设置请求的页码大于最后一页后的操作 true则返回首页 false则继续请求 默认false
paginationInterceptor.setOverflow(true);
//设置单页最大限制数量 默认500条 -1不限制
paginationInterceptor.setLimit(1000);
return paginationInterceptor;
}
}
属性分组表关联属性的功能后续添加
3.2 属性表pms_attr的增删改查
3.2.1 基本属性的增删改查
1 新增属性
先导知识
由于展示需求,要给新增属性绑定一个表中没有的属性GroupId分组Id,按之前的做法给AttrEntity添加一个属性并用@TableField(exist = false)标注为自定义展示属性,这样是不合规范的。所以才有创建vo包的需求。
VO(value object) 值对象 - 用view Object 视图对象理解更形象
通常用于业务层之间的数据传递,和 PO 一样也是仅仅包含数据而已,但应是抽象出的业务对象,可以和表对应,也可以不,这根据业务的需要,用 new 关键字创建,由 GC 回收
view Object 视图对象
一方面接受页面传递来的数据,封装成对象;另一方面业务处理完的对象,封装成页面需要用的数据
后端
- 创建vo包以及AttrVO 复制AttrEntity属性到这里
/**
* 属性Vo对象
* 一方面接受页面传递来的数据,封装成对象;
* 另一方面业务处理完的对象,封装成页面需要用的数据
* 用到的对象就是Vo对象
*
*/
@Data
public class AttrVO implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 属性id
*/
//@TableId//不需要标注数据库的注解
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;
}
- 改造com.atguigu.gulimall.product.controller.AttrController#save方法
/**
* 保存
* 改造 将请求体中的页面封装为一个AttrVO对象并保存
*/
@RequestMapping("/save")
//@RequiresPermissions("product:attr:save")
public R save(@RequestBody AttrVO attr){
//attrService.save(attr);
attrService.saveAttr(attr);
return R.ok();
}
- 新增com.atguigu.gulimall.product.service.impl.AttrServiceImpl#saveAttr方法 接口略
//自定义保存属性
@Transactional
@Override
public void saveAttr(AttrVO attr) {
//1.创建po对象保存到数据库中
AttrEntity attrEntity = new AttrEntity();
//使用spring提供的BeanUtils.copyProperties(源,目标)完成vo属性值拷贝到po
//使用前提-Vo中属性名和Po中相同
BeanUtils.copyProperties(attr,attrEntity);
this.save(attrEntity);
//2.保存关联关系 - 注入AttrAttrgroupRelationDao
AttrAttrgroupRelationEntity relationEntity = new AttrAttrgroupRelationEntity();
//主要接收属性Id和属性分组ID
relationEntity.setAttrGroupId(attr.getAttrGroupId());
relationEntity.setAttrId(attrEntity.getAttrId());
attrAttrgroupRelationDao.insert(relationEntity);
}
重启项目新增两个属性后发现不仅属性表中存入了前端返回的数据
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-giKTvK4b-1646456434680)(C:\Users\ck\AppData\Roaming\Typora\typora-user-images\1645629593559.png)]
并且在属性和属性分组关联表中也存入了关联关系
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PFlhfR8C-1646456434680)(C:\Users\ck\AppData\Roaming\Typora\typora-user-images\1645629652958.png)]
2 查询属性
后端
-
创建新的查询方法匹配前端发送的请求 com.atguigu.gulimall.product.controller.AttrController#baseList
/** * 创建一个查询列表方法 * 携带路径参数catelogId三级分类标签Id * 请求参数(分页参数&模糊查询key) * 查询全部属性和当前节点属性 */ @GetMapping("/base/list/{catelogId}") //@RequiresPermissions("product:attr:list") public R baseList(@RequestParam Map<String, Object> params,@PathVariable("catelogId") Long catelogId){ //自定义方法 PageUtils page = attrService.queryBaseListPage(params,catelogId); return R.ok().put("page", page); }
-
创建com.atguigu.gulimall.product.service.impl.AttrServiceImpl#queryBaseListPage方法 接口方法略
@Override public PageUtils queryBaseListPage(Map<String, Object> params, Long catelogId) { //①创建询条件 QueryWrapper<AttrEntity> queryWrapper = new QueryWrapper<>(); //②分类讨论拼接条件 catelogId==0?params.key isEmpty? if (catelogId != 0){ //查询指定三级分类标签下的属性 queryWrapper.eq("catelog_id",catelogId); } String key = (String) params.get("key"); if (!StringUtils.isEmpty(key)){ //attr_id attr_name queryWrapper.and((wrapper)->{ wrapper.eq("attr_id",key).or().like("attr_name",key); }); } //③创建分页信息 传入包含分页信息的请求参数集合以及查询条件 IPage<AttrEntity> page = this.page( new Query<AttrEntity>().getPage(params), queryWrapper ); //④返回查询到的包装后的分页数据 return new PageUtils(page); }
-
按如上查询方法所示,只能查出来pms_attr属性表定义的信息,其中只有所属分类ID,没有所属分组ID,而前端需要的所属分组名group_name和所属分类名catelog_name都没放进放进分页信息中。
这种情况由于不推荐联表查询(属性表数据量极大情况下,即使分组数较少联表查询下对数据库资源都是一种极大的损耗),也没设计冗余字段,只能改造业务层查询方法com.atguigu.gulimall.product.service.impl.AttrServiceImpl#queryBaseListPage
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2Uc1Vv4l-1646456434681)(C:\Users\ck\AppData\Roaming\Typora\typora-user-images\1645696757997.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Wv5bsAYu-1646456434681)(C:\Users\ck\AppData\Roaming\Typora\typora-user-images\1645696834475.png)]
在改造查询方法前创建AttrRespVO类,用来专门收集前端需要的属性表信息。
/** * 属性Vo对象 * 一方面接受页面传递来的数据,封装成对象; * 另一方面业务处理完的对象,封装成页面需要用的数据 * 用到的对象就是Vo对象 * * 这里是将业务处理完的数据封装成页面需要的数据响应回去 * */ @Data public class AttrRespVO extends AttrVO { /** * 额外需要响应回去的两个成员变量 * catelogName 所属分类名 如“手机/数码/手机” * groupName 所属分组名 如“主体” */ private String catelogName; private String groupName; }
queryBaseListPage
@Override
public PageUtils queryBaseListPage(Map<String, Object> params, Long catelogId) {
//①创建询条件
QueryWrapper<AttrEntity> queryWrapper = new QueryWrapper<>();
//②分类讨论拼接条件 catelogId==0?params.key isEmpty?
if (catelogId != 0){
//查询指定三级分类标签下的属性
queryWrapper.eq("catelog_id",catelogId);
}
String key = (String) params.get("key");
if (!key.isEmpty()){
//attr_id attr_name
queryWrapper.and((wrapper)->{
wrapper.eq("attr_id",key).or().like("attr_name",key);
});
}
//③创建分页信息 传入包含分页信息的请求参数集合以及查询条件
IPage<AttrEntity> page = this.page(
new Query<AttrEntity>().getPage(params),
queryWrapper
);
//④从page中得到数据库中查询到的记录 进行二次封装将分组名和分类名封装到attrRespVO中
List<AttrEntity> records = page.getRecords();
//流式编程
List<AttrRespVO> respVOS = records.stream().map((attrEntity -> {
//创建attrRespVO对象并拷贝信息
AttrRespVO attrRespVO = new AttrRespVO();
BeanUtils.copyProperties(attrEntity, attrRespVO);
//查询并设置所属分组名
//先根据关联表查出分组Id 再根据分组Dao查到分组名并设置进去
AttrAttrgroupRelationEntity relationEntity = attrAttrgroupRelationDao.selectOne(new QueryWrapper<AttrAttrgroupRelationEntity>().eq("attr_id", attrEntity.getAttrId()));
//非空判断 由于属性在新增时分组可以不选所以有可能为空
if (relationEntity != null && relationEntity.getAttrGroupId()!=null) {
//获取分组ID并查设分组名
Long attrGroupId = relationEntity.getAttrGroupId();
AttrGroupEntity attrGroupEntity = attrGroupDao.selectById(attrGroupId);
if (attrGroupEntity!=null){
attrRespVO.setGroupName(attrGroupEntity.getAttrGroupName());
}
}
//查询并设置所属分类名
CategoryEntity categoryEntity = categoryDao.selectById(attrEntity.getCatelogId());
if (categoryEntity != null) {
attrRespVO.setCatelogName(categoryEntity.getName());
}
return attrRespVO;
})).collect(Collectors.toList());
//⑤将page信息封装并重新设置结果集 将重新封装好的respVOS设置为结果集
PageUtils pageUtils = new PageUtils(page);
pageUtils.setList(respVOS);
return pageUtils;
}
3 修改属性
后端
回显–
-
给AttrRespVO添加一条成员变量private Long[] catelogPath;因为级联菜单回显需要完整路径
-
改造修改后获取属性数据回显的接口和业务层方法
com.atguigu.gulimall.product.controller.AttrController#info
/** * 信息 * 改造 完善回显需要的数据 */ @RequestMapping("/info/{attrId}") //@RequiresPermissions("product:attr:info") public R info(@PathVariable("attrId") Long attrId){ //AttrEntity attr = attrService.getById(attrId); AttrRespVO attrRespVO = attrService.getAttrInfo(attrId); return R.ok().put("attr", attrRespVO); }
com.atguigu.gulimall.product.service.impl.AttrServiceImpl#getAttrInfo
@Override public AttrRespVO getAttrInfo(Long attrId) { //查询PO AttrEntity attrEntity = this.getById(attrId); //创建VO并拷贝 AttrRespVO attrRespVO = new AttrRespVO(); BeanUtils.copyProperties(attrEntity,attrRespVO); //分组 AttrAttrgroupRelationEntity relationEntity = attrAttrgroupRelationDao.selectOne(new QueryWrapper<AttrAttrgroupRelationEntity>().eq("attr_id", attrId)); if (relationEntity!=null && relationEntity.getAttrGroupId()!=null){ attrRespVO.setAttrGroupId(relationEntity.getAttrGroupId()); AttrGroupEntity attrGroupEntity = attrGroupDao.selectById(relationEntity.getAttrGroupId()); System.out.println("attrGroupEntity:"+attrGroupEntity); if (attrGroupEntity!=null){ attrRespVO.setGroupName(attrGroupEntity.getAttrGroupName()); } } //标签名 CategoryEntity categoryEntity = categoryDao.selectById(attrEntity.getCatelogId()); if (categoryEntity!=null){ attrRespVO.setCatelogName(categoryEntity.getName()); } //完整路径 //这里之前Category业务层自定义过一个方法根据分类catId获取携带完整路径的方法 注入 //categoryService 方便起见注入service 略微不规范 Long[] catelogPath = categoryService.findCatelogPath(categoryEntity.getCatId()); attrRespVO.setCatelogPath(catelogPath); return attrRespVO; }
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1je1U4uD-1646456434682)(C:\Users\ck\AppData\Roaming\Typora\typora-user-images\1645708856354.png)]
修改–
-
改造接口修改方法com.atguigu.gulimall.product.controller.AttrController#update
/** * 修改 * 改造 用AttrVO接收 */ @RequestMapping("/update") //@RequiresPermissions("product:attr:update") public R update(@RequestBody AttrVO attr){ attrService.updateAttr(attr); return R.ok(); }
2.业务层具体实现com.atguigu.gulimall.product.service.impl.AttrServiceImpl#updateAttr
@Transactional
@Override
public void updateAttr(AttrVO attr) {
//PO
AttrEntity attrEntity = new AttrEntity();
BeanUtils.copyProperties(attr,attrEntity);
this.updateById(attrEntity);
//修改分组关联表
//注意 如果之前就没有给当前属性设置分组 那么此次操作就是新增
AttrAttrgroupRelationEntity relationEntity = new AttrAttrgroupRelationEntity();
relationEntity.setAttrId(attr.getAttrId());
relationEntity.setAttrGroupId(attr.getAttrGroupId());
QueryWrapper<AttrAttrgroupRelationEntity> queryWrapper = new QueryWrapper<AttrAttrgroupRelationEntity>().eq("attr_id", attr.getAttrId());
Integer count = attrAttrgroupRelationDao.selectCount(queryWrapper);
if (count > 0){
//修改操作
attrAttrgroupRelationDao.update(relationEntity, queryWrapper);
}else {
//新增操作
attrAttrgroupRelationDao.insert(relationEntity);
}
}
3.2.2 销售属性的增删改查
先导-
由于前端中销售属性的组件saleattr.vue之间导入基本属性的组件baseattr.vue仅仅是将 参数attrtype设置为了0。查看组件baseattr.vue,查询请求地址为url: this. h t t p . a d o r n U r l ( ‘ / p r o d u c t / a t t r / http.adornUrl(`/product/attr/ http.adornUrl(‘/product/attr/{type}/list/${this.catId}`),携带了attrtype参数且默认给值1.
总而言之,销售属性模块通过attrtype属性实现了和基本属性模块的复用,所以后端attrController和attrService中方法都要考虑复用后的改动:查询所有-拼接条件改造 查询单个信息-分组判断 保存销售属性-分组判断 修改销售属性-分组判断
1.查询销售属性
后端
-
改造com.atguigu.gulimall.product.controller.AttrController#baseList
/** * 创建一个查询列表方法 * 携带路径参数catelogId三级分类标签Id * 请求参数(分页参数&模糊查询key) * 查询全部属性和当前节点属性 * * 进一步改造 由于获取销售属性的请求地址是/sale/list/{catelogId} * 基本属性的请求地址是/base/list/{catelogId},所以可以将此方法复用 */ @GetMapping("/{attrType}/list/{catelogId}") //@RequiresPermissions("product:attr:list") public R baseList(@RequestParam Map<String, Object> params, @PathVariable("catelogId") Long catelogId, @PathVariable("attrType") String type){ //自定义方法 //PageUtils page = attrService.queryBaseListPage(params,catelogId); //添加路径参数attrType后进一步改造 PageUtils page = attrService.queryBaseListPage(params,catelogId,type); return R.ok().put("page", page); }
-
common模块下新增constant包并创建ProductConstant用来存放商品服务需要用到的常量
public class ProductConstant { //属性相关的枚举 public enum AttrEnum{ ATTR_TYPE_BASE(1,"基本属性"),ATTR_TYPE_SALE(0,"销售属性"); private int typeCode; private String typeMsg; AttrEnum(int typeCode, String typeMsg) { this.typeCode = typeCode; this.typeMsg = typeMsg; } public int getTypeCode() { return typeCode; } public String getTypeMsg() { return typeMsg; } } }
-
改造com.atguigu.gulimall.product.service.impl.AttrServiceImpl#queryBaseListPage
//①创建查询条件
//进一步改造 销售属性复用基本属性模块
//增加type参量后进一步拼接新的条件 --三元拼接换参数类型
QueryWrapper<AttrEntity> queryWrapper = new QueryWrapper<AttrEntity>().eq("attr_type", "base".
equalsIgnoreCase(type) ? ProductConstant.AttrEnum.ATTR_TYPE_BASE.getTypeCode() :
ProductConstant.AttrEnum.ATTR_TYPE_SALE.getTypeCode());
修改销售属性
- 改造修改时获取属性详情的方法com.atguigu.gulimall.product.service.impl.AttrServiceImpl#getAttrInfo
在设置分组信息时讨论
//复用的改造 探讨是否是基本属性调用
if (attrEntity.getAttrType() == ProductConstant.AttrEnum.ATTR_TYPE_BASE.getTypeCode()){
//分组
AttrAttrgroupRelationEntity relationEntity = attrAttrgroupRelationDao.selectOne(new QueryWrapper<AttrAttrgroupRelationEntity>().eq("attr_id", attrId));
if (relationEntity!=null && relationEntity.getAttrGroupId()!=null){
attrRespVO.setAttrGroupId(relationEntity.getAttrGroupId());
AttrGroupEntity attrGroupEntity = attrGroupDao.selectById(relationEntity.getAttrGroupId());
System.out.println("attrGroupEntity:"+attrGroupEntity);
if (attrGroupEntity!=null){
attrRespVO.setGroupName(attrGroupEntity.getAttrGroupName());
}
}
}
- 改造修改方法com.atguigu.gulimall.product.service.impl.AttrServiceImpl#updateAttr
修改分组关联表之前判断是否是基本属性
//进一步改造 基本属性和销售属性复用
if (attrEntity.getAttrType() == ProductConstant.AttrEnum.ATTR_TYPE_BASE.getTypeCode()){
//修改分组关联表
//注意 如果之前就没有给当前属性设置分组 那么此次操作就是新增
AttrAttrgroupRelationEntity relationEntity = new AttrAttrgroupRelationEntity();
relationEntity.setAttrId(attr.getAttrId());
relationEntity.setAttrGroupId(attr.getAttrGroupId());
QueryWrapper<AttrAttrgroupRelationEntity> queryWrapper = new QueryWrapper<AttrAttrgroupRelationEntity>().eq("attr_id", attr.getAttrId());
Integer count = attrAttrgroupRelationDao.selectCount(queryWrapper);
if (count > 0){
//修改操作
attrAttrgroupRelationDao.update(relationEntity, queryWrapper);
}else {
//新增操作
attrAttrgroupRelationDao.insert(relationEntity);
}
}
新增销售属性
@Transactional
@Override
public void saveAttr(AttrVO attr) {
//1.创建po对象保存到数据库中
AttrEntity attrEntity = new AttrEntity();
//使用spring提供的BeanUtils.copyProperties(源,目标)完成vo属性值拷贝到po
//使用前提-Vo中属性名和Po中相同
BeanUtils.copyProperties(attr,attrEntity);
this.save(attrEntity);
//2.保存关联关系 - 注入AttrAttrgroupRelationDao
//2.1 进一步改造 探讨是否是基本属性调用 如果是基本属性则保存关联关系
//注意属性分组Id也要判空
if (attr.getAttrType() == ProductConstant.AttrEnum.ATTR_TYPE_BASE.getTypeCode() && attr.getAttrGroupId() != null){
AttrAttrgroupRelationEntity relationEntity = new AttrAttrgroupRelationEntity();
//主要接收属性Id和属性分组ID
relationEntity.setAttrGroupId(attr.getAttrGroupId());
relationEntity.setAttrId(attrEntity.getAttrId());
attrAttrgroupRelationDao.insert(relationEntity);
}
}
3.3 属性分组表pms_attr_group关联属性的增删改查
3.3.1 查询当前属性分组Id的所有关联属性
后端
- 新增方法com.atguigu.gulimall.product.controller.AttrGroupController#attrRelation
/**
* 新增方法
* 根据当前分组Id获取所有关联的基本属性(规格参数)
*/
@RequestMapping("/{attrgroupId}/attr/relation")
//@RequiresPermissions("product:attrgroup:list")
public R attrRelation(@PathVariable("attrgroupId") Long attrgroupId){
//注入AttrService 创造一个根据属性分组Id获取属性PO列表的方法
List<AttrEntity> attrEntities = attrService.getRelationAttr(attrgroupId);
//包装后返回 起名为data
return R.ok().put("data", attrEntities);
}
- 新增com.atguigu.gulimall.product.service.impl.AttrServiceImpl#getRelationAttr
//通过属性分组Id获取关联属性的信息 基本属性|规格参数
@Override
public List<AttrEntity> getRelationAttr(Long attrgroupId) {
//通过关联表查出当前分组Id的所有关联实体类
List<AttrAttrgroupRelationEntity> relationEntities = attrAttrgroupRelationDao.selectList(new QueryWrapper<AttrAttrgroupRelationEntity>().eq("attr_group_id", attrgroupId));
//取出所有属性Id
List<Long> attrIds = relationEntities.stream().map((relationEntity) -> {
return relationEntity.getAttrId();
}).collect(Collectors.toList());
//判断属性ID数组是否为空 不能用attrIds != null批断
if (attrIds.size()>0){
//根据属性Ids查出所有属性集合
Collection<AttrEntity> attrEntities = this.listByIds(attrIds);
return (List<AttrEntity>) attrEntities;
}
//属性ID集合为空
return null;
}
3.3.2 移除当前属性分组和当前属性的关联关系
- 新增com.atguigu.gulimall.product.vo.AttrGroupRelationVO数据格式
@Data
public class AttrGroupRelationVO {
//收集属性ID和属性分组ID 用于移除属性和属性分组关联关系用
private Long attrId;
private Long attrGroupId;
}
-
新增接口方法com.atguigu.gulimall.product.controller.AttrGroupController#deleteRelation
注意这里发送的POST请求 获取前端发送的JSON数据要封装成自定义对象要添加@RequestBody注解
/**
* 新增方法
* 移除当前属性分组和当前属性的关联关系
*
* 注意:这里发送的POST请求 获取前端发送的JSON数据要封装成自定义对象要添加@RequestBody注解
*/
@PostMapping("/attr/relation/delete")
public R deleteRelation(@RequestBody AttrGroupRelationVO[] vos){
//新增删除关联关系方法
attrService.deleteRelation(vos);
return R.ok();
}
- 新增业务方法com.atguigu.gulimall.product.service.impl.AttrServiceImpl#deleteRelation
//根据AttrGroupRelationVO[]删除关联关系
@Override
public void deleteRelation(AttrGroupRelationVO[] vos) {
//如果都按如下方式逐个删除咋需要访问很多次数据库,占用资源太多。
// attrAttrgroupRelationDao.delete(new QueryWrapper<AttrAttrgroupRelationEntity>().
eq("attr_id",1L).and().eq("attr_group_id",1L));
//Dao层自定义一个批量删除的方法
//由于Dao层操作的都是PO类 所以将vos重新映射为POs --规范操作
List<AttrAttrgroupRelationEntity> relationEntities = Arrays.asList(vos).stream().map((vo) -> {
AttrAttrgroupRelationEntity relationEntity = new AttrAttrgroupRelationEntity();
BeanUtils.copyProperties(vo, relationEntity);
return relationEntity;
}).collect(Collectors.toList());
attrAttrgroupRelationDao.deleteBatchRelation(relationEntities);
}
- 新增关联表Dao方法com.atguigu.gulimall.product.dao.AttrAttrgroupRelationDao#deleteBatchRelation
@Mapper
public interface AttrAttrgroupRelationDao extends BaseMapper<AttrAttrgroupRelationEntity> {
//注意标注@Params("属性名") 方便Mapper文件取值
void deleteBatchRelation(@Param("entities") List<AttrAttrgroupRelationEntity> relationEntities);
}
5.添加Mapper文件SQL语句
理想发送的SQL语句应为如下方式-(这里不通过 IN 查属性Id原因未知)
DELETE FROM pms_attr_attrgroup_relation
WHERE
(attr_id = #{item.attrId} AND attr_group_id = #{item.attrGroupId}) OR
(attr_id = #{item.attrId} AND attr_group_id = #{item.attrGroupId}) OR 略
另外遍历拼接查询条件的分隔符separator设置注意前后空格
<!--自定义批量删除关联关系方法 遍历关联集合时 每遍历一对关联关系数据都要添加一个 OR 注意前后空格-->
<delete id="deleteBatchRelation">
DELETE FROM `pms_attr_attrgroup_relation` WHERE
<foreach collection="entities" item="item" separator=" OR ">
(attr_id = #{item.attrId} AND attr_group_id = #{item.attrGroupId})
</foreach>
</delete>
3.3.3 添加当前属性分组的关联属性
查出当前分组没有关联属性分组的基本属性–
-
新增接口com.atguigu.gulimall.product.controller.AttrGroupController#attrNoRelation
/** * 新增方法 * 获取当前分组未关联属性分组的属性 */ @RequestMapping("/{attrgroupId}/noattr/relation") public R attrNoRelation(@RequestParam Map<String, Object> params, @PathVariable("attrgroupId") Long attrgroupId){ //创建一个获取当前分组无关联属性分组的属性的方法 返回page信息 因为要获取分页数据 PageUtils page = attrService.getNoRelation(params,attrgroupId); return R.ok().put("page", page); }
-
新增com.atguigu.gulimall.product.service.impl.AttrServiceImpl#getNoRelation
注意点tips:
①思路: 查出当前属性分组Id的分类Id -> 查出当前分类Id的所有属性分组->在中间表中查出所有属性分组中有关联属性的中间Bean并取出有关联的属性Ids->查找属性为当前分类且是基本属性且Id不在有关联的属性Ids中的属性(期间封装条件时别忘了判断params.key和ids是否为空->封装成页面信息返回
②拼接条件查询时记得做非空处理
/**
* 获取当前分组未关联的无关联状态的属性
* @param params
* @param attrgroupId
* @return
*/
@Override
public PageUtils getNoRelation(Map<String, Object> params, Long attrgroupId) {
//1.当前分组只能关联自己所属的分类里的所有属性 先查出所属分类Id
AttrGroupEntity attrGroupEntity = attrGroupDao.selectById(attrgroupId);
Long catelogId = attrGroupEntity.getCatelogId();
//2.当前分组只能关联别的分组没有引用的属性
//2.1 查出当前分类下的所有属性分组
List<AttrGroupEntity> allAttrGroupEntities = attrGroupDao.selectList(new QueryWrapper<AttrGroupEntity>().
eq("catelog_id", catelogId));
List<Long> allAttrGrpIds = allAttrGroupEntities.stream().map((attrGrp -> {
return attrGrp.getAttrGroupId();
})).collect(Collectors.toList());
//2.2通过属性属性分组关联表查找所有属性分组存在关联的属性Id
//先从中间表查出关联的中间Bean
List<AttrAttrgroupRelationEntity> anotherAttrAgRels = attrAttrgroupRelationDao.selectList(new QueryWrapper<AttrAttrgroupRelationEntity>().in("attr_group_id", allAttrGrpIds));
//流式编程取出attrId
List<Long> anotherAttrIds = anotherAttrAgRels.stream().map((attrAg) -> {
return attrAg.getAttrId();
}).collect(Collectors.toList());
//2.3查找当前属性分组可以关联的属性 即当前分组的所有attrId不在anotherAttrIds中即可
//this.baseMapper 相当于注入了attrDao 这里已经注入
//构造查询条件 ①查出当前分类ID下的所有基本属性 ②属性ID不在anotherAttrIds中的即为可绑定属性
QueryWrapper<AttrEntity> queryWrapper = new QueryWrapper<AttrEntity>().
eq("catelog_id", catelogId).eq("attr_type",ProductConstant.AttrEnum.ATTR_TYPE_BASE.getTypeCode());
//拼接.notIn("attr_id", anotherAttrIds);前要先判断anotherAttrIds是否为空且数组长度大于0
if (anotherAttrIds != null && anotherAttrIds.size()>0){
queryWrapper.notIn("attr_id", anotherAttrIds);
}
//请求参数中有key 判断key是否为空进而拼装新的查询条件
String key = (String) params.get("key");
if (!StringUtils.isEmpty(key)){
//拼接模糊查询条件 注意用and(函数式编程)
queryWrapper.and((wrapper)->{
wrapper.eq("attr_id",key).or().like("attr_name",key);
});
}
//调用page方法封装为Ipage
IPage<AttrEntity> page = this.page(new Query<AttrEntity>().getPage(params), queryWrapper);
return new PageUtils(page);
}
保存选中要关联的属性–
- 新增接口com.atguigu.gulimall.product.controller.AttrGroupController#addRelation
/**
* 新增方法
* 新增属性分组和属性的关联关系
*
*/
@PostMapping("/attr/relation")
public R addRelation(@RequestBody List<AttrGroupRelationVO> vos){
//新增添加关联关系方法
relationService.saveBatch(vos);
return R.ok();
}
2.新增中间表业务层方法com.atguigu.gulimall.product.service.impl.AttrAttrgroupRelationServiceImpl#saveBatch
@Override
public void saveBatch(List<AttrGroupRelationVO> vos) {
//将vos用流式编程映射处attrAttrGroupEntites
List<AttrAttrgroupRelationEntity> relationEntities = vos.stream().map((vo) -> {
AttrAttrgroupRelationEntity relationEntity = new AttrAttrgroupRelationEntity();
BeanUtils.copyProperties(vo, relationEntity);
return relationEntity;
}).collect(Collectors.toList());
this.saveBatch(relationEntities);
}
3.4 发布商品功能的实现–大保存
3.4.1 实现获取当前分类关联品牌接口
后端
-
新增接口方法com.atguigu.gulimall.product.controller.CategoryBrandRelationController#relationBrandslist
延伸-Controller功能定位
/** * 新增方法 * 获取当前分类关联的所有品牌 * * 延伸讲述-对Controller的功能定位 * 1.处理请求,接收和校验数据 * 2.将通过校验的数据交给Service进行业务处理 * 3.接收Service处理完的数据,封装页面指定的VO以及其它需求(分页,统一返回等)返回 */ @GetMapping("/brand/list") public R relationBrandslist(@RequestParam(value = "catId",required = true) Long catId){ //自定义方法查出BrandEnties 再创建BrandVO 将PO映射为VO List<BrandEntity> brandEntities = categoryBrandRelationService.getBrandsByCatId(catId); List<BrandVO> vos = brandEntities.stream().map((brandEntity -> { BrandVO brandVO = new BrandVO(); //由于这里PO 和 VO 关于品牌名设置不同,不能使用属性拷贝的方式 brandVO.setBrandId(brandEntity.getBrandId()); brandVO.setBrandName(brandEntity.getName()); return brandVO; })).collect(Collectors.toList()); return R.ok().put("data", vos); }
-
新增业务层方法com.atguigu.gulimall.product.service.impl.CategoryBrandRelationServiceImpl#getBrandsByCatId
/**
*
* @param catId
* @return
*/
@Override
public List<BrandEntity> getBrandsByCatId(Long catId) {
//关联表中查出当前分类ID的关联表Bean
List<CategoryBrandRelationEntity> relationEntities = this.baseMapper.selectList(new QueryWrapper<CategoryBrandRelationEntity>().eq("catelog_id", catId));
//查出对应ID的品牌
List<BrandEntity> brandEntities = relationEntities.stream().map((relation) -> {
Long brandId = relation.getBrandId();
BrandEntity brandEntity = brandService.getById(brandId);
return brandEntity;
}).collect(Collectors.toList());
return brandEntities;
}
3.4.2 实现获取当前分类属性分组及属性集合的接口
-
新增com.atguigu.gulimall.product.vo.AttrGroupWithAttrsVO封装类
@Data public class AttrGroupWithAttrsVO { /** * 分组id */ private Long attrGroupId; /** * 组名 */ private String attrGroupName; /** * 排序 */ private Integer sort; /** * 描述 */ private String descript; /** * 组图标 */ private String icon; /** * 所属分类id */ private Long catelogId; //attrs private List<AttrEntity> attrs; }
-
新增接口方法com.atguigu.gulimall.product.controller.AttrGroupController#getAttrGroupWithAttrs
/** * 新增方法 * 获取当前分类属性分组及属性 */ @GetMapping("/{catelogId}/withattr") public R getAttrGroupWithAttrs(@PathVariable("catelogId") Long catelogId){ //自定义业务层接口方法 List<AttrGroupWithAttrsVO> vos = attrGroupService.getAttrGroupWithAttrsByCatelogId(catelogId); return R.ok().put("data",vos); }
-
实现业务层方法com.atguigu.gulimall.product.service.impl.AttrGroupServiceImpl#getAttrGroupWithAttrsByCatelogId
/** * 根据分类Id查出所有属性分组及这些组里关联的属性 * @param catelogId * @return */ @Override public List<AttrGroupWithAttrsVO> getAttrGroupWithAttrsByCatelogId(Long catelogId) { //1.查出所有属性分组 List<AttrGroupEntity> attrGroupEntities = this.list(new QueryWrapper<AttrGroupEntity>().eq("catelog_id", catelogId)); //2.查出所有组里关联的属性 List<AttrGroupWithAttrsVO> vos = attrGroupEntities.stream().map((attrGroupEntity -> { AttrGroupWithAttrsVO vo = new AttrGroupWithAttrsVO(); BeanUtils.copyProperties(attrGroupEntity, vo); //注入AttrService 之前写过通过当前分组ID获取当前分类ID下的所有基本属性的方法 //循环查表?? List<AttrEntity> relationAttr = attrService.getRelationAttr(attrGroupEntity.getAttrGroupId()); if (relationAttr != null){ //非空处理 vo.setAttrs(relationAttr); } return vo; })).collect(Collectors.toList()); return vos; }
3.4.3 实现保存当前商品信息 大保存–发布商品菜单
-
先通过Json在线格式化网站生成前端商品信息的SpuSaveVo 并改造成员变量类型 如ID->Long,有小数字段->BigDecimal等
-
修改保存的接口方法com.atguigu.gulimall.product.controller.SpuInfoController#save
/** * 保存 * 修改保存方法 用SpuSaveVo接收 * 并新建业务层保存方法 */ @RequestMapping("/save") //@RequiresPermissions("product:spuinfo:save") public R save(@RequestBody SpuSaveVo vo){ spuInfoService.saveSpuInfo(vo); return R.ok(); }
-
新增业务层保存方法–大保存com.atguigu.gulimall.product.service.impl.SpuInfoServiceImpl#saveSpuInfo
/** * //TODO 高级部分完善 * @param vo */ @Transactional @Override public void saveSpuInfo(SpuSaveVo vo) { //1、保存spu基本信息 pms_spu_info //字段 id spu_name spu_description catalog_id brand_id weight publish_status create_time update_time SpuInfoEntity infoEntity = new SpuInfoEntity(); BeanUtils.copyProperties(vo,infoEntity); infoEntity.setCreateTime(new Date()); infoEntity.setUpdateTime(new Date()); this.saveBaseSpuInfo(infoEntity); //2、保存Spu的描述图片 pms_spu_info_desc //字段 spu_id decript List<String> decript = vo.getDecript(); SpuInfoDescEntity descEntity = new SpuInfoDescEntity(); descEntity.setSpuId(infoEntity.getId()); //将描述信息(图片路径)用","分割 descEntity.setDecript(String.join(",",decript)); spuInfoDescService.saveSpuInfoDesc(descEntity); //3、保存spu的图片集 pms_spu_images //字段 id spu_id img_name img_url img_sort default_img List<String> images = vo.getImages(); //创建spuImagesService批量保存图片的方法 传入spu_id和图片集 spuImagesService.saveImages(infoEntity.getId(),images); //4、保存spu的规格参数(基本属性);pms_product_attr_value // id spu_id attr_id attr_name attr_value attr_sort quick_show List<BaseAttrs> baseAttrs = vo.getBaseAttrs(); //在ProductAttrValueService创建批量保存的方法 productAttrValueService.saveProductAttr(baseAttrs,infoEntity.getId()); //5、保存spu的积分信息;远程调用优惠券服务 gulimall_sms->sms_spu_bounds //需要参数 成长积分growBounds 购物积分buyBounds 封装在Bounds中 //还需要额外提交spuId //这样服务间传递的数据可以封装成一个TO对象 在common中创建to包并创建 Bounds bounds = vo.getBounds(); SpuBoundTo spuBoundTo = new SpuBoundTo(); BeanUtils.copyProperties(bounds,spuBoundTo); spuBoundTo.setSpuId(infoEntity.getId()); R r = couponFeignService.saveSpuBounds(spuBoundTo); if(r.getCode() != 0){ log.error("远程保存spu积分信息失败"); } //6、保存当前spu对应的所有sku信息; //获取所有sku信息 List<Skus> skus = vo.getSkus(); //非空处理 //if(skus!=null && skus.size()>0){ if(!CollectionUtils.isEmpty(skus)){ skus.forEach(item->{ //6.1)、sku的基本信息;pms_sku_info //获取默认图片,遍历图集 查看defaultImg字段是否为1 为1则设置默认图片URL String defaultImg = ""; //这里不使用forEach遍历 因为forEach是线程不安全的 for (Images image : item.getImages()) { if(image.getDefaultImg() == 1){ defaultImg = image.getImgUrl(); } } //创建SkuInfoEntity实体类 SkuInfoEntity skuInfoEntity = new SkuInfoEntity(); //SkuInfoEntity需要的字段 //sku_id spu_id sku_name sku_desc catalog_id brand_id // sku_default_img sku_title sku_subtitle price sale_count //Item中字段 //attr skuName skuTitle skuSubtitle images descar fullCount discount countStatus //fullPrice reducePrice priceStatus memberPrice //属性对拷 BeanUtils.copyProperties(item,skuInfoEntity); // item中没有的字段 // brand_id catalog_id sale_count spu_id skuName sku_default_img // price skuTitle skuSubtitle skuInfoEntity.setBrandId(infoEntity.getBrandId()); skuInfoEntity.setCatalogId(infoEntity.getCatalogId()); skuInfoEntity.setSaleCount(0L); skuInfoEntity.setSpuId(infoEntity.getId()); //默认图片的寻找在代码起始部分处理 skuInfoEntity.setSkuDefaultImg(defaultImg); //保存封装好的sku基本信息 skuInfoService.saveSkuInfo(skuInfoEntity); //6.2)、sku的图片信息;pms_sku_image //将skuId获取出去方便后续使用 Long skuId = skuInfoEntity.getSkuId(); //获取SkuImagesEntities SkuImagesEntity存储的字段 //id sku_id img_url img_sort default_img List<SkuImagesEntity> imagesEntities = item.getImages().stream().map(img -> { SkuImagesEntity skuImagesEntity = new SkuImagesEntity(); skuImagesEntity.setSkuId(skuId); skuImagesEntity.setImgUrl(img.getImgUrl()); skuImagesEntity.setDefaultImg(img.getDefaultImg()); return skuImagesEntity; }).filter(entity->{ //TODO 没有图片路径的无需保存 已解决 通过过滤器过滤掉 //返回true就是需要,false就是剔除 return !StringUtils.isEmpty(entity.getImgUrl()); }).collect(Collectors.toList()); //批量保存 skuImagesService.saveBatch(imagesEntities); //6.3)、sku的销售属性信息:pms_sku_sale_attr_value // id sku_id attr_id attr_name attr_value attr_sort //获取sku中存放的销售属性 //attrId attrName attrValue //缺少sku_id字段 List<Attr> attr = item.getAttr(); List<SkuSaleAttrValueEntity> skuSaleAttrValueEntities = attr.stream().map(a -> { SkuSaleAttrValueEntity attrValueEntity = new SkuSaleAttrValueEntity(); BeanUtils.copyProperties(a, attrValueEntity); attrValueEntity.setSkuId(skuId); return attrValueEntity; }).collect(Collectors.toList()); skuSaleAttrValueService.saveBatch(skuSaleAttrValueEntities); //6.4)、sku的优惠、满减等信息;gulimall_sms->sms_sku_ladder\sms_sku_full_reduction\sms_member_price //需要操作远程服务 //复习-通过feign调用远程服务的前提 //1.远程服务必须上线且放在服务注册中心中 - 这里coupon优惠券服务在bootstarp.properties中 //已经配置好配置中心 且在关联的配置中心配置文件中有相关的注册中心地址等配置 //2.远程服务必须开启服务的注册与发现 - 这里coupon优惠券服务启动类已经标注注解@EnableDiscoveryClient //3.在想要调用远程服务的模块定义一个feign包并编写一个接口 类上注明@FeignClient("远程服务名") //类内指明要调用的远程服务方法(不需要写方法体)以及请求路径@RequestMapping SkuReductionTo skuReductionTo = new SkuReductionTo(); BeanUtils.copyProperties(item,skuReductionTo); skuReductionTo.setSkuId(skuId); //BigDeciaml类型变量的比较方式 if(skuReductionTo.getFullCount() >0 || skuReductionTo.getFullPrice().compareTo(new BigDecimal("0")) == 1){ R r1 = couponFeignService.saveSkuReduction(skuReductionTo); if(r1.getCode() != 0){ log.error("远程保存sku优惠信息失败"); } } }); } }
-
以上大保存方法调用了Coupon模块的服务,所以创建feign包并定义了com.atguigu.gulimall.product.feign.CouponFeignService.并在Coupon模块自定义了两个方法用于保存积分和满减折扣信息
@FeignClient("gulimall-coupon") public interface CouponFeignService { //远程调用优惠券服务中保存积分的方法-注意一定要填写完整路径 //另外这里发送Post请求且封装成自定义的数据类型 要使用@RequestBody @PostMapping("/coupon/spubounds/save") R saveSpuBounds(@RequestBody SpuBoundTo spuBoundTo); //远程调用自定义的满减折扣方法 @PostMapping("/coupon/skufullreduction/saveinfo") R saveSkuReduction(@RequestBody SkuReductionTo skuReductionTo); }
保存积分的方法调用逆向生成的方法即可,且方法的参数SpuBoundTo以json方式传递,Coupon模块匹配请求以SpuBoundsEntity接收时会字段转换。
而保存满减折扣方法则需要新增一个接口和方法以及新的数据封装格式
接口com.atguigu.gulimall.coupon.controller.SkuFullReductionController#saveInfo
/** * 自定义满减折扣方法 用于Product模块远程调用 * @param skuReductionTo * @return */ @PostMapping("/saveinfo") R saveInfo(@RequestBody SkuReductionTo skuReductionTo){ //自定义方法 skuFullReductionService.saveSkuReduction(skuReductionTo); return R.ok(); }
满减折扣To-服务之间传递数据的封装 com.atguigu.common.to.SkuReductionTo
/** * 优惠券服务满减TO */ @Data public class SkuReductionTo { private Long skuId; private int fullCount; private BigDecimal discount; private int countStatus; private BigDecimal fullPrice; private BigDecimal reducePrice; private int priceStatus; private List<MemberPrice> memberPrice; }
新增业务层方法com.atguigu.gulimall.coupon.service.impl.SkuFullReductionServiceImpl#saveSkuReduction
/** * 自定义保存满减折扣等信息 * @param skuReductionTo */ @Override public void saveSkuReduction(SkuReductionTo skuReductionTo) { //1.保存阶梯价格 //sms_sku_ladder表 SkuLadderEntity skuLadderEntity = new SkuLadderEntity(); skuLadderEntity.setSkuId(skuReductionTo.getSkuId()); skuLadderEntity.setFullCount(skuReductionTo.getFullCount()); skuLadderEntity.setDiscount(skuReductionTo.getDiscount()); skuLadderEntity.setAddOther(skuReductionTo.getCountStatus()); //折后价price下订单时最后算 //保存前判断是否有打折价格 if (skuReductionTo.getFullCount()>0){ skuLadderService.save(skuLadderEntity); } //2.保存满减信息 //sms_sku_full_reduction表 SkuFullReductionEntity skuFullReductionEntity = new SkuFullReductionEntity(); //属性对拷 BeanUtils.copyProperties(skuReductionTo,skuFullReductionEntity); //SkuFullReductionEntity中叠加信息用的AddOther字段而skuReductionTo中存放的是CountStatus skuFullReductionEntity.setAddOther(skuReductionTo.getCountStatus()); //判断是否有满减 满减价格>0保存 if (skuFullReductionEntity.getFullPrice().compareTo(new BigDecimal("0")) == 1){ this.save(skuFullReductionEntity); } //3.会员价格 //sms_member_price表 List<MemberPrice> memberPrices = skuReductionTo.getMemberPrice(); List<MemberPriceEntity> memberPriceEntities = memberPrices.stream().map((memberPrice) -> { MemberPriceEntity memberPriceEntity = new MemberPriceEntity(); memberPriceEntity.setSkuId(skuReductionTo.getSkuId()); memberPriceEntity.setMemberLevelId(memberPrice.getId()); memberPriceEntity.setMemberLevelName(memberPrice.getName()); memberPriceEntity.setMemberPrice(memberPrice.getPrice()); //默认叠加其它优惠 memberPriceEntity.setAddOther(1); return memberPriceEntity; }).filter(item->{ //过滤掉没有会员价的实体 return item.getMemberPrice().compareTo(new BigDecimal("0")) == 1; }).collect(Collectors.toList()); memberPriceService.saveBatch(memberPriceEntities); }
-
使用debug模式给com.atguigu.gulimall.product.service.impl.SpuInfoServiceImpl#saveSpuInfo方法打断点,查看各个子方法结果是否正确。
3.4.4 spu管理菜单
后端
实现根据条件查询spu
-
改造接口方法com.atguigu.gulimall.product.controller.SpuInfoController#list
/** * 列表 * 改造携带上查询条件 */ @RequestMapping("/list") public R list(@RequestParam Map<String, Object> params){ //PageUtils page = spuInfoService.queryPage(params); PageUtils page = spuInfoService.queryPageByCondition(params); return R.ok().put("page", page); }
-
新增实现方法com.atguigu.gulimall.product.service.impl.SpuInfoServiceImpl#queryPageByCondition
/** * 根据查询条件查找商品信息 * @param params * @return */ @Override public PageUtils queryPageByCondition(Map<String, Object> params) { QueryWrapper<SpuInfoEntity> wrapper = new QueryWrapper<>(); //拼接查询条件key status brandId catelogId //模糊查询关键字 String key = (String) params.get("key"); if(!StringUtils.isEmpty(key)){ wrapper.and((w)->{ w.eq("id",key).or().like("spu_name",key); }); } // status=1 and (id=1 or spu_name like xxx) String status = (String) params.get("status"); if(!StringUtils.isEmpty(status)){ wrapper.eq("publish_status",status); } String brandId = (String) params.get("brandId"); if(!StringUtils.isEmpty(brandId)&&!"0".equalsIgnoreCase(brandId)){ wrapper.eq("brand_id",brandId); } String catelogId = (String) params.get("catelogId"); if(!StringUtils.isEmpty(catelogId)&&!"0".equalsIgnoreCase(catelogId)){ wrapper.eq("catelog_id",catelogId); } IPage<SpuInfoEntity> page = this.page( new Query<SpuInfoEntity>().getPage(params), wrapper ); return new PageUtils(page); }
此外由于有日期类型的数据返回,可以在配置文件application.yml中通过spring.jackson.date-format设置日期返回格式。如果格式化日期不对可以设置一下时区time-zone
spring:
jackson:
date-format: yyyy-MM-dd HH:mm:ss
time-zone: GMT+8
实现获取spu规格信息-用于点击规格操作的回显
- 新增接口com.atguigu.gulimall.product.controller.AttrController#baseAttrListForSpu
@GetMapping("/base/listforspu/{spuId}")
public R baseAttrListForSpu(@PathVariable("spuId") Long spuId){
//注入ProductAttrValueService并创建根据spuId获取规格参数的方法
List<ProductAttrValueEntity> entities = productAttrValueService.baseAttrListForSpu(spuId);
return R.ok().put("data",entities);
}
- 业务层实现com.atguigu.gulimall.product.service.impl.ProductAttrValueServiceImpl#baseAttrListForSpu
/**
* 根据spuId获取规格参数
* @param spuId
* @return
*/
@Override
public List<ProductAttrValueEntity> baseAttrListForSpu(Long spuId) {
List<ProductAttrValueEntity> productAttrValueEntities = this.baseMapper.selectList(new QueryWrapper<ProductAttrValueEntity>().eq("spu_id", spuId));
return productAttrValueEntities;
}
实现更新spu规格信息-用于保存修改后的spu规格参数
-
新增接口方法com.atguigu.gulimall.product.controller.AttrController#updateSpuAttr
/** * 修改-根据spuId批量修改基本属性 * 注意前端以Json方式将数据放在请求体中 * 所以后端接收时加上@RequestBody注解 * 另外这里前端传递的字段与ProductAttrValueEntity数量不对 * 理应创建vo但是此处省略 */ @PostMapping("/update/{spuId}") public R updateSpuAttr(@PathVariable("spuId") Long spuId, @RequestBody List<ProductAttrValueEntity> entities){ //注入productAttrValueService并定义批量更新方法 productAttrValueService.updateSpuAttr(spuId,entities); return R.ok(); }
-
业务层具体实现com.atguigu.gulimall.product.service.impl.ProductAttrValueServiceImpl#updateSpuAttr
/**
* 根据spuId和ProductAttrValueentities批量更新
* @param spuId
* @param entities
*/
@Transactional
@Override
public void updateSpuAttr(Long spuId, List<ProductAttrValueEntity> entities) {
//由于没有批量更新的方法 这里偷懒起见 先批量删除然后再批量保存 实际业务要自己写批量更新的方法
//删除
this.remove(new QueryWrapper<ProductAttrValueEntity>().eq("spu_id",spuId));
//遍历entities集合设置spuId
entities.forEach((entity)->{
entity.setSpuId(spuId);
});
//批量保存
this.saveBatch(entities);
}
3.4.5 实现根据条件查询sku-商品管理菜单
后端
- 改造接口方法com.atguigu.gulimall.product.controller.SkuInfoController#list
/**
* 列表
* 改造 新增条件查询方法
*/
@RequestMapping("/list")
//@RequiresPermissions("product:skuinfo:list")
public R list(@RequestParam Map<String, Object> params){
//PageUtils page = skuInfoService.queryPage(params);
PageUtils page = skuInfoService.queryPageByCondition(params);
return R.ok().put("page", page);
}
-
新增业务层方法com.atguigu.gulimall.product.service.impl.SkuInfoServiceImpl#queryPageByCondition
注意前端默认值的处理,在拼接条件时注意排除
/**
* 新增sku条件查询的方法
* @param params
* @return
*/
@Override
public PageUtils queryPageByCondition(Map<String, Object> params) {
//创造条件
QueryWrapper<SkuInfoEntity> queryWrapper = new QueryWrapper<>();
//待拼接的条件字段 params
//key catelogId brandId min max
//拼接key 注意模糊查询有两个查询情况 所以要先用and拼接
String key = (String) params.get("key");
if (!StringUtils.isEmpty(key)){
queryWrapper.and((wrapper)->{
wrapper.eq("sku_id",key).or().like("sku_name",key);
});
}
//注意前端传送默认值的处理 非0则拼接
//拼接catelogId
String catelogId = (String) params.get("catelogId");
if (!StringUtils.isEmpty(key) && !"0".equalsIgnoreCase(catelogId)){
queryWrapper.eq("catelog_id",catelogId);
}
//注意前端传送默认值的处理 非0则拼接
//拼接brandId
String brandId = (String) params.get("brandId");
if (!StringUtils.isEmpty(key) && !"0".equalsIgnoreCase(brandId)){
queryWrapper.eq("brand_id",brandId);
}
//最小最大价格 对比表中的price ge()大于等于
//拼接min
String min = (String) params.get("min");
if (!StringUtils.isEmpty(key)){
queryWrapper.ge("price",min);
}
//由于前端给min和max都传入了默认值0
//当没有给值时发送的sql语句就变成了查询price=0的商品则永远查不到商品
//所以要进一步改造代码 将max=0的情况加以考虑
//最小最大价格 对比表中的price le()小于等于
//拼接max
/*String max = (String) params.get("max");
if (!StringUtils.isEmpty(key)){
queryWrapper.le("price",max);
}*/
String max = (String) params.get("max");
if (!StringUtils.isEmpty(key)){
//将key转为BigDecimal排除0的情况 其次有可能会有用户绕过
//前端发送非数字字符串的max导致创建BigDecimal异常这里捕获一下
try {
BigDecimal bigDecimal = new BigDecimal(key);
//跟0比较 比0大则拼接条件查询否则不拼接
if (bigDecimal.compareTo(new BigDecimal(0)) == 1){
queryWrapper.le("price",max);
}
}catch (Exception e){
}
}
IPage<SkuInfoEntity> page = this.page(
new Query<SkuInfoEntity>().getPage(params),
queryWrapper
);
return new PageUtils(page);
}
4.会员模块-gulimall_ums库
先导-
- 配置网关的路由地址,将前端会员模块发送的请求路由到会员模块
spring:
cloud:
gateway:
routes:
- id: member_route
uri: lb://gulimall-member
predicates:
- Path=/api/member/**
filters:
- RewritePath=/api/(?<segment>.*),/$\{segment}
-
检查会员模块的application.yml文件和bootstrap.properties文件
application.yml文件中检查服务注册中心 应用名 数据源 端口号 MyBatisPlus整合配置等
server: port: 8000 spring: datasource: username: root data-password: root driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://192.168.58.149:3306/gulimall_ums cloud: nacos: discovery: server-addr: 127.0.0.1:8848 application: name: gulimall-member mybatis-plus: mapper-locations: classpath:/mapper/**/*.xml global-config: db-config: id-type: auto
bootstrap.properties中主要设置配置中心地址,拉去配置中心配置等信息
-
检查主启动类的必要注解
/** * 远程调用别的服务的步骤(此处测试调用coupon优惠券服务) * * 1.pom.xml文件中引入spring-cloud-starter-openfeign,使该服务具有远程调用其它微服务的能力 * * 2.创建一个Feign包(盛放远程调用服务的接口)并在包下编写一个接口,使用注解@FeignClient("远程服务名") * * 告诉springcloud该接口需要远程调用的服务,且接口的每个方法告知调用该服务的哪个请求即都是对应该服务的 * * 控制器方法签名,注意请求路径完整 * * 3.开启远程调用其它服务的功能,在启动类上使用注解@EnableFeignClients(basePackages = "feign包的全包名"), * * 这样一旦启动该服务则会自动扫描feign包下使用@FeignClient注解的接口 */ @MapperScan("com.atguigu.gulimall.member.dao") @EnableFeignClients(basePackages = "com.atguigu.gulimall.member.feign") @EnableDiscoveryClient @SpringBootApplication public class GulimallMemberApplication { public static void main(String[] args) { SpringApplication.run(GulimallMemberApplication.class, args); } }
5.库存模块-gulimall_wms库
先导-
①检测注册中心,应用名等配置是否在配置文件中添加
查看SQL语句可以配置logging-level 指定包下debug
logging:
level:
com.atguigu: debug
②检测主启动类必要注解是否添加,事务@EnableTransactionManagement看需求
@MapperScan("com.atguigu.gulimall.ware.dao")
@EnableDiscoveryClient
③添加新的网关路由
spring:
cloud:
gateway:
routes:
- id: ware_route
uri: lb://gulimall-ware
predicates:
- Path=/api/ware/**
filters:
- RewritePath=/api/(?<segment>.*),/$\{segment}
5.1 仓库维护菜单的实现
5.1.1 条件查询实现
- 改造业务层方法com.atguigu.gulimall.ware.service.impl.WareInfoServiceImpl#queryPage
/**
* 改造方法 添加条件查询
* @param params
* @return
*/
@Override
public PageUtils queryPage(Map<String, Object> params) {
QueryWrapper<WareInfoEntity> 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).or().like("areacode",key);
}
IPage<WareInfoEntity> page = this.page(
new Query<WareInfoEntity>().getPage(params),
queryWrapper
);
return new PageUtils(page);
}
其它功能使用逆向生成的代码即可
5.2 商品库存菜单的实现
5.2.1 条件查询的实现
- 改造业务层查询方法com.atguigu.gulimall.ware.service.impl.WareSkuServiceImpl#queryPage
/**
* 条件查询 根据skuId 和 wareId查询
* @param params
* @return
*/
@Override
public PageUtils queryPage(Map<String, Object> params) {
QueryWrapper<WareSkuEntity> wrapper = new QueryWrapper<>();
String skuId = (String) params.get("skuId");
if (!StringUtils.isEmpty(skuId) && !"0".equalsIgnoreCase(skuId)){
wrapper.eq("sku_id",skuId);
}
String wareId = (String) params.get("wareId");
if (!StringUtils.isEmpty(wareId) && !"0".equalsIgnoreCase(wareId)){
wrapper.eq("ware_id", wareId);
}
IPage<WareSkuEntity> page = this.page(
new Query<WareSkuEntity>().getPage(params),
);
return new PageUtils(page);
}
实际场景中商品库存不能由当前菜单去新增和修改,而是通过一整套采购流程增加库存,这里只是为了可以测试阶段设置的新增和修改,具体的库存增加而是由后面的采购单维护目录实现。
5.3采购单维护目录的实现
5.3.1 采购需求菜单wms_purchase_detail
先导-
采购需要的创建由两种方式:①人工从后台新增采购需求 ②库存量过低,系统发出低库存预警自动生成采购需求.以下是采购的简要流程
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-U4jRyEzd-1646456434683)(C:\Users\ck\AppData\Roaming\Typora\typora-user-images\1646199827197.png)]
- 采购需求查询改造com.atguigu.gulimall.ware.service.impl.PurchaseDetailServiceImpl#queryPage
/**
* 改造查询采购需求方法wms_purchase_detail
* status状态 wareId仓库ID key模糊查询关键字
* 携带三个查询条件
* @param params
* @return
*/
@Override
public PageUtils queryPage(Map<String, Object> params) {
QueryWrapper<PurchaseDetailEntity> wrapper = new QueryWrapper<>();
String key = (String) params.get("key");
if (!StringUtils.isEmpty(key)){
wrapper.and((w)->{
w.eq("purchase_id",key).or().eq("sku_id",key)
});
}
String status = (String) params.get("status");
if (!StringUtils.isEmpty(status)){
wrapper.eq("status",status);
}
String wareId = (String) params.get("wareId");
if (!StringUtils.isEmpty(status) && !"0".equalsIgnoreCase(wareId)){
wrapper.eq("ware_id",wareId);
}
IPage<PurchaseDetailEntity> page = this.page(
new Query<PurchaseDetailEntity>().getPage(params),
wrapper
);
return new PageUtils(page);
}
- 新增查询未领取采购单的接口-具体见5.3.2 采购单菜单wms_purchase
- 合并采购单-前提先创建一个采购单使用逆向生成好的新增方法即可-具体见5.3.2 采购单菜单wms_purchase
5.3.2 采购单菜单wms_purchase
-
新增查询未领取采购单的接口和业务层实现方法
com.atguigu.gulimall.ware.controller.PurchaseController#unreceivelist
/** * 新增接口 查询未被领取的采购单 * 列表 */ @RequestMapping("/unreceive/list") //@RequiresPermissions("ware:purchase:list") public R unreceivelist(@RequestParam Map<String, Object> params){ //新增业务层方法 PageUtils page = purchaseService.queryPageUnreceivePurchase(params); return R.ok().put("page", page); }
com.atguigu.gulimall.ware.service.impl.PurchaseServiceImpl#queryPageUnreceivePurchase
/**
* 新增查询未被领取的采购单
* @param params
* @return
*/
@Override
public PageUtils queryPageUnreceivePurchase(Map<String, Object> params) {
QueryWrapper<PurchaseEntity> wrapper = new QueryWrapper<>();
//拼接status条件 0-采购单新建 1-采购单刚分配给某个人还未领取出发
wrapper.eq("status",0).or().eq("status",1);
IPage<PurchaseEntity> page = this.page(
new Query<PurchaseEntity>().getPage(params),
wrapper
);
return new PageUtils(page);
}
-
合并采购单
-
创建MergeVo用于接收点击合并采购单后前端发送的数据com.atguigu.gulimall.ware.vo.MergeVo
@Data public class MergeVo { private Long purchaseId;//采购单Id private List<Long> items;//合并项集合 }
-
新增接口方法com.atguigu.gulimall.ware.controller.PurchaseController#merge
/** * 新增接口 合并采购单 * post请求数据在请求体中 */ @PostMapping("/merge") //@RequiresPermissions("ware:purchase:list") public R merge(@RequestBody MergeVo mergeVo){ //新增业务层方法 purchaseService.mergePurchase(mergeVo); return R.ok(); }
-
创建库存模块的枚举类com.atguigu.common.constant.WareConstant
public class WareConstant { //采购状态相关的枚举 public enum PuchaseStatusEnum{ CREATED(0,"新建"),ASSIGNED(1,"已分配"), RECEIVE(2,"已领取"),FINISH(3,"已完成"), HASERROR(4,"有异常"),; private int typeCode; private String typeMsg; PuchaseStatusEnum(int typeCode, String typeMsg) { this.typeCode = typeCode; this.typeMsg = typeMsg; } public int getTypeCode() { return typeCode; } public String getTypeMsg() { return typeMsg; } } //采购需求的状态相关的枚举 public enum PuchaseDetailStatusEnum{ CREATED(0,"新建"),ASSIGNED(1,"已分配"), BUYING(2,"正在采购"),FINISH(3,"已完成"), HASERROR(4,"采购失败"),; private int typeCode; private String typeMsg; PuchaseDetailStatusEnum(int typeCode, String typeMsg) { this.typeCode = typeCode; this.typeMsg = typeMsg; } public int getTypeCode() { return typeCode; } public String getTypeMsg() { return typeMsg; } } }
-
新增合并采购单的业务层方法com.atguigu.gulimall.ware.service.impl.PurchaseServiceImpl#mergePurchase
/** * 合并采购单 * @param mergeVo */ @Transactional @Override public void mergePurchase(MergeVo mergeVo) { //获取purchaseId 为null还得新建 Long purchaseId = mergeVo.getPurchaseId(); if (purchaseId == null){ //采购单需要新建 PurchaseEntity purchaseEntity = new PurchaseEntity(); //给予创建时间和修改时间的默认值 purchaseEntity.setCreateTime(new Date()); purchaseEntity.setUpdateTime(new Date()); //采购单状态 purchaseEntity.setStatus(WareConstant.PuchaseStatusEnum.CREATED.getTypeCode()); this.save(purchaseEntity); //设置采购单Id purchaseId = purchaseEntity.getId(); } //有采购单id即purchaseId,检查采购单状态是否为0|1 不是则不允许合并 PurchaseEntity byId = this.getById(purchaseId); if (byId.getStatus() == WareConstant.PuchaseStatusEnum.CREATED.getTypeCode() || byId.getStatus() == WareConstant.PuchaseStatusEnum.ASSIGNED.getTypeCode()){ //采购单需求的采购单合并功能-本质就是一个修改 修改wms_purchase_detail表中 // id为items中遍历的元素值的purchase_id 和 status //获取要变更采购单的Id集合 List<Long> items = mergeVo.getItems(); //创建一个标记 即该集合是否可用的依据 Boolean isCorrect = true; for (Long item : items) { //查出当前Id对应的PurchaseDetailEntity PurchaseDetailEntity purDetailById = purchaseDetailService.getById(item); //根据Id查不到PurchaseDetailEntity或存在PurchaseDetailEntity.status的状态有问题直接结束方法 if (purDetailById == null || (purDetailById.getStatus() != WareConstant.PuchaseDetailStatusEnum.CREATED.getTypeCode() && purDetailById.getStatus() != WareConstant.PuchaseDetailStatusEnum.ASSIGNED.getTypeCode())){ System.out.println("不合理的purchaseDetailEntity:"+purDetailById); isCorrect = false; return; } } if (isCorrect){ //遍历并收集为purchaseDetailEntity集合 //System.out.println("通过了forEach 所有purchaseDetailEntity合理"); Long finalPurchaseId = purchaseId; List<PurchaseDetailEntity> purchaseDetailEntities = items.stream().map(i -> { PurchaseDetailEntity purchaseDetailEntity = new PurchaseDetailEntity(); purchaseDetailEntity.setId(i); //这里purchaseId必须是final类型的 //理由是局部内部类引用外部的变量时外部的变量默认变成final修饰 purchaseDetailEntity.setPurchaseId(finalPurchaseId); purchaseDetailEntity.setStatus(WareConstant.PuchaseDetailStatusEnum.ASSIGNED.getTypeCode()); return purchaseDetailEntity; }).collect(Collectors.toList()); //注入purchaseDetailService调用批量修改的方法 purchaseDetailService.updateBatchById(purchaseDetailEntities); //修改当前采购单的修改时间 if (purchaseId != null){ PurchaseEntity p = new PurchaseEntity(); p.setId(purchaseId); p.setUpdateTime(new Date()); this.updateById(p); } } } }
-
-
领取采购单
-
新增接口com.atguigu.gulimall.ware.controller.PurchaseController#reveived
/** * 新增接口 领取采购单 * post请求数据在请求体中 * 这里接收的是一个采购单id集合 */ @PostMapping("/reveived") public R reveived(@RequestBody List<Long> ids){ //这里细节不做考虑 如查出某员工自己未领取的采购单 // 员工只能领取属于自己的采购单等 //新增方法 purchaseService.reveived(ids); return R.ok(); }
-
实现方法com.atguigu.gulimall.ware.service.impl.PurchaseServiceImpl#reveived
/** * 领取采购单 ids采购单集合 * @param ids */ @Override public void reveived(List<Long> ids) { //1.确认当前采购单是新增或已分配的状态 List<PurchaseEntity> purchaseEntities = ids.stream().map(id -> { return this.getById(id); }).filter(item -> { //过滤状态 只留下新增或已分配的采购单 if (item.getStatus() == WareConstant.PuchaseStatusEnum.CREATED.getTypeCode() || item.getStatus() == WareConstant.PuchaseStatusEnum.ASSIGNED.getTypeCode()) { return true; } return false; }).map(item->{ //继续映射 改变状态 item.setStatus(WareConstant.PuchaseStatusEnum.RECEIVE.getTypeCode()); item.setUpdateTime(new Date()); return item; }).collect(Collectors.toList()); //2.改变采购单的状态 this.updateBatchById(purchaseEntities); //3.改变采购需求中采购项的状态 purchaseEntities.forEach(item->{ //注入purchaseDetailService并新增listDetailByPurchaseId方法 // 根据采购单ID即purchaseId获取采购需求列表 List<PurchaseDetailEntity> purchaseDetailEntities = purchaseDetailService.listDetailByPurchaseId(item.getId()); //更新采购需求状态 仅设置好采购需求Id和采购状态即可 List<PurchaseDetailEntity> collect = purchaseDetailEntities.stream().map(entity -> { PurchaseDetailEntity purchaseDetailEntity = new PurchaseDetailEntity(); purchaseDetailEntity.setStatus(WareConstant.PuchaseDetailStatusEnum.BUYING.getTypeCode()); purchaseDetailEntity.setId(entity.getId()); return purchaseDetailEntity; }).collect(Collectors.toList()); //批量更新采购需求的状态 非空判断 if (!CollectionUtils.isEmpty(collect)){ purchaseDetailService.updateBatchById(collect); } }); }
-
com.atguigu.gulimall.ware.service.impl.PurchaseDetailServiceImpl#listDetailByPurchaseId
/**
* 根据采购单ID即purchaseId获取采购需求列表
* @param id
* @return
*/
@Override
public List<PurchaseDetailEntity> listDetailByPurchaseId(Long id) {
List<PurchaseDetailEntity> purchaseDetailEntitiesByPurchaseId = this.list(new QueryWrapper<PurchaseDetailEntity>().eq("purchase_id", id));
return purchaseDetailEntitiesByPurchaseId;
}
-
使用postman测试领取采购单
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-C13EuzrU-1646456434683)(C:\Users\ck\AppData\Roaming\Typora\typora-user-images\1646207526264.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TsrkrKYF-1646456434684)(C:\Users\ck\AppData\Roaming\Typora\typora-user-images\1646207496089.png)]
-
完成采购
-
创建新的数据封装格式com.atguigu.gulimall.ware.vo.PurchaseDoneVo和com.atguigu.gulimall.ware.vo.PurchaseDoneItemVo
@Data public class PurchaseDoneVo { @NotNull private Long id;//采购单id private List<PurchaseDoneItemVo> items;//采购项集合 }
@Data public class PurchaseDoneItemVo { private Long itemId;//采购需求项ID private Integer status;//采购状态 0-4 private String reason;//采购失败的原因 }
-
创建接口方法com.atguigu.gulimall.ware.controller.PurchaseController#finish
/** * 新增接口 完成采购单 * post请求数据在请求体中 * 这里接收的是一个采购单id集合 */ @PostMapping("/done") public R finish(@RequestBody PurchaseDoneVo doneVo){ //新增方法 purchaseService.done(doneVo); return R.ok(); }
-
新增实现方法com.atguigu.gulimall.ware.service.impl.PurchaseServiceImpl#done
/** * 采购完成的处理 * @param doneVo */ @Transactional @Override public void done(PurchaseDoneVo doneVo) { //1.改变采购项|采购需求的状态 List<PurchaseDoneItemVo> items = doneVo.getItems(); //定义一个flag 一旦有一个采购项出错那么该采购单就认为采购失败 Boolean flag = true; //创建一个集合用于收集处理后的采购项 List<PurchaseDetailEntity> detailList = new ArrayList<>(); //遍历items分离出成功的采购项和失败的采购项目 for (PurchaseDoneItemVo item : items) { //创建采购项实体类 PurchaseDetailEntity purchaseDetailEntity = new PurchaseDetailEntity(); //采购项失败 if (item.getStatus() == WareConstant.PuchaseDetailStatusEnum.HASERROR.getTypeCode()){ //采购单采购失败 flag = false; //设置失败采购项状态 purchaseDetailEntity.setStatus(item.getStatus()); }else { //采购成功 //设置采购项状态 purchaseDetailEntity.setStatus(WareConstant.PuchaseDetailStatusEnum.FINISH.getTypeCode()); //2.采购成功的商品入库 //根据采购项id查询采购需求 用于后续获取sku_id ware_id stock PurchaseDetailEntity purDetailById = purchaseDetailService.getById(item.getItemId()); //注入wareSkuService并根据sku_id商品id,ware_id仓库id和stock新增库存给商品入库 wareSkuService.addStock(purDetailById.getSkuId(), purDetailById.getWareId(), purDetailById.getSkuNum()); } //设置采购项Id purchaseDetailEntity.setId(item.getItemId()); //添加进集合中 detailList.add(purchaseDetailEntity); } //批量更新采购项集合 if (!CollectionUtils.isEmpty(detailList)){ purchaseDetailService.updateBatchById(detailList); } //3.改变采购单状态 //获取采购单Id Long purchaseId = doneVo.getId(); PurchaseEntity purchaseEntity = new PurchaseEntity(); purchaseEntity.setId(purchaseId); purchaseEntity.setStatus(flag?WareConstant.PuchaseStatusEnum.FINISH.getTypeCode() :WareConstant.PuchaseStatusEnum.HASERROR.getTypeCode()); purchaseEntity.setUpdateTime(new Date()); this.updateById(purchaseEntity); }
-
新增wareSkuService.addStock方法 -Dao层接口略Mapper方法也略
这里获取商品名时远端调用了Product模块
/** * 根据skuId wareId skuNum更新库存 * @param skuId 商品Id * @param wareId 仓库Id * @param skuNum 新增商品数量 */ @Override public void addStock(Long skuId, Long wareId, Integer skuNum) { //判断库存中是否有该条记录 如果没有则新增 如果有就修改 List<WareSkuEntity> list = this.list(new QueryWrapper<WareSkuEntity>().eq("sku_id", skuId).eq("ware_id", wareId)); if (list == null || list.size() == 0){ //查无此记录 新增 WareSkuEntity wareSkuEntity = new WareSkuEntity(); wareSkuEntity.setSkuId(skuId); wareSkuEntity.setStock(skuNum); wareSkuEntity.setWareId(wareId); //默认不锁定库存 wareSkuEntity.setStockLocked(0); //注入商品服务的远程调用接口productFeignService //这里仅仅远程调用服务获取一个skuName 如果失败抛出异常后回滚得不偿失 //使用此处直接捕获一下异常保证不会仅因skuName获取失败就回滚 //TODO 还有另一种方法可以不回滚事务在高级篇优化 try { R r = productFeignService.info(skuId); // 商品服务传过来的有用数据是一个Map<String,Object> Map<String,Object> skuInfo = (Map<String, Object>) r.get("skuInfo"); if (r.getCode() == 0){ //查询成功 设置skuName wareSkuEntity.setSkuName((String) skuInfo.get("skuName")); } } catch (Exception e) { e.printStackTrace(); } this.baseMapper.insert(wareSkuEntity); }else { //修改库存 //自定义dao层方法 this.baseMapper.addStock(skuId,wareId,skuNum); } }
-
新增MP的配置类保证分页插件的正常使用 事务开启和Dao接口的扫描也从主启动类迁移过来
@Configuration @EnableTransactionManagement @MapperScan("com.atguigu.gulimall.ware.dao") public class WareMyBatisConfig { //引入分页插件 @Bean public PaginationInterceptor paginationInterceptor(){ PaginationInterceptor paginationInterceptor = new PaginationInterceptor(); //设置请求的页码大于最后一页后的操作 true则返回首页 false则继续请求 默认false paginationInterceptor.setOverflow(true); //设置单页最大限制数量 默认500条 -1不限制 paginationInterceptor.setLimit(1000); return paginationInterceptor; } }
-
第四步远端调用gulimall-product模块的方法的强调点
创建feign包及接口com.atguigu.gulimall.ware.entity.feign.ProductFeignService
主启动类开启feign-@EnableFeignClients,是否需要过网关的不同写法
@FeignClient("gulimall-gateway") public interface ProductFeignService { /** * 匹配的路径/product/skuinfo/info/{skuId} vs /api/product/skuinfo/info/{skuId} * 1.让所有的请求过网关(安全 负载均衡等) * 1)匹配的路径/api/product/skuinfo/info/{skuId} * 2)服务名@FeignClient("gulimall-gateway")给网关服务所在机器发送请求 * 2.直接让后台指定服务处理 * 1)匹配的路径/product/skuinfo/info/{skuId} * 2)服务名@FeignClient("gulimall-product")给商品服务所在机器发送请求 * @param skuId * @return */ //@RequestMapping("/product/skuinfo/info/{skuId}") @RequestMapping("/api/product/skuinfo/info/{skuId}") public R info(@PathVariable("skuId") Long skuId); }
-
用postman模拟采购发送的数据
-
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PUd2joGI-1646456434684)(C:\Users\ck\AppData\Roaming\Typora\typora-user-images\1646227808777.png)]
-
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-x51zyEwl-1646456434685)(C:\Users\ck\AppData\Roaming\Typora\typora-user-images\1646234302774.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-efbSPUs3-1646456434685)(C:\Users\ck\AppData\Roaming\Typora\typora-user-images\1646234345179.png)]
四、分布式基础篇总结
1.分布式基础概念
微服务–最大的特点每个微服务都是独立自治的,不同的功能模块可以抽成一个微服务让不同的开发人员并行开发,提高开发效率。
注册中心(Nacos)–项目拆分成各个微服务后,不同服务之间可能有互相调用的需求,这样就需要一个注册中心实时感知各个微服务的位置,在本服务调用其他微服务时就可以通过注册中心获取到其注册列表。
配置中心(Nacos)–不管是单体服务还是微服务,上线后都推荐使用配置中心,这样不需要通过改本地的源代码的配置文件后重新打包上线而可以通过一个可视化的配置中心界面在线修改配置文件且改完后微服务自动更新最新的配置信息。
**远程调用(Feign)–**开发阶段有远程调用其它服务的需求,这里通过Feign来给其它服务发送一个请求从而调用其它服务的功能。
使用Feign的前提:①导入feign的依赖 ②主启动类通过@EnableFeignClients开启远程调用的功能 ③远程服务也注册到注册中心(@EnableDiscoveryClient) ④编写一个远程服务的feign接口(标记@FeignClient(“远程服务名”))并注明远程服务的请求路径和控制器方法。
网关(gateway)–所有服务请求都要经过网关,并通过网关负载均衡到指定服务。也因此网关就可以做很多统一设置,比如前端给后端发送Ajax请求由于服务地址不同出现跨域问题,基础篇通过网关添加配置类设置CorsWebFiter解决了跨域问题。
2.基础开发
SpringBoot 2.0–基础篇暂时没有体现仅仅配置文件的一些配置信息名有改动。但是springboot2.0基于spring5引入了Reactor(反应式编程),Reactor带来的Webflux可以容易的创建一个高性能高并发的web应用。在网关服务配置跨域用到的CorsWebFiter使用了Webflux
**SpringCloud–**基础篇仅仅用了一些注解,如开启服务注册与发现@EnableDiscoveryClient,开启服务的远程调用@EnableFeignClients(basePackage=“feign包”)
**Mybatis-Plus–**仅仅设置了配置类,添加了分页插件需要的PaginationInterceptor,设置了包扫描@MapperScan(basePackage=“dao包”),以及开启事务@EnableTransactionManagement。
**Vue组件化–**前端使用了renren-fast-vue快速搭建前端框架,了解了Vue组件化开发的流程,使用了树形控件,对话框,级联选择器等ElementUI组件。
**阿里云对象存储OSS–**体会了第三方服务的调用过程
3.环境
VMware,Linux,Docker,Mysql,Redis,逆向工程&人人开源
4.开发规范
数据校验JSR303 、全局异常处理、全局统一返回、全局跨域处理
枚举状态、业务状态码、VO&TO&PO划分、逻辑删除–(整合MP,通过给实体类字段加上逻辑删除注解**@TableLogic** 且@TableLogic(value = “1”,delval = “0”)可以设置逻辑删除与不删除的值)
Lombok–@Data @Slf4j(log.error())
nService
//这里仅仅远程调用服务获取一个skuName 如果失败抛出异常后回滚得不偿失
//使用此处直接捕获一下异常保证不会仅因skuName获取失败就回滚
//TODO 还有另一种方法可以不回滚事务在高级篇优化
try {
R r = productFeignService.info(skuId);
// 商品服务传过来的有用数据是一个Map<String,Object>
Map<String,Object> skuInfo = (Map<String, Object>) r.get(“skuInfo”);
if (r.getCode() == 0){
//查询成功 设置skuName
wareSkuEntity.setSkuName((String) skuInfo.get(“skuName”));
}
} catch (Exception e) {
e.printStackTrace();
}
this.baseMapper.insert(wareSkuEntity);
}else {
//修改库存
//自定义dao层方法
this.baseMapper.addStock(skuId,wareId,skuNum);
}
}
```
5. 新增MP的配置类保证分页插件的正常使用 事务开启和Dao接口的扫描也从主启动类迁移过来
```java
@Configuration
@EnableTransactionManagement
@MapperScan("com.atguigu.gulimall.ware.dao")
public class WareMyBatisConfig {
//引入分页插件
@Bean
public PaginationInterceptor paginationInterceptor(){
PaginationInterceptor paginationInterceptor = new PaginationInterceptor();
//设置请求的页码大于最后一页后的操作 true则返回首页 false则继续请求 默认false
paginationInterceptor.setOverflow(true);
//设置单页最大限制数量 默认500条 -1不限制
paginationInterceptor.setLimit(1000);
return paginationInterceptor;
}
}
```
6. 第四步远端调用gulimall-product模块的方法的强调点
创建feign包及接口com.atguigu.gulimall.ware.entity.feign.ProductFeignService
主启动类开启feign-**@EnableFeignClients**,是否需要过网关的不同写法
```java
@FeignClient("gulimall-gateway")
public interface ProductFeignService {
/**
* 匹配的路径/product/skuinfo/info/{skuId} vs /api/product/skuinfo/info/{skuId}
* 1.让所有的请求过网关(安全 负载均衡等)
* 1)匹配的路径/api/product/skuinfo/info/{skuId}
* 2)服务名@FeignClient("gulimall-gateway")给网关服务所在机器发送请求
* 2.直接让后台指定服务处理
* 1)匹配的路径/product/skuinfo/info/{skuId}
* 2)服务名@FeignClient("gulimall-product")给商品服务所在机器发送请求
* @param skuId
* @return
*/
//@RequestMapping("/product/skuinfo/info/{skuId}")
@RequestMapping("/api/product/skuinfo/info/{skuId}")
public R info(@PathVariable("skuId") Long skuId);
}
```
7. 用postman模拟采购发送的数据
[外链图片转存中…(img-PUd2joGI-1646456434684)]
[外链图片转存中…(img-x51zyEwl-1646456434685)]
[外链图片转存中…(img-efbSPUs3-1646456434685)]
四、分布式基础篇总结
1.分布式基础概念
微服务–最大的特点每个微服务都是独立自治的,不同的功能模块可以抽成一个微服务让不同的开发人员并行开发,提高开发效率。
注册中心(Nacos)–项目拆分成各个微服务后,不同服务之间可能有互相调用的需求,这样就需要一个注册中心实时感知各个微服务的位置,在本服务调用其他微服务时就可以通过注册中心获取到其注册列表。
配置中心(Nacos)–不管是单体服务还是微服务,上线后都推荐使用配置中心,这样不需要通过改本地的源代码的配置文件后重新打包上线而可以通过一个可视化的配置中心界面在线修改配置文件且改完后微服务自动更新最新的配置信息。
**远程调用(Feign)–**开发阶段有远程调用其它服务的需求,这里通过Feign来给其它服务发送一个请求从而调用其它服务的功能。
使用Feign的前提:①导入feign的依赖 ②主启动类通过@EnableFeignClients开启远程调用的功能 ③远程服务也注册到注册中心(@EnableDiscoveryClient) ④编写一个远程服务的feign接口(标记@FeignClient(“远程服务名”))并注明远程服务的请求路径和控制器方法。
网关(gateway)–所有服务请求都要经过网关,并通过网关负载均衡到指定服务。也因此网关就可以做很多统一设置,比如前端给后端发送Ajax请求由于服务地址不同出现跨域问题,基础篇通过网关添加配置类设置CorsWebFiter解决了跨域问题。
2.基础开发
SpringBoot 2.0–基础篇暂时没有体现仅仅配置文件的一些配置信息名有改动。但是springboot2.0基于spring5引入了Reactor(反应式编程),Reactor带来的Webflux可以容易的创建一个高性能高并发的web应用。在网关服务配置跨域用到的CorsWebFiter使用了Webflux
**SpringCloud–**基础篇仅仅用了一些注解,如开启服务注册与发现@EnableDiscoveryClient,开启服务的远程调用@EnableFeignClients(basePackage=“feign包”)
**Mybatis-Plus–**仅仅设置了配置类,添加了分页插件需要的PaginationInterceptor,设置了包扫描@MapperScan(basePackage=“dao包”),以及开启事务@EnableTransactionManagement。
**Vue组件化–**前端使用了renren-fast-vue快速搭建前端框架,了解了Vue组件化开发的流程,使用了树形控件,对话框,级联选择器等ElementUI组件。
**阿里云对象存储OSS–**体会了第三方服务的调用过程
3.环境
VMware,Linux,Docker,Mysql,Redis,逆向工程&人人开源
4.开发规范
数据校验JSR303 、全局异常处理、全局统一返回、全局跨域处理
枚举状态、业务状态码、VO&TO&PO划分、逻辑删除–(整合MP,通过给实体类字段加上逻辑删除注解**@TableLogic** 且@TableLogic(value = “1”,delval = “0”)可以设置逻辑删除与不删除的值)
Lombok–@Data @Slf4j(log.error())