《谷粒商城》基础篇——后台系统功能的开发
一、商品三级分类(category)
1.数据模型
(1)表结构
商品分类信息对应的数据库表是gulimall_pms下的pms_category,各字段信息如下:
(2)导入数据
2.功能开发
(1)三级分类的查询、逻辑删除、添加
1)查询出所有商品分类及其子分类,并以树型结构组装起来
【难点】
- 注意一个递归查找子分类的方法
- stream流和lambda表达式的知识不牢
【controller】 /** * 查询出所有商品分类及其子分类,并以树型结构组装起来 */ @RequestMapping("/list/tree") //@RequiresPermissions("product:category:list") public R list(){ List<CategoryEntity> categoryEntities = categoryService.listWithTree(); return R.ok().put("data", categoryEntities); } ----------------------------------------------------------------------------------- 【serviceImpl】 @Override public List<CategoryEntity> listWithTree() { //1.查询所有商品分类信息 //根据查询条件获取多个分类信息,不给查询条件就是查询所有 List<CategoryEntity> categoryEntities = baseMapper.selectList(null); //★2.组装成父子的树型结构 List<CategoryEntity> level1Menus = categoryEntities.stream() //2.1 找所有的一级分类(父分类id parent_cid为0的就是一级分类) .filter(categoryEntity -> categoryEntity.getParentCid() == 0) //2.2 将该一级分类的所有子分类找到并放进去 .map((menu) -> { menu.setChildren(getChildren(menu, categoryEntities)); return menu; }) //2.3 给所有子分类排序 .sorted((menu1, menu2) -> { return (menu1.getSort() == null ? 0 : menu1.getSort()) - (menu2.getSort() == null ? 0 : menu2.getSort()); }) .collect(Collectors.toList()); return level1Menus; } /** ★ * 递归获取root的所有子分类(包括孙子分类) * @param root 父分类 * @param categoryEntities 所有的分类信息(就从这里面找root的子分类) * @return */ private List<CategoryEntity> getChildren(CategoryEntity root, List<CategoryEntity> categoryEntities) { List<CategoryEntity> children = categoryEntities.stream().filter((category) -> { return category.getParentCid() == root.getCatId(); }) .map((categoryEntity) -> { categoryEntity.setChildren(getChildren(categoryEntity, categoryEntities)); return categoryEntity; }) .sorted((menu1, menu2) -> { return (menu1.getSort() == null ? 0 : menu1.getSort()) - (menu2.getSort() == null ? 0 : menu2.getSort()); }) .collect(Collectors.toList()); return children; }2)★配置网关路由和路径重写
-
运行renren-fast,打开人人快速开发平台,在系统管理里添加【商品系统】,并添加【分类维护】(菜单url:/product/category),后期我们希望将上面得到的三级分类在分类维护里显示出来,方便对这些商品分类进行增删改查维护。
-
在VS Code中打开renren-fast-vue项目,在src→vuews→modules→sys下可以找到平台上系统管理里的所有前端页面,比如role.vue就是系统管理里的角色管理的前端页面。因此可以在modules下创建我们在上一步中添加的商品系统和分类维护的前端页面category.vue。
-
使用ElementUI中的【Tree 树形控件】来在分类管理中展示我们的三级分类数据。
-
我们如何从后台获取三级分类数据到前端呢?
由前端给gulimall-product服务(port=12000)发送请求(localhost:12000/product/category/list/tree),获取三级分类的数据。在category.vue中创建方法给后台项目发送请求获取数据,并设置什么时候调用该方法,这里我们让组件创建完成就调用它:
-
我们发现这样做仍达不到预期效果,点击平台的分类管理后会报错,可以看到发送的请求地址是localhost:8080/renren-fast/product/category/list/tree,而不是我们期望的localhost:12000/product/category/list/tree。这里有两处问题
一是这里是给localhost:8080/renren-fast发的请求,我们希望给12000发送请求;
二是如果我们想给其他端口的服务发送请求,是不是每次都要手动的去改这个基准路径?显然不是的,不能将请求地址写死。这里我们想到可以用网关来做路由,将前端的请求都发给网关,再根据路由规则路由到对应的服务。
- 将前端的请求都发给网关
-
在static/config下的index.js文件中,将前端的baseUrl修改为网关地址,这样由前端发送的请求就由baseUrl+adornUrl (getMenus方法中写的那个url) 组成了,现在调用getMenus方法发送的请求就是了:http://localhost:88/api/product/category/list/tree。
-
重新刷新开发平台后跳转到登录界面,又遇到一个问题:验证码不显示。原因是前端直接给网关发送了验证码请求,获取验证码应该发送请求给8080端口的renren-fast。
为解决这个问题,需要将renren-fast注册到注册中心(不再赘述),然后由网关将验证码请求路由给renren-fast。 -
★配置网关路由:在gulimall-gateway的配置文件中配置前端的请求路由规则
-
你以为这样就行了吗?no,还是不行!登录界面的验证码请求还是404。可以看到请求路径跟上面有了差别,网关看到88后面的api会将其路由到renren-fast,但是会转到renren-fast的哪呢?——http://renren-fast:8080/api/captcha.jpg。
而正确的验证码请求应该是localhost:8080/renren-fast/captcha.jpg。所以需要让网关将http://localhost:88/api/captcha.jpg转成正确的localhost:8080/renren-fast/captcha.jpg,这就要用到用到路径重写了。
-
★路径重写:在前端请求路由规则下继续配置路径重写
-
再次刷新登录界面,可以看到能正常加载验证码图片了。但是问题又来了,报错信息如下。CORS policy,CORS策略指的是跨域问题,浏览器为了安全起见,会默认拒绝这种跨域请求。
Access to XMLHttpRequest at 'http://localhost:88/api/sys/login' from origin 'http://localhost:8001' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource.
-
★网关统一配置跨域
①跨域:指的是浏览器不能执行其他网站的脚本,是由浏览器的同源策略造成的,是浏览器对JavaScript施加的安全限制。
②同源策略:指的是协议、域名、端口都要相同,有一个不同都会产生跨域问题:
③跨域的流程:
④解决跨域的方案:
a.将原属于不同域的请求使用nginx部署为同一域
b.配置当前请求允许跨域:可以给当前请求按需求添加以下响应头。因为我们是通过网关进行路由,所以可以在网关中配置filter来统一给请求添加响应头。
⑤★网关统一配置跨域
在gulimall-gateway中创建一个配置类,在该配置类中配置跨域。import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.reactive.CorsWebFilter; //注意别导错包,是带reactive的 import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource; @Configuration public class GulimallCorsConfigration { @Bean public CorsWebFilter corsWebFilter(){ //跨域的配置信息 UrlBasedCorsConfigurationSource source=new UrlBasedCorsConfigurationSource(); CorsConfiguration corsConfiguration=new CorsConfiguration(); //配置跨域信息 corsConfiguration.addAllowedHeader("*"); corsConfiguration.addAllowedMethod("*"); corsConfiguration.addAllowedOrigin("*"); corsConfiguration.setAllowCredentials(true); //注册跨域配置:哪些请求需要跨域,这里"/**"是所有请求都要跨域;按什么规则处理跨域,都配置在corsConfiguration里面。 source.registerCorsConfiguration("/**",corsConfiguration); return new CorsWebFilter(source); } }
重启网关服务后再刷新登录界面,发现可以正常登录进去了。同时可以看到确实有两个login请求——预检请求和真实请求。
3)树型展示三级分类数据
-
向gulimall-product服务发送请求,获取请求数据。同样是向网关发送请求:http://localhost:88/api/product/category/list/tree,通过网关路由到product服务。实现步骤:
①将gulimall-product注册到nacos
②配置网关路由和路径重写(http://localhost:88/api/product/category/list/tree → http://localhost:88/product/category/list/tree) -
将请求到的数据展示在前端页面:
-
后台删除功能的开发
实际开发中的删除功能实际上是逻辑删除,而不是真的把数据库中对应的信息删除了。(关于MP中逻辑删除的内容可以参考笔记:https://blog.nowcoder.net/n/6c7e11de8d96459fbfd738a967ada6c4或官方文档:https://baomidou.com/pages/6b03c5/)
配置好逻辑删除后调用categoryController中的delete就能实现后台分类信息的逻辑删除功能了。
-
前端删除功能的细化
确认删除提示框
删除成功提示框
删除后保持菜单展开
5)商品分类新增功能
-
后端新增功能:使用逆向工程生成的save方法
-
前端新增功能的开发
新增对话框的弹出
新增成功提示框
新增成功后刷新列表并默认展开其父分类
6)商品分类修改功能
- 后端批量修改功能:使用逆向工程生成的updateBatchById方法
-
前端修改功能的开发
为每个分类增加修改按钮
★修改对话框需要回显待修改的分类信息
节点拖拽效果
拖拽数据的收集
二、品牌(brand)管理
1.数据模型
(1)表结构
(2)导入数据
2.功能开发
(1)使用逆向工程生成的前端vue,来实现前端基本的增删改查功能。
(2)效果优化与快速显示开关
-
修改表头的显示状态
-
快速切换显示状态的开关
(3)★文件上传功能介绍
在分布式项目中,如果第一次访问时将某文件上传到了A服务器,而下次再访问却负载均衡到了没有该文件的B服务器,这样就获取不到该文件了。该如何解决这个问题呢?
解决分布式文件存储问题的方案是将文件统一存储到一台专门存储文件的C服务器中,这样无论负载均衡到哪台服务器,需要文件时中需要从C服务器中获取文件即可。
解决分布式文件存储问题的方案是将文件统一存储到一台专门存储文件的C服务器中,这样无论负载均衡到哪台服务器,需要文件时中需要从C服务器中获取文件即可。
这里介绍如何使用阿里云对象存储(OSS)服务。
- 开通阿里云OSS服务
- 阅读帮助文档:https://help.aliyun.com/document_detail/31947.html?spm=5176.8465980.help.dexternal.41231450U0vRIq
- 创建新的Bucket存储空间(按服务创建)——这里我们创建测试bucket,起名gulimall-nanjing
-
上传文件:
- 先通过应用服务器,再向OSS上传数据
-
采用【服务端签名后直传】的方式【推荐】
- 查看JavaSDK文档:https://help.aliyun.com/document_detail/32008.htm?spm=a2c4g.11186623.0.0.7e1b7663Zuwiw2#concept-32008-zh进行快速入门的学习。
(4)文件上传——先通过应用服务器,再向OSS上传数据
上传文件原生API快速入门:
-
1)安装SDK:导入依赖坐标
<dependency> <groupId>com.aliyun.oss</groupId> <artifactId>aliyun-sdk-oss</artifactId> <version>3.5.0</version> </dependency>
2)简单上传文件流:
①获取要上传的存储空间的endpoint:gulimall-nanjing → 概览 → endpoint-外网访问
②创建专门管理OSS的子用户账号
③代码// Endpoint以华东1(杭州)为例 // 以上传到我们创建的gulimall-nanjing这个存储空间为例,在概览里找到该bucket的EndPoint:oss-cn-nanjing.aliyuncs.com String endpoint = "oss-cn-nanjing.aliyuncs.com"; // 创建并使用RAM用户进行API访问或日常运维 String accessKeyId = "LTAI5t9ecLvSMfuxXTadUHJy"; String accessKeySecret = "qxofcVRRFdYXvFY6Div1UnkVn1IYTz"; // 创建OSSClient实例。 OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret); // 要上传的文件的本地目录。如果未指定本地路径,则默认从示例程序所属项目对应本地路径中上传文件流。 String filePath= "C:\\Users\\AdminZyb\\Desktop\\test\\pic.jpg"; InputStream inputStream = new FileInputStream(filePath); // 创建PutObject请求。 // 填写Bucket名称 String bucketName="gulimall-nanjing"; // 填写Object完整路径,完整路径中不能包含Bucket名称 String objectName="pic.jpg"; // 上传文件流 ossClient.putObject(bucketName, objectName, inputStream); // 关闭OSSClient ossClient.shutdown();
-
★使用SpringCloudAlibaba提供的组件进行文件上传
1)导入依赖坐标:<dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alicloud-oss</artifactId> </dependency>
2)在yml文件中配置OSS服务对应的accessKey、secretKey和endpoint:spring: cloud: alicloud: access-key: xxx secret-key: xxx oss: endpoint: oss-cn-nanjing.aliyuncs.com
3)自动注入OSSClient并进行文件上传下载等操作:@Service public class YourService { @Autowired private OSS ossClient; public void saveFile() { // download file to local ossClient.getObject(new GetObjectRequest(bucketName, objectName), new File("pathOfYourLocalFile")); } }
(5)文件上传——服务端签名后直传
服务端签名后直传是基于Post Policy的使用规则在服务端通过java代码完成签名,然后通过表单直传数据到OSS。也就是说我们是通过http请求的方式向服务端发送一个获取签名的请求,这也是我们在项目中创建OSSController的原因。
在此之前,我们先创建一个新的模块专门进行第三方服务的操作——gulimall-third-party。
-
配置应用服务器
- 配置客户端
-
获取服务端签名
-
在gateway中配置OSS路由规则
- id: third_party_route uri: lb://gulimall-third-party predicates: - Path=/api/thirdparty/** filters: - RewritePath=/api/thirdparty/?(?<segment>.*), /$\{segment}
-
创建OSSController
@RestController public class OSSController { @Autowired OSS ossClient; @Value("${spring.cloud.alicloud.oss.endpoint}") private String endpoint; @Value("${spring.cloud.alicloud.access-key}") private String accessId; @RequestMapping("/oss/policy") public Map<String, String> policy() { // 填写Bucket名称,例如examplebucket。 String bucket = "gulimall-nanjing"; // 填写Host地址,即给谁上传文件,格式为https://bucketname.endpoint。 String host = "https://" + bucket + "." + endpoint; // 设置上传到OSS文件的前缀,可置空此项。置空后,文件将上传至Bucket的根目录下。 String format = new SimpleDateFormat("yyyy-MM-dd").format(new Date()); String dir = format + "/"; Map<String, String> respMap =null; try { long expireTime = 30; long expireEndTime = System.currentTimeMillis() + expireTime * 1000; Date expiration = new Date(expireEndTime); PolicyConditions policyConds = new PolicyConditions(); policyConds.addConditionItem(PolicyConditions.COND_CONTENT_LENGTH_RANGE, 0, 1048576000); policyConds.addConditionItem(MatchMode.StartWith, PolicyConditions.COND_KEY, dir); String postPolicy = ossClient.generatePostPolicy(expiration, policyConds); byte[] binaryData = postPolicy.getBytes("utf-8"); String encodedPolicy = BinaryUtil.toBase64String(binaryData); String postSignature = ossClient.calculatePostSignature(postPolicy); respMap = new LinkedHashMap<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)); } catch (Exception e) { System.out.println(e.getMessage()); } return respMap; } }
-
向服务端发送请求获取签名
-
在gateway中配置OSS路由规则
-
前端vue(略):
<el-form-item label="品牌logo地址" prop="logo"> <!-- <el-input v-model="dataForm.logo" placeholder="品牌logo地址"></el-input> --> <!-- 使用导入的SingleUpload组件实现文件上传 --> <SingleUpload v-model="dataForm.logo"></SingleUpload> </el-form-item> <script> import SingleUpload from "@/components/upload/singleUpload.vue" export default { components:{ SingleUpload }, .........
-
★设置bucket的跨域权限,让浏览器能给oss对象存储发数据
-
在平台中中点击新增按钮,品牌logo地址变为“点击上传”了
-
试了一下上传文件成功
-
前端:
修改显示状态:显示为1,隐藏为0,通过active/inactive-value实现
更新前端项目使用的组件库——为了使用image组件,来在前端页面中显示logo图片
表单校验功能:默认的表单校验只是进行了不能为空的校验,而我们希望更细致的校验,比如排序只能为数字。
①Form 组件提供了表单验证的功能,只需要通过 rules 属性传入约定的验证规则,并将 Form-Item 的 prop 属性设置为需校验的字段名即可。
②自定义校验规则:在表单验证的基础上,可以使用自定义校验器validator。
-
后端:前端校验只能保证前端发送给后端的数据是正确的格式或者内容,为避免有人绕开前端校验(如使用postman)直接向后台服务器发送错误请求,因此还需要进行后端校验的开发。使用 JSR303 数据校验标准来实现后端数据校验。
①给需要校验的字段标注校验规则
②在controller中使用@Valid注解开启校验
③使用postman绕过前端校验发送错误的数据——让品牌名为空,并获取返回的消息{ "timestamp": "2022-11-28T11:33:51.313+0000", "status": 400, # 错误代码400 "error": "Bad Request", "errors": [ { "codes": [ "NotBlank.brandEntity.name", "NotBlank.name", "NotBlank.java.lang.String", "NotBlank" ], "arguments": [ { "codes": [ "brandEntity.name", "name" ], "arguments": null, "defaultMessage": "name", "code": "name" } ], "defaultMessage": "品牌名不能为空", # 错误信息 "objectName": "brandEntity", # 出错的实体类 "field": "name", # 出错字段 "rejectedValue": "", # 填的错误数据是个空串 "bindingFailure": false, "code": "NotBlank" # 校验规则,不能为空 } ], "message": "Validation failed for object='brandEntity'. Error count: 1", "path": "/product/brand/save" }
④我们之前是统一用R来返回响应信息,现在我们需要把上面的有用信息提取出来,还是用R返回:可以在要校验的实体类后面,紧跟着实体类用一个BindingResult,这个类里面就有被校验实体类出错时的错误信息(也就是上面那些信息),然后我们可以通过.hasErrors来判断是否出错,并获取到想要的数据,封装到R中返回。最终再校验出错时,响应到的数据如右图所示。
⑤自定义校验规则:使用@Pattern的正则表达式来自定义校验规则。同时同一字段可以被多个校验规则修饰。
-
★数据校验失败的统一异常处理
使用SpringBoot提供的@ControllerAdvice异常处理器来统一处理项目出现的所有异常。
①创建异常处理类GulimallExceptionControllerAdvice(注意一下几个注解的使用)
//@RestControllerAdvice = @ResponseBody + @ControllerAdvice @RestControllerAdvice(basePackages = "com.zyb.gulimall.product.controller")//basePackages = "com.zyb.gulimall.product.controller" : 规定了哪些地方的异常需要这个异常处理类来处理 public class GulimallExceptionControllerAdvice {
②针对不同的异常类型定义对应的异常处理方法
-
JSR303——分组校验
分组校验允许我们在不同场景采用不同的校验规则,比如同一字段 新增时 和 修改时 的校验规则可能不同,此时就可以进行分组。
①给校验注解实用groups属性标注什么情况下采用该校验注解进行校验
②开启校验的注解就不能用@Valid了,因为他不能分组,要改用@Validated(value={})来实现分组校验
-
★自定义校验注解
自定义校验规则除可以使用正则表达式的@Pattern外,还可以使用自定义的校验注解:
①创建自定义的校验注解
②创建自定义的校验器ConstraintValidator
③关联校验注解与校验器
(8)品牌管理的分页功能开发
如下图,现在我们品牌管理的页面不能正确显示分页数据。
-
我们使用MP提供的分页插件来实现品牌管理的分页功能。创建一个MP配置类,在该配置类中配置分页插件:
@Configuration @MapperScan("com.zyb.gulimall.product.dao") @EnableTransactionManagement//开启事务 public class mybatisPlusConfig { @Bean public PaginationInterceptor paginationInterceptor() { PaginationInterceptor paginationInterceptor = new PaginationInterceptor(); //设置请求的页面大于最大页后的操作,true调回到首页,false继续请求。 默认false paginationInterceptor.setOverflow(true); //设置最大单页限制数量,默认500条,-1不受限制 paginationInterceptor.setLimit(1000); return paginationInterceptor; } }
现在就可以正常显示分页数据了:
修改BrandServiceImpl中的queryPage方法:
1)获取与某品牌关联的分类信息
@Override public PageUtils queryPage(Map<String, Object> params) { String key = (String) params.get("key"); QueryWrapper<BrandEntity> wrapper = new QueryWrapper<>(); if(!StringUtils.isEmpty(key)){ wrapper.eq("brand_id",key).or().like("name",key); } IPage<BrandEntity> page = this.page( new Query<BrandEntity>().getPage(params), wrapper ); return new PageUtils(page); }(10)品牌brand 与 分类category 的关联功能
1)获取与某品牌关联的分类信息
-
参照接口文档,可以看到获取品牌关联的分类需要发送的请求路径、请求参数和响应数据:
-
按文档要求创建对应的controller方法:
@GetMapping("/catelog/list") public R catelogList(@RequestParam("brandId") Long brandId){ List<CategoryBrandRelationEntity> data =categoryBrandRelationService.list(new QueryWrapper<CategoryBrandRelationEntity>() .eq("brand_id",brandId)); return R.ok().put("data", data); }
2)保存与某品牌关联的分类信息 -
查文档:
-
创方法:
//controller /** * 保存品牌关联分类信息 */ @PostMapping("/save") //@RequiresPermissions("product:categorybrandrelation:save") public R save(@RequestBody CategoryBrandRelationEntity categoryBrandRelation){ categoryBrandRelationService.saveDetail(categoryBrandRelation); return R.ok(); } //service @Override public void saveDetail(CategoryBrandRelationEntity categoryBrandRelation) { Long brandId = categoryBrandRelation.getBrandId(); Long catelogId = categoryBrandRelation.getCatelogId(); BrandEntity brand = brandService.getById(brandId); CategoryEntity category = categoryService.getById(catelogId); categoryBrandRelation.setBrandName(brand.getName()); categoryBrandRelation.setCatelogName(category.getName()); this.save(categoryBrandRelation); }
3)级联更新
-
在数据库中存在冗余字段,即多个表中都存在该字段,那么我们在一张表中修改该字段的数据时,要保证同步更新所有关联表中该字段的数据。这就叫级联更新。现在我们修改品牌名(如把华为→华为1),但是华为的关联分类中的品牌名没有修改(还是叫华为,而不是华为1):
-
实现品牌的级联更新:
//BrandController /** * 级联修改 */ @RequestMapping("/update") public R update(@Validated(value={UpdateGroup.class}) @RequestBody BrandEntity brand){ brandService.updateDetail(brand); return R.ok(); } //BrandService @Override public void updateDetail(BrandEntity brand) { //先更新自己的表 this.updateById(brand); //再更新关联的表 if(!StringUtils.isEmpty(brand.getName())){ categoryBrandRelationService.updateBrand(brand.getBrandId(),brand.getName()); } } //CategoryBrandRelationService @Override public void updateBrand(Long brandId, String name) { CategoryBrandRelationEntity categoryBrandRelationEntity = new CategoryBrandRelationEntity(); categoryBrandRelationEntity.setBrandId(brandId); categoryBrandRelationEntity.setBrandName(name); this.update(categoryBrandRelationEntity,new UpdateWrapper<CategoryBrandRelationEntity>().eq("brand_id", brandId)); }
-
实现分类的级联更新:
同上。
三、商品的平台属性
商品的平台属性包括:属性分组、规格参数(基本属性)和销售属性。我们希望实现的功能是,点击对应的菜单,可以显示出商品的三级分类信息,以及商品对应的属性分组(或规格参数、或销售属性)信息。
1.更新前端展示菜单
人人开发平台上的菜单栏都是由下图的gulimall_admin数据库管理的,这里我们把《谷粒商城》项目所有要用到的菜单一次性创建好。运行资料中的sql文件,平台菜单栏就变成了这样:
2.平台属性——属性分组的开发
(1)前端组件的抽取及使用
-
将商品的三级分类抽取为一个公共组件:
-
将抽取的category.vue导入attrgroup.vue中使用:
-
前端页面显示的效果:
(2)父子组件交互
我们想要实现点击左边三级分类中的某一分类,如手机,能在右边的表格中显示出手机分类的相关属性信息。但是现在三级分类是我们抽取出来的category组件,而表格写在attrgroup里,该如何实现二者数据的传递呢?
这就要用到 父子组件交互 功能了。在attrgroup中导入了category组件,attrgroup就是父,category就是子。
这就要用到 父子组件交互 功能了。在attrgroup中导入了category组件,attrgroup就是父,category就是子。
-
父子组件传递数据问题:
-
子给父传递数据:通过事件机制来实现:
-
子组件给父组件发送一个携带数据的事件;
-
父组件感知子组件发送过来的事件:
-
子组件给父组件发送一个携带数据的事件;
- 父给子传递数据:
-
子给父传递数据:通过事件机制来实现:
(3)根据点击的分类节点获取对应的属性分组信息
-
后端:修改对应controller中的list方法,改为请求中携带参数catelogId:
重写对应的serviceImpl中的分页查询方法:
-
前端:修改查询时的请求路径:
更新catId并重新获取表格数据:
(4)属性分组的新增功能
-
前端:级联选择器的使用:
3.平台属性——规格参数
(1)新增规格参数功能的开发
- 新增规格参数时会发送携带右图的参数,而AttrEntity中是没有attrGroupId这个字段的,此时调用逆向工程生成的save方法只能将除了attrGroupId字段外的其他数据存入pms_attr这张表中,而此时的attrGroupId字段没有存入数据库。
-
之前我们的做法是在AttrEntity新增一个attrGroupId字段,并用@TableField(exist = false)标注这是我们自定义的属性,表中无该字段。这样做其实不符合规范,我们使用vo对象来抽取AttrEntity中的所有字段以及自定义的字段,避免了大量注解的使用。这样就可以用AttrVo代替AttrEntity来接收数据了。因此我们在save方法中的参数就不是AttrEntity而是AttrVo了。
@Data public class AttrVo { private String attrName; private Integer valueType; private String icon; private String valueSelect; private Integer attrType; private Long enable; private Long catelogId; private Integer showDesc; /** * 分组id */ private Long attrGroupId; }
-
基于逆向生成的save方法定义一个新的saveAttr方法:
@Override public void saveAttr(AttrVo attr) { //将AttrEntity中的数据存到pms_attr表中 AttrEntity attrEntity = new AttrEntity(); BeanUtils.copyProperties(attr,attrEntity); this.save(attrEntity); //将attrGroupId存入pms_attr_attrgroup_relation AttrAttrgroupRelationEntity attrGroup=new AttrAttrgroupRelationEntity(); attrGroup.setAttrGroupId(attr.getAttrGroupId()); attrGroup.setAttrId(attrEntity.getAttrId()); relationService.save(attrGroup); }
-
定义一个queryBaseAttrPage方法,根据请求中携带的catelogId以及是否有key来查询规格参数。同样地,在原来的AttrEntity中没有groupName和categoryName字段,我们定义一个AttrRespVo来获取分组名和分类名并返回数据。
@Data public class AttrRespVo extends AttrVo{ /** * 所属分类名 */ private String catelogName; /** * 所属分组名 */ private String groupName; }
@Override public PageUtils queryBaseAttrPage(Map<String, Object> params, Long catelogId) { QueryWrapper<AttrEntity> wrapper = new QueryWrapper<>(); //不是查询所有 if (catelogId != 0) { wrapper.eq("catelog_id", catelogId); } //查询所有 String key = (String) params.get("key"); if (!StringUtils.isEmpty(key)) { wrapper.and((obj) -> { obj.eq("attr_id", key).or().like("attr_name", key); }); } IPage<AttrEntity> page = this.page( new Query<AttrEntity>().getPage(params), wrapper ); //设置分组名和分类名 List<AttrEntity> records = page.getRecords(); List<AttrRespVo> respVos = records.stream().map((attrEntity) -> { AttrRespVo attrRespVo = new AttrRespVo(); BeanUtils.copyProperties(attrEntity, attrRespVo); //设置分组名 AttrAttrgroupRelationEntity attrId = relationService.getOne(new QueryWrapper<AttrAttrgroupRelationEntity>().eq("attr_id",attrEntity.getAttrId())); if (attrId != null) { AttrGroupEntity attrGroup = attrGroupService.getById(attrId.getAttrGroupId()); attrRespVo.setGroupName(attrGroup.getAttrGroupName()); } //设置分类名 CategoryEntity categoryEntity = categoryService.getById(attrEntity.getCatelogId()); if (categoryEntity != null) { attrRespVo.setCatelogName(categoryEntity.getName()); } return attrRespVo; }).collect(Collectors.toList()); PageUtils pageUtils = new PageUtils(page); pageUtils.setList(respVos); return pageUtils; }
(3)修改规格参数
-
点击修改按钮,现在无法回显所属分类和所属分组的信息,通过接口文档也可以发现,查询请求的响应数据应该包括分组id和分类的完整路径,所以我们先解决通过修改查询属性详情的方法来解决数据回显的问题。
-
将catelogPath字段加到AttrRespVo里,在controller中使用自定义的getAttrInfo方法查询属性信息:
//AttrController @RequestMapping("/info/{attrId}") public R info(@PathVariable("attrId") Long attrId){ AttrRespVo attrRespVo = attrService.getAttrInfo(attrId); return R.ok().put("attr", attrRespVo); } //AttrService @Override public AttrRespVo getAttrInfo(Long attrId) { AttrRespVo attrRespVo = new AttrRespVo(); AttrEntity attrEntity = this.getById(attrId); BeanUtils.copyProperties(attrEntity, attrRespVo); //设置分组信息 AttrAttrgroupRelationEntity relationEntity = relationService.getById(attrId); if (relationEntity != null) { attrRespVo.setAttrGroupId(relationEntity.getAttrGroupId()); AttrGroupEntity attrGroupEntity = attrGroupService.getById(relationEntity.getAttrGroupId()); if (attrGroupEntity != null) { attrRespVo.setGroupName(attrGroupEntity.getAttrGroupName()); } } //设置分类信息 Long[] catelogPath = categoryService.findCatelogPath(attrEntity.getCatelogId()); attrRespVo.setCatelogPath(catelogPath); CategoryEntity categoryEntity = categoryService.getById(attrEntity.getCatelogId()); if (categoryEntity != null) { attrRespVo.setCatelogName(categoryEntity.getName()); } return attrRespVo; }
-
解决了数据回显问题后点击确定,发现并没有真正的修改数据,这是因为我们还没有修改 修改规格参数的update方法:
@Override public void updateAttr(AttrVo attr) { AttrEntity attrEntity = new AttrEntity(); BeanUtils.copyProperties(attr,attrEntity); this.updateById(attrEntity); AttrAttrgroupRelationEntity attrAttrgroupRelationEntity = new AttrAttrgroupRelationEntity(); attrAttrgroupRelationEntity.setAttrId(attr.getAttrId()); attrAttrgroupRelationEntity.setAttrGroupId(attr.getAttrGroupId()); int count = relationService.count(new QueryWrapper<AttrAttrgroupRelationEntity>().eq("attr_id", attr.getAttrId())); if(count>0){//修改操作 relationService.update(attrAttrgroupRelationEntity,new UpdateWrapper<AttrAttrgroupRelationEntity>().eq("attr_id",attr.getAttrId())); }else{//新增操作 relationService.save(attrAttrgroupRelationEntity); } }
4.平台属性——销售属性
(1)获取分类销售属性
-
获取分类销售属性跟获取分类的规格参数类似,这里参考文档发送的是请求,而获取分类的规格参数发送的是请求,只差在base和sale上,因此我们可以复用之前获取分类规格参数的list方法,根据请求的是base还是sale来区分是获取什么属性。
①修改controller,添加一个type参数,根据type来区分是base还是sale:
②修改baseList方法:我们在每次进行查询时,都需要加上attr_type的判断,如果请求传来的type参数是base,查询条件就是attr_type=1,即查询到的都是规格参数信息;反之就是attr_type=0,查询到的就是销售属性信息。
(2)新增分类销售属性
-
新增销售属性和新增规格参数都是一样的,新增时通过属性类型的选择新增不同的信息即可。
5.平台属性——属性分组关联基本属性
(1)查询指定属性分组关联的所有基本属性
-
//AttrGroupController @GetMapping("/{attrgroupId}/attr/relation") public R attrRelation(@PathVariable("attrgroupId") Long attrgroupId){ List<AttrEntity> attrEntities = attrService.getRelationAttr(attrgroupId); return R.ok().put("data",attrEntities); } //AttrService @Override public List<AttrEntity> getRelationAttr(Long attrgroupId) { List<AttrAttrgroupRelationEntity> relationEntities = relationService.list(new QueryWrapper<AttrAttrgroupRelationEntity>().eq("attr_group_id", attrgroupId)); List<Long> attrIds = relationEntities.stream().map((attr) -> { return attr.getAttrId(); }).collect(Collectors.toList()); Collection<AttrEntity> attrEntities = this.listByIds(attrIds); return (List<AttrEntity>) attrEntities; }
(2)批量删除属性与分组的关联关系
-
//AttrGroupController @PostMapping("/attr/relation/delete") public R deleteRelation(@RequestBody AttrGroupRelationVo[] vos) { attrService.deleteRelationAttr(vos); return R.ok(); } //AttrService @Override public void deleteRelationAttr(AttrGroupRelationVo[] vos) { List<AttrAttrgroupRelationEntity> entities = Arrays.asList(vos).stream().map((vo) -> { AttrAttrgroupRelationEntity relationEntity = new AttrAttrgroupRelationEntity(); BeanUtils.copyProperties(vo, relationEntity); return relationEntity; }).collect(Collectors.toList()); relationService.deleteBatchRelation(entities); } //AttrAttrgroupRelationService @Override public void deleteBatchRelation(List<AttrAttrgroupRelationEntity> entities) { for (AttrAttrgroupRelationEntity entity : entities) { this.remove(new QueryWrapper<AttrAttrgroupRelationEntity>().eq("attr_id",entity.getAttrId()).eq("attr_group_id",entity.getAttrGroupId())); } }
(3)查询分组未关联的基本属性
-
点击新建关联时,我们希望能够查询到当前属性分组未关联的基本属性(即可以关联哪些属性)。
//AttrGroupController @GetMapping("/{attrgroupId}/noattr/relation") public R attrNoRelation(@RequestParam Map<String, Object> params, @PathVariable("attrgroupId") Long attrgroupId) { PageUtils page = attrService.getNoRelationAttr(params, attrgroupId); return R.ok().put("page", page); } //AttrService @Override public PageUtils getNoRelationAttr(Map<String, Object> params, Long attrgroupId) { //1.当前分组只能关联自己所属分类下的所有属性 //1.1 获取当前分组所属的分类id AttrGroupEntity attrGroupEntity = attrGroupService.getById(attrgroupId); Long catelogId = attrGroupEntity.getCatelogId(); //2.当前分组只能关联别的分组没有关联的属性 //2.1 获取当前分类下的所有分组 List<AttrGroupEntity> attrGroupEntityList = attrGroupService.list(new QueryWrapper<AttrGroupEntity>().eq("catelog_id", catelogId)); //获取所有分组的id List<Long> attrGroupIds = attrGroupEntityList.stream().map((entity) -> { return entity.getAttrGroupId(); }).collect(Collectors.toList()); //2.2 获取这些分组关联的属性 List<AttrAttrgroupRelationEntity> attrList = relationService.list(new QueryWrapper<AttrAttrgroupRelationEntity>().in("attr_group_id", attrGroupIds)); //获取属性id List<Long> attrIds = attrList.stream().map((entity) -> { return entity.getAttrId(); }).collect(Collectors.toList()); //2.3 从当前分类的所有属性中提出2.2中的这些属性 QueryWrapper<AttrEntity> wrapper = new QueryWrapper<AttrEntity>().eq("catelog_id", catelogId).eq("attr_type", ProductConstant.AttrEnum.ATTR_TYPE_BASE.getCode()); if (attrIds != null && attrIds.size() > 0) { wrapper.notIn("attr_id", attrIds); } String key = (String) params.get("key"); if (!StringUtils.isEmpty(key)) { wrapper.and((w) -> { w.eq("attr_id", key).or().like("attr_name", key); }); } IPage<AttrEntity> page = this.page(new Query<AttrEntity>().getPage(params), wrapper); PageUtils pageUtils = new PageUtils(page); return pageUtils; }
(4)新增分组与属性的关联关系
-
@PostMapping("/attr/relation") public R addRelation(@RequestBody List<AttrGroupRelationVo> relationVo){ relationService.addRelation(relationVo); return R.ok(); } @Override public void addRelation(List<AttrGroupRelationVo> relationVo) { List<AttrAttrgroupRelationEntity> relationEntities = relationVo.stream().map((vo) -> { AttrAttrgroupRelationEntity relationEntity = new AttrAttrgroupRelationEntity(); BeanUtils.copyProperties(vo,relationEntity); return relationEntity; }).collect(Collectors.toList()); this.saveBatch(relationEntities); }
四、发布商品
1.调试会员等级相关接口
- 检查member服务是否注册到nacos
-
编写gateway中的路由规则
- id: member_route uri: lb://gulimall-member predicates: - Path=/api/member/** filters: - RewritePath=/api/(?<segment>.*),/$\{segment}
- 将逆向生成的member服务vue页面导入到前端项目的modules中
-
测试:
2.获取分类关联的品牌信息
-
//CategoryBrandRelationController @GetMapping("/brands/list") public R relationBrandsList(@RequestParam("catId") Long catId){ List<BrandEntity> brandEntities = categoryBrandRelationService.getBrands(catId); List<BrandVo> brandVos = brandEntities.stream().map((brand) -> { BrandVo brandVo = new BrandVo(); brandVo.setBrandId(brand.getBrandId()); brandVo.setBrandName(brand.getName()); return brandVo; }).collect(Collectors.toList()); return R.ok().put("data", brandVos); } //CategoryBrandRelationService @Override public List<BrandEntity> getBrands(Long catId) { List<CategoryBrandRelationEntity> relationEntities = categoryBrandRelationService.list(new QueryWrapper<CategoryBrandRelationEntity>().eq("catelog_id", catId)); List<BrandEntity> brandEntities = relationEntities.stream().map((entity) -> { Long brandId = entity.getBrandId(); BrandEntity brandEntity = brandService.getById(brandId); return brandEntity; }).collect(Collectors.toList()); return brandEntities; }
3.获取某个分类下的所有分组及关联属性
-
//AttrGroupController @GetMapping("/{catelogId}/withattr") public R getAttrGroupWithAttrs(@PathVariable("catelogId") Long catelogId) { List<AttrGroupWithAttrsVo> attrGroupRelationVos = attrGroupService.getAttrGroupWithAttrsByCatelogId(catelogId); return R.ok().put("data", attrGroupRelationVos); } //AttrGroupService @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((attrGroup) -> { AttrGroupWithAttrsVo attrGroupWithAttrsVo = new AttrGroupWithAttrsVo(); BeanUtils.copyProperties(attrGroup, attrGroupWithAttrsVo); List<AttrEntity> attrs = attrService.getRelationAttr(attrGroupWithAttrsVo.getAttrGroupId()); attrGroupWithAttrsVo.setAttrs(attrs); return attrGroupWithAttrsVo; }).filter(attrGroup -> attrGroup.getAttrs() != null).collect(Collectors.toList()); return vos; }
4.新增商品
(1)抽取vo
- 我们新增商品后会将如下json发送给后端,这样在控制台看这种数据很长的json不方便,我们可以使用json在线解析工具(https://www.json.cn/json/json2java.html)来解析复杂的json。
-
使用json在线解析工具逆向生成vo接收上面的json数据
-
将下载的压缩包解压,复制到pruduct项目下的vo
(2)商品新增业务流程分析
从上面可以看到,在发布一个新的商品时,需要保存的数据很多,涉及到多张表以及跨服务操作表,所以在开发新增商品功能之前,我们先分析一下商品新增的业务流程:
(3)保存spu的信息(1-4步)
-
@Transactional @Override public void saveSpuInfo(SpuSaveVo spuSaveVo) { //1.保存spu基本信息——pms_spu_info SpuInfoEntity spuInfoEntity = new SpuInfoEntity(); BeanUtils.copyProperties(spuSaveVo,spuInfoEntity); spuInfoEntity.setCreateTime(new Date()); spuInfoEntity.setUpdateTime(new Date()); this.saveSpuBaseInfo(spuInfoEntity); //2.保存spu的描述图片——pms_spu_info_desc List<String> decript = spuSaveVo.getDecript(); SpuInfoDescEntity descEntity = new SpuInfoDescEntity(); descEntity.setSpuId(spuInfoEntity.getId()); descEntity.setDecript(String.join(",",decript)); descService.saveSpuDescInfo(descEntity); //3.保存spu的图片集——pms_spu_images List<String> images = spuSaveVo.getImages(); imagesService.saveImages(spuInfoEntity.getId(),images); //4.保存spu的规格参数——pms_product_attr_value List<BaseAttrs> baseAttrs = spuSaveVo.getBaseAttrs(); List<ProductAttrValueEntity> attrValueEntities = baseAttrs.stream().map((baseAttr) -> { ProductAttrValueEntity productAttrValueEntity = new ProductAttrValueEntity(); productAttrValueEntity.setAttrId(baseAttr.getAttrId()); AttrEntity attrEntity = attrService.getById(baseAttr.getAttrId()); productAttrValueEntity.setAttrName(attrEntity.getAttrName()); productAttrValueEntity.setAttrValue(baseAttr.getAttrValues()); productAttrValueEntity.setQuickShow(baseAttr.getShowDesc()); productAttrValueEntity.setSpuId(spuInfoEntity.getId()); return productAttrValueEntity; }).collect(Collectors.toList()); productAttrValueService.saveProductAttr(attrValueEntities); //。。。 。。。 }
(4)保存当前spu对应的所有sku信息(不跨服务)
-
//。。。 。。。 //6、保存当前spu对应的所有sku信息: List<Skus> skus = spuSaveVo.getSkus(); if(skus!=null&&skus.size()>0){ skus.forEach((sku)->{ SkuInfoEntity skuInfoEntity = new SkuInfoEntity(); BeanUtils.copyProperties(sku,skuInfoEntity); skuInfoEntity.setBrandId(spuInfoEntity.getBrandId()); skuInfoEntity.setCatalogId(spuInfoEntity.getCatalogId()); skuInfoEntity.setSaleCount(0L); skuInfoEntity.setSpuId(spuInfoEntity.getId()); String defaultImage=""; for (Images image : sku.getImages()) { if(image.getDefaultImg()==1){ defaultImage=image.getImgUrl(); } } skuInfoEntity.setSkuDefaultImg(defaultImage); //6.1 sku的基本信息;pms_sku_info skuInfoService.saveSkuInfo(skuInfoEntity); Long skuId = skuInfoEntity.getSkuId(); List<SkuImagesEntity> imagesEntities = sku.getImages().stream().map(img -> { SkuImagesEntity skuImagesEntity = new SkuImagesEntity(); skuImagesEntity.setSkuId(skuId); skuImagesEntity.setImgUrl(img.getImgUrl()); skuImagesEntity.setDefaultImg(img.getDefaultImg()); return skuImagesEntity; }).collect(Collectors.toList()); //6.2 sku的图片信息;pms_sku_image skuImagesService.saveBatch(imagesEntities); //6.3 sku的销售属性信息:pms_sku_sale_attr_value List<Attr> attr = sku.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 //。。。 。。。 }); }
(5)★调用远程服务保存spu的积分信息、sku的优惠等信息
服务A要想远程调用服务B的步骤:
1)保存spu的积分信息——product远程调用coupon的"/coupon/spubounds/save"请求路径的方法
- ①导入坐标spring-cloud-starter-openfeign
- ②在服务A的启动类上添加注释开启Feign功能
- ③在服务A中编写Feign客户端接口(用来基于 SpringMVC 的注解 @GetMapping 来声明远程调用的信息)
- ④服务A调用服务B的某个方法
1)保存spu的积分信息——product远程调用coupon的"/coupon/spubounds/save"请求路径的方法
-
在product中编写Feign客户端接口
@FeignClient("gulimall-coupon")//被调用者的服务名 public interface CouponFeignClient { @PostMapping("/coupon/spubounds/save")//要调用的方法的请求路径及请求方式 R saveSpuBounds(@RequestBody SpuBoundsTo spuBoundsTo); }
-
在product的SpuInfoService的方法中调用上面的saveSpuBounds方法
@Autowired CouponFeignClient couponFeignClient; //5.保存spu的积分信息:gulimall_sms库下的:sms_spu_bounds Bounds bounds = spuSaveVo.getBounds(); SpuBoundsTo spuBoundsTo = new SpuBoundsTo(); BeanUtils.copyProperties(bounds,spuBoundsTo); spuBoundsTo.setSpuId(spuInfoEntity.getId()); couponFeignClient.saveSpuBounds(spuBoundsTo);
2)保存sku的优惠、满减等信息——product远程调用coupon的"/coupon/skufullreduction/saveinfo"请求路径的方法
-
Feign接口:
@FeignClient("gulimall-coupon")//被调用者的服务名 public interface CouponFeignClient { @PostMapping("/coupon/spubounds/save") R saveSpuBounds(@RequestBody SpuBoundsTo spuBoundsTo); @PostMapping("/coupon/skufullreduction/saveinfo") R saveSkuReduction(@RequestBody SkuReductionTo skuReductionTo); }
-
product的SpuInfoService:
//6.4 sku的优惠、满减等信息;gulimall_sms->sms_sku_ladder\sms_sku_full_reduction\sms_member_price SkuReductionTo skuReductionTo = new SkuReductionTo(); BeanUtils.copyProperties(sku, skuReductionTo); skuReductionTo.setSkuId(skuId); R r1 = couponFeignClient.saveSkuReduction(skuReductionTo); if(r1.getCode()!=0){ log.error("远程保存sku优惠、满减等信息失败"); }
-
coupon的SkuFullReductionService:
/** * 保存sku的优惠、满减等信息 * @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()); if (skuReductionTo.getFullCount() > 0) { skuLadderService.save(skuLadderEntity); } //2、sms_sku_full_reduction SkuFullReductionEntity reductionEntity = new SkuFullReductionEntity(); BeanUtils.copyProperties(skuReductionTo, reductionEntity); if (reductionEntity.getFullPrice().compareTo(new BigDecimal("0")) == 1) { this.save(reductionEntity); } //3、sms_member_price List<MemberPrice> memberPrice = skuReductionTo.getMemberPrice(); List<MemberPriceEntity> collect = memberPrice.stream().map(item -> { MemberPriceEntity priceEntity = new MemberPriceEntity(); priceEntity.setSkuId(skuReductionTo.getSkuId()); priceEntity.setMemberLevelId(item.getId()); priceEntity.setMemberLevelName(item.getName()); priceEntity.setMemberPrice(item.getPrice()); priceEntity.setAddOther(1); return priceEntity; }).filter(item -> { return item.getMemberPrice().compareTo(new BigDecimal("0")) == 1; }).collect(Collectors.toList()); memberPriceService.saveBatch(collect); }
五、商品维护
1.spu检索
(1)页面效果
(2)请求参数
(3)代码实现
@RequestMapping("/list") public R list(@RequestParam Map<String, Object> params){ PageUtils page = spuInfoService.queryPageByCondition(params); return R.ok().put("page", page); } @Override public PageUtils queryPageByCondition(Map<String, Object> params) { QueryWrapper<SpuInfoEntity> wrapper = new QueryWrapper<>(); 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("catalog_id",catelogId); } IPage<SpuInfoEntity> page = this.page( new Query<SpuInfoEntity>().getPage(params), wrapper ); return new PageUtils(page); }
2.sku检索
sku检索跟spu检索差不多,就是注意价格区间的sql语句就行。@RequestMapping("/list") public R list(@RequestParam Map<String, Object> params){ PageUtils page = skuInfoService.queryPageByCondition(params); return R.ok().put("page", page); } @Override public PageUtils queryPageByCondition(Map<String, Object> params) { QueryWrapper<SkuInfoEntity> queryWrapper = new QueryWrapper<>(); String key = (String) params.get("key"); if(!StringUtils.isEmpty(key)){ queryWrapper.and((wrapper)->{ wrapper.eq("sku_id",key).or().like("sku_name",key); }); } String catelogId = (String) params.get("catelogId"); if(!StringUtils.isEmpty(catelogId)&&!"0".equalsIgnoreCase(catelogId)){ queryWrapper.eq("catalog_id",catelogId); } String brandId = (String) params.get("brandId"); if(!StringUtils.isEmpty(brandId)&&!"0".equalsIgnoreCase(catelogId)){ queryWrapper.eq("brand_id",brandId); } String min = (String) params.get("min"); if(!StringUtils.isEmpty(min)){ queryWrapper.ge("price",min); } String max = (String) params.get("max"); if(!StringUtils.isEmpty(max) ){ try{ BigDecimal bigDecimal = new BigDecimal(max); 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); }
3.spu规格维护
(1)获取spu规格
-
@GetMapping("/base/listforspu/{spuId}") public R listForSpu(@PathVariable("spuId") Long spuId){ List<ProductAttrValueEntity> entities = productAttrValueService.listForSpu(spuId); return R.ok().put("data",entities); } @Override public List<ProductAttrValueEntity> listForSpu(Long spuId) { List<ProductAttrValueEntity> list = this.list(new QueryWrapper<ProductAttrValueEntity>().eq("spu_id", spuId)); return list; }
(2)修改规格
-
@PostMapping("/update/{spuId}") public R updateSpu(@RequestBody List<ProductAttrValueEntity> productAttrValueEntities,@PathVariable("spuId") Long spuId){ productAttrValueService.updateSpu(productAttrValueEntities,spuId); return R.ok(); } @Transactional @Override public void updateSpu(List<ProductAttrValueEntity> productAttrValueEntities, Long spuId) { //删除spuid对应的所有属性 this.remove(new QueryWrapper<ProductAttrValueEntity>().eq("spu_id", spuId)); //新增修改后的所有属性 List<ProductAttrValueEntity> entities = productAttrValueEntities.stream().map(entity -> { entity.setSpuId(spuId); return entity; }).collect(Collectors.toList()); this.saveBatch(entities); }
六、库存系统——前期准备
1.将gulimall-ware服务注册到nacos
-
在yml中配置nacos注册中心信息
Spring: cloud: nacos: discovery: server-addr: localhost:8848 # nacos服务地址 application: name: gulimall-ware
- 在启动类上开启注册发现功能:@EnableDiscoveryClient
2.配置gateway路径
-
- id: ware_route uri: lb://gulimall-ware predicates: - Path=/api/ware/** filters: - RewritePath=/api/(?<segment>.*), /$\{segment}
3.将ware的前端vue导入项目的modules
现在可以正常访问到开发平台的仓库系统了,同时基于逆向工程生成的增删改查功能,都是能正常使用的。
七、仓库系统相关功能开发
1.仓库维护——获取仓库列表
逆向生成的获取仓库列表功能是不带模糊查询的,我们需要修改一下controller的方法,实现模糊查询。
@Override public PageUtils queryPage(Map<String, Object> params) { QueryWrapper<WareInfoEntity> wrapper = new QueryWrapper<>(); String key = (String) params.get("key"); if(!StringUtils.isEmpty(key)){ wrapper.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), wrapper ); return new PageUtils(page); }
2.采购单维护
(1)采购需求——模糊检索
@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(wareId)) { wrapper.eq("ware_id", wareId); } IPage<PurchaseDetailEntity> page = this.page( new Query<PurchaseDetailEntity>().getPage(params), wrapper ); return new PageUtils(page); }(2)采购需求——合并采购需求
-
采购流程
-
查询未领取的采购单
@Override public PageUtils queryPageUnreceiveList(Map<String, Object> params) { IPage<PurchaseEntity> page = this.page( new Query<PurchaseEntity>().getPage(params), new QueryWrapper<PurchaseEntity>().eq("status",0).or().eq("status",1) ); return new PageUtils(page); }
-
合并采购需求
需要先在common中创建枚举和在ware模块中自定义Vo类,这里略过。
@Transactional @Override public void mergePurchase(MergeVo mergeVo) { //得到要合并的整单id Long purchaseId = mergeVo.getPurchaseId(); //得到要合并的项的id List<Long> items = mergeVo.getItems(); //如果整单id为空 if(purchaseId == null){ //1、新建一个整单entity PurchaseEntity purchaseEntity = new PurchaseEntity(); //设置整单状态 purchaseEntity.setStatus(WareConstant.PurchaseStatusEnum.CREATED.getCode()); //设置时间 purchaseEntity.setCreateTime(new Date()); purchaseEntity.setUpdateTime(new Date()); //保存这个实体类 this.save(purchaseEntity); //保存后拿到其id purchaseId = purchaseEntity.getId(); } Long finalPurchaseId = purchaseId; //对每个项的id进行处理 List<PurchaseDetailEntity> collect = items.stream().map(i -> { //新建一个PurchaseDetailEntity PurchaseDetailEntity detailEntity = new PurchaseDetailEntity(); //设置id detailEntity.setId(i); //设置整单的id detailEntity.setPurchaseId(finalPurchaseId); //设置状态 detailEntity.setStatus(WareConstant.PurchaseDetailStatusEnum.ASSIGNED.getCode()); return detailEntity; }).collect(Collectors.toList()); //集体更新PurchaseDetailEntity detailService.updateBatchById(collect); //更新整单时间 PurchaseEntity purchaseEntity = new PurchaseEntity(); purchaseEntity.setId(purchaseId); purchaseEntity.setUpdateTime(new Date()); this.updateById(purchaseEntity); }
(3)领取采购单
-
模拟业务员领取采购整单——使用postman模拟向后台发送请求
-
@Override public void receivedPurchase(List<Long> ids) {//传来的是采购整单的ids //1、确认当前采购单是新建或者已分配状态 List<PurchaseEntity> collect = ids.stream().map(id -> { //通过ids得到每一个整单的实体类 PurchaseEntity byId = this.getById(id); return byId; }).filter(item -> { //只有新建和已分配状态的采购整单才会被采购人员领取 if (item.getStatus() == WareConstant.PurchaseStatusEnum.CREATED.getCode() || item.getStatus() == WareConstant.PurchaseStatusEnum.ASSIGNED.getCode()) { return true; } return false; }).map(item->{ //把采购单的状态设置为已经被领取 item.setStatus(WareConstant.PurchaseStatusEnum.RECEIVE.getCode()); item.setUpdateTime(new Date()); return item; }).collect(Collectors.toList()); //2、改变采购单的状态 this.updateBatchById(collect); //3、改变采购需求里的状态 collect.forEach((item)->{ List<PurchaseDetailEntity> entities = detailService.listDetailByPurchaseId(item.getId()); List<PurchaseDetailEntity> detailEntities = entities.stream().map(entity -> { PurchaseDetailEntity entity1 = new PurchaseDetailEntity(); entity1.setId(entity.getId()); entity1.setStatus(WareConstant.PurchaseDetailStatusEnum.BUYING.getCode()); return entity1; }).collect(Collectors.toList()); detailService.updateBatchById(detailEntities); }); }
-
使用postman模拟
-
@Transactional @Override public void donePurchase(PurchaseDoneVo doneVo) { //拿到采购单的Id Long id = doneVo.getId(); //2、改变采购项的状态 Boolean flag = true; //得到vo的项 //这里面有PurchaseDetail的id List<PurchaseItemDoneVo> items = doneVo.getItems(); List<PurchaseDetailEntity> updates = new ArrayList<>(); for (PurchaseItemDoneVo item : items) { //创建对应实体类 PurchaseDetailEntity detailEntity = new PurchaseDetailEntity(); //如果任意一项item状态存在异常 if (item.getStatus() == WareConstant.PurchaseDetailStatusEnum.HASERROR.getCode()) { //设置flag为false flag = false; //设置其状态 detailEntity.setStatus(item.getStatus()); } else { //状态没有异常 为对应实体类设置状态 detailEntity.setStatus(WareConstant.PurchaseDetailStatusEnum.FINISH.getCode()); //3、将成功采购的进行入库 PurchaseDetailEntity entity = detailService.getById(item.getItemId());//????????????????????? wareSkuService.addStock(entity.getSkuId(), entity.getWareId(), entity.getSkuNum()); } detailEntity.setId(item.getItemId());//???????????????? updates.add(detailEntity); } detailService.updateBatchById(updates); //1、改变采购单状态 PurchaseEntity purchaseEntity = new PurchaseEntity(); purchaseEntity.setId(id); purchaseEntity.setStatus(flag ? WareConstant.PurchaseStatusEnum.FINISH.getCode() : WareConstant.PurchaseStatusEnum.HASERROR.getCode()); purchaseEntity.setUpdateTime(new Date()); this.updateById(purchaseEntity); }
3.商品库存
(1)查询商品库存
@Override public PageUtils queryPage(Map<String, Object> params) { QueryWrapper<WareSkuEntity> wrapper = new QueryWrapper<>(); //wareId: 123,仓库id //skuId: 123,商品id String wareId = (String) params.get("wareId"); if (!StringUtils.isEmpty(wareId)) { wrapper.eq("ware_id", wareId); } String skuId = (String) params.get("skuId"); if (!StringUtils.isEmpty(skuId)) { wrapper.eq("sku_id", skuId); } IPage<WareSkuEntity> page = this.page( new Query<WareSkuEntity>().getPage(params), wrapper ); return new PageUtils(page); }
八、基础篇总结
1.技术栈
2.总结
基础篇主要是根据接口文档(https://easydoc.net/s/78237135/ZUqEdvA4/hKJTcbfd)对使用人人开源逆向工程生成的商城后台系统的相关功能(商品系统、库存系统)进行了开发。大部分是前端和后端业务逻辑的开发,同时解决了全局跨域等问题,学习如何debug调试解决问题。