前序准备
将gulimall-ware注册到nacos,修改配置文件,主启动类加上@EnableDiscoveryClient和@EnableTransactionManagement注解。
配置网关,新增路由规则
# 要放在renren-fast的转发之前- id: ware_routeuri: lb://gulimall-warepredicates:- Path=/api/gulimallware/**filters:- RewritePath=/api/(?<segment>.*), /$\{segment}
一、仓库维护
这里需要完善条件查询
@Overridepublic 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 查询sku库存
https://easydoc.net/s/78237135/ZUqEdvA4/hwXrEXBZ 
进入商品库存页面会发送/waresku/list给后台,这里我们需要完善条件查询,直接在原来的方法上修改。
@Overridepublic PageUtils queryPage(Map<String, Object> params) {/*** skuId: 1* wareId: 2*/QueryWrapper<WareSkuEntity> wrapper = new QueryWrapper<>();String skuId = (String) params.get("skuId");if (!StringUtils.isEmpty(skuId)) {wrapper.eq("sku_id", skuId);}String wareId = (String) params.get("wareId");if (!StringUtils.isEmpty(wareId)) {wrapper.eq("ware_id", wareId);}IPage<WareSkuEntity> page = this.page(new Query<WareSkuEntity>().getPage(params),wrapper);return new PageUtils(page);}
2.2 新增sku库存(先完成采购单维护)
新增库存不是直接在商品库存页面进行新增,而是通过采购。所以我们需要先完成采购的相关功能。
三、采购单维护
3.1 采购需求

采购需求来自两方面:
- 人工添加的采购需求
-
3.1.1 查询采购需求
https://easydoc.net/s/78237135/ZUqEdvA4/Ss4zsV7R 查询wms_purchase_detail表,前端会发送查询条件,需要进行条件查询。
@Overridepublic PageUtils queryPage(Map<String, Object> params) {/*** key: '华为',//检索关键字* status: 0,//状态* wareId: 1,//仓库id*/QueryWrapper<PurchaseDetailEntity> wrapper = new QueryWrapper<>();String key = (String) params.get("key");// and (purchase_id = key or sku_id = 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);}
3.1.2 合并采购需求
新增采购单
在合并采购单之前,需要新增采购单。


此时前端会发送请求http://localhost:88/api/gulimallware/purchase/unreceive/list?t=1647226496404 查询新建的和已分配的采购单(采购单有五种状态:新建、已分配、已领取、已完成、有异常五种状态;其中只有新建和已分配的采购单能够对采购内容进行修改)。
https://easydoc.net/s/78237135/ZUqEdvA4/cUlv9QvK
/*** 查询新建和已分配状态的采购单*/// /ware/purchase/unreceive/list@GetMapping("/unreceive/list")public R unreceiveList(@RequestParam Map<String, Object> params) {PageUtils page = purchaseService.queryPageUnreceivePurchase(params);return R.ok().put("page", page);}
/*** 查询新建和已分配状态的采购单* @param params* @return*/@Overridepublic PageUtils queryPageUnreceivePurchase(Map<String, Object> params) {// 采购单的status必须是0(新建)或者是1(已分配)IPage<PurchaseEntity> page = this.page(new Query<PurchaseEntity>().getPage(params),new QueryWrapper<PurchaseEntity>().eq("status", 0).or().eq("status", 1));return new PageUtils(page);}
采购单分配给采购人员
合并采购单的时候,展示的应该是采购单的编号和采购人员,所以需要给采购单分配采购人员。
合并采购需求
https://easydoc.net/s/78237135/ZUqEdvA4/cUlv9QvK
当选定采购单时,会将采购需求合并到目标采购单;若没有选定采购单,则新建采购单。
新建一个MergeVo接收前端传过来的数据(采购单ID和采购计划ID)
package com.atguigu.gulimall.gulimallware.vo;@Datapublic class MergeVo {private Long purchaseId;private List<Long> items;}
/*** 合并采购需求*/// /ware/purchase/merge@PostMapping("/merge")public R merge(@RequestBody MergeVo vo) {purchaseService.mergePurchase(vo);return R.ok();}
因为采购单和采购计划都有状态属性,新建WareConstant类,存放采购单与采购计划的状态枚举类
package com.atguigu.common.constant;public class WareConstant {public enum PurchaseStatusEnum {CREATED(0, "新建状态"), ASSIGNED(1, "已分配"),RECEIVE(2, "已领取"), FINISH(3, "已完成"),ERROR(4, "有异常");private int code;private String message;PurchaseStatusEnum(int code, String message) {this.code = code;this.message = message;}public int getCode() {return code;}public String getMessage() {return message;}}public enum PurchaseDetailStatusEnum {CREATED(0, "新建状态"), ASSIGNED(1, "已分配"),BUYING(2, "正在采购"), FINISH(3, "已完成"),FAIL(4, "采购失败");private int code;private String message;PurchaseDetailStatusEnum(int code, String message) {this.code = code;this.message = message;}public int getCode() {return code;}public String getMessage() {return message;}}}
方法声明&实现
@AutowiredPurchaseDetailServiceImpl purchaseDetailService;/*** 合并采购需求到采购单;若指定采购单,则合并到指定采购单,反之新建采购单合并* @param vo*/@Transactional@Overridepublic void mergePurchase(MergeVo vo) {Long purchaseId = vo.getPurchaseId();if (purchaseId == null) { //没有指定采购单,那么需要新建一个采购单PurchaseEntity purchaseEntity = new PurchaseEntity();purchaseEntity.setStatus(WareConstant.PurchaseStatusEnum.CREATED.getCode()); // 设置采购单状态 采购单状态使用一个枚举类purchaseEntity.setCreateTime(new Date()); // 设置新建时间purchaseEntity.setUpdateTime(new Date()); // 设置修改时间this.save(purchaseEntity);purchaseId = purchaseEntity.getId();}List<Long> purchaseDetailIds = vo.getItems(); // 获取需要合并到采购单的采购计划idLong finalPurchaseId = purchaseId;// 更新每个id对应的采购计划的属性List<PurchaseDetailEntity> purchaseDetailEntities = purchaseDetailIds.stream().map(item -> {PurchaseDetailEntity purchaseDetailEntity = new PurchaseDetailEntity();purchaseDetailEntity.setId(item);purchaseDetailEntity.setPurchaseId(finalPurchaseId); // 更新采购计划属于哪个采购单purchaseDetailEntity.setStatus(WareConstant.PurchaseDetailStatusEnum.ASSIGNED.getCode()); // 更新状态return purchaseDetailEntity;}).collect(Collectors.toList());purchaseDetailService.updateBatchById(purchaseDetailEntities); // 批量更新采购计划// 更新采购单的更新时间PurchaseEntity purchaseEntity = new PurchaseEntity();purchaseEntity.setId(purchaseId);purchaseEntity.setUpdateTime(new Date());this.updateById(purchaseEntity);}
3.2 领取采购单
采购单分配以后,被分配人员应该可以看到自己被分配到的采购单,然后领取;领取完成后,采购单的状态会改为已领取。同时,已经被领取的采购单将无法再合并采购需求,同时已经合并的采购需求状态变为正在采购中。(领取采购单不属于前端的操作,我们通过postman来模拟手机上领取采购单)
https://easydoc.net/s/78237135/ZUqEdvA4/vXMBBgw1 领取采购单api文档
/*** 领取采购单* @param purchaseIds* @return*/// /ware/purchase/received@PostMapping("/received")public R receivedPurchase(@RequestBody List<Long> purchaseIds) {purchaseService.receivedPurchase(purchaseIds);return R.ok();}
/*** 领取采购单* @param purchaseIds 采购单Id集合*/@Overridepublic void receivedPurchase(List<Long> purchaseIds) {// 1、确认当前采购单是新建或者已分配状态List<PurchaseEntity> purchaseEntities = purchaseIds.stream().map(id -> {// 找出purchaseId对应的采购单实体PurchaseEntity purchase = this.getById(id);return purchase;}).filter(item -> { // 选择已分配的采购单boolean isValiable = item.getStatus() == WareConstant.PurchaseStatusEnum.ASSIGNED.getCode();return isValiable;}).map(purchase -> { // 更新采购单的状态为已领取purchase.setStatus(WareConstant.PurchaseStatusEnum.RECEIVE.getCode()); // 状态为已领取purchase.setUpdateTime(new Date()); // 更新时间return purchase;}).collect(Collectors.toList());// 2、数据库中改变采购单的状态为已领取this.updateBatchById(purchaseEntities);// 3、改变采购需求的状态为采购中purchaseEntities.forEach(item -> {Long purchaseId = item.getId(); // 获取当前采购单Id// 找出所有purchase_id对应的采购需求List<PurchaseDetailEntity> detailEntityList = purchaseDetailService.listDetailByPurchaseId(purchaseId);// 更新采购需求的状态List<PurchaseDetailEntity> detailEntities = detailEntityList.stream().map(detail -> {PurchaseDetailEntity entity = new PurchaseDetailEntity();entity.setId(detail.getId());entity.setStatus(WareConstant.PurchaseDetailStatusEnum.BUYING.getCode());return entity;}).collect(Collectors.toList());purchaseDetailService.updateBatchById(detailEntities);});}
/*** 通过采购单Id查询当前采购单下的所有采购计划* @param purchaseId* @return*/@Overridepublic List<PurchaseDetailEntity> listDetailByPurchaseId(Long purchaseId) {QueryWrapper<PurchaseDetailEntity> wrapper = new QueryWrapper<>();wrapper.eq("purchase_id", purchaseId);List<PurchaseDetailEntity> purchaseDetailEntities = this.list(wrapper);return purchaseDetailEntities;}
3.3 完善合并采购需求(视频没写)
确认采购单状态是0,1的时候才能合并采购需求。这里我是按自己思路写的,视频里面没写这些。
我让mergePurchase方法返回一个boolean类项的值,以此来判断合并是否成功。
/*** 合并采购需求*/// /ware/purchase/merge@PostMapping("/merge")public R merge(@RequestBody MergeVo vo) {boolean mergePurchase = false;mergePurchase = purchaseService.mergePurchase(vo);if (mergePurchase) {return R.ok();}return R.error(404, "所选采购单状态不合法");}
/*** 合并采购需求到采购单;若指定采购单,则合并到指定采购单,反之新建采购单合并* @param vo*/@Transactional@Overridepublic boolean mergePurchase (MergeVo vo) {Long purchaseId = vo.getPurchaseId(); // 获取前端传送的采购单Idif (purchaseId == null) { //没有指定采购单,那么需要新建一个采购单PurchaseEntity purchaseEntity = new PurchaseEntity();purchaseEntity.setStatus(WareConstant.PurchaseStatusEnum.CREATED.getCode()); // 设置采购单状态 采购单状态使用一个枚举类purchaseEntity.setCreateTime(new Date()); // 设置新建时间purchaseEntity.setUpdateTime(new Date()); // 设置修改时间this.save(purchaseEntity);purchaseId = purchaseEntity.getId();}// TODO 确认采购单的状态是0或者1才可以合并Integer status = this.getById(purchaseId).getStatus();// 获取当前采购单的状态if (status < WareConstant.PurchaseStatusEnum.RECEIVE.getCode()) { // 只有当采购单的状态是新建 或 已分配时,才能够合并采购计划List<Long> purchaseDetailIds = vo.getItems(); // 获取需要合并到采购单的采购需求idLong finalPurchaseId = purchaseId; // 采购单Id// 更新每个id对应的采购计划的属性List<PurchaseDetailEntity> purchaseDetailEntities = purchaseDetailIds.stream().map(item -> {PurchaseDetailEntity purchaseDetailEntity = new PurchaseDetailEntity();purchaseDetailEntity.setId(item);purchaseDetailEntity.setPurchaseId(finalPurchaseId); // 更新采购计划属于哪个采购单purchaseDetailEntity.setStatus(WareConstant.PurchaseDetailStatusEnum.ASSIGNED.getCode()); // 更新状态return purchaseDetailEntity;}).collect(Collectors.toList());purchaseDetailService.updateBatchById(purchaseDetailEntities); // 批量更新采购计划// 更新采购单的更新时间PurchaseEntity purchaseEntity = new PurchaseEntity();purchaseEntity.setId(purchaseId);purchaseEntity.setUpdateTime(new Date());this.updateById(purchaseEntity);return true;}return false;}
3.4 完成采购
完成采购后,采购需求将会添加到库存中去。 https://easydoc.net/s/78237135/ZUqEdvA4/cTQHGXbK
整个业务逻辑分为三个步骤:
- 改变采购项的状态
- 改变采购单的状态(如果采购需求项存在异常,那么采购单异常)
- 将采购成功的采购需求入库
新建Vo类型class,用于接收前端数据
@Datapublic class PurchaseItemDoneVo {private Long itemId; //采购需求Idprivate Integer status; // 采需求状态private String reason; // 采购失败原因}
Mybatis配置类 (把MapperScan跟 @EnableTransactionManagement【开启事务支持】放到这边来,其实无所谓) 这里分页配置我是新版配置,跟视频里面的不一样,参考官网
@Configuration@EnableTransactionManagement@MapperScan("com.atguigu.gulimall.gulimallware.dao")public class WareMyBatisConfig {@Beanpublic MybatisPlusInterceptor mybatisPlusInterceptor() {MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();PaginationInnerInterceptor paginationInnerInterceptor = new PaginationInnerInterceptor(DbType.MYSQL);// 设置请求的页面大于最大页后操作, true调回到首页,false 继续请求 默认falsepaginationInnerInterceptor.setOverflow(true);// 设置最大单页限制数量,默认 500 条,-1 不受限制paginationInnerInterceptor.setMaxLimit(1000L);// optimizeJoin字段设置(默认为true)是否生成 countSql 优化掉 join 现在只支持 left joinpaginationInnerInterceptor.setOptimizeJoin(true);interceptor.addInnerInterceptor(paginationInnerInterceptor);return interceptor;}}
/*** 完成采购单*/@PostMapping("/done")public R finish(@RequestBody PurchaseDoneVo doneVo) {purchaseService.done(doneVo);return R.ok();}
@AutowiredWareSkuService wareSkuService;/*** 完成采购单*/@Overridepublic void done(PurchaseDoneVo doneVo) {// 1、改变采购需求状态Boolean flag = true; // 记录采购项是否都正常List<PurchaseItemDoneVo> items = doneVo.getItems(); // 获取采购项List<PurchaseDetailEntity> updates = new ArrayList<>();for (PurchaseItemDoneVo item : items) {PurchaseDetailEntity detailEntity = new PurchaseDetailEntity();if (item.getStatus() == WareConstant.PurchaseDetailStatusEnum.FAIL.getCode()) {flag = false;} else { // 3、采购项采购成功,将成功采购的入库// 通过id查出当前采购项的详细信息PurchaseDetailEntity byId = purchaseDetailService.getById(item.getItemId());// 将当前sku的skuId 仓库Id 以及采购数量传过去wareSkuService.addStock(byId.getSkuId(), byId.getWareId(), byId.getSkuNum());}// PurchaseDetailEntity实体类封装需要改变的statusdetailEntity.setStatus(item.getStatus());detailEntity.setId(item.getItemId());updates.add(detailEntity);}purchaseDetailService.updateBatchById(updates);// 2、改变采购单状态Long purchaseId = doneVo.getId();PurchaseEntity purchase = new PurchaseEntity();purchase.setId(purchaseId);purchase.setStatus(flag ? WareConstant.PurchaseStatusEnum.FINISH.getCode() : WareConstant.PurchaseStatusEnum.ERROR.getCode());purchase.setUpdateTime(new Date());this.updateById(purchase);}
在讲采购项入库的操作中,需要根据skuId获取到skuName,因此需要远程调用gulimall-product服务。主启动类不要忘记加上@EnableFeignClients注解。
这里远程调用有两种写法:
1. 让所有请求过网关:
@FeignClient(“gulimall-gateway”):给gulimall-gateway所在的机器发请求
@RequestMapping(“/api/gulimallproduct/skuinfo/info/{skuId}”)
2. 不过网关直接给后台服务发请求
@FeignClient(“gulimall-product”)
@RequestMapping(“/gulimallproduct/skuinfo/info/{skuId}”)
package com.atguigu.gulimall.gulimallware.feign;@FeignClient("gulimall-product")//@FeignClient("gulimall-gateway")public interface ProductFeignService {// @RequestMapping("/api/gulimallproduct/skuinfo/info/{skuId}")@RequestMapping("/gulimallproduct/skuinfo/info/{skuId}")//@RequiresPermissions("gulimallproduct:skuinfo:info")public R info(@PathVariable("skuId") Long skuId);}
在远程调用过程中,可能会出现异常,如果单纯是因为skuName失败就回滚整个事务,不太划算。这里可以采用try-catch捕获异常。也可以使用其他方法(服务降级,准备一个兜底方法),将会在高级篇讲解。
@AutowiredWareSkuDao wareSkuDao;@AutowiredProductFeignService productFeignService;/*** 添加sku库存** @param skuId* @param wareId* @param skuNum*/@Transactional@Overridepublic void addStock(Long skuId, Long wareId, Integer skuNum) {// 1、判断是否存在这个库存记录,没有就是新增操作List<WareSkuEntity> wareSkuEntities = wareSkuDao.selectList(new QueryWrapper<WareSkuEntity>().eq("sku_id", skuId).eq("ware_id", wareId));if (CollectionUtils.isEmpty(wareSkuEntities)) {WareSkuEntity wareSkuEntity = new WareSkuEntity();wareSkuEntity.setSkuId(skuId);wareSkuEntity.setWareId(wareId);wareSkuEntity.setStock(skuNum);wareSkuEntity.setStockLocked(0);// TODO 这里应该远程查询sku的名字,如果失败整个事务不需要回滚// 方式1:自己catch异常// TODO 还可用什么办法让异常出现以后不会滚? 高级try {R info = productFeignService.info(skuId);if (info.getCode() == 0) {Map<String, Object> map = (Map<String, Object>) info.get("skuInfo");wareSkuEntity.setSkuName(map.get("skuName").toString());}}catch (Exception e) {}wareSkuDao.insert(wareSkuEntity);}else {wareSkuDao.addStock(skuId, wareId, skuNum);}}
四、商品维护-SPU管理
4.1 解决规格维护不显示

点击spu的规格的时候就直接跳转到了400页面。这是因为gulimall-admin的sys-menu表里面没有规格的目录信息
我们添加上去即可
4.2 获取spu规格
https://easydoc.net/s/78237135/ZUqEdvA4/GhhJhkg7 api文档
@Autowiredprivate ProductAttrValueService productAttrValueService;/*** 按spuId查出spu的规格属性 pms_product_attr_value*/// /product/attr/base/listforspu/{spuId}@GetMapping("/base/listforspu/{spuId}")public R baseAttrListForSpu(@PathVariable("spuId") Long spuId) {List<ProductAttrValueEntity> entities = productAttrValueService.baseAttrListForSpu(spuId); // 通过spuId查找spu对应的所有属性return R.ok().put("data", entities);}
声明&实现
/*** 通过spuId查询spu对应的所有属性* @param spuId*/@Overridepublic List<ProductAttrValueEntity> baseAttrListForSpu(Long spuId) {List<ProductAttrValueEntity> entities = this.baseMapper.selectList(new QueryWrapper<ProductAttrValueEntity>().eq("spu_id", spuId));return entities;}
4.3 修改spu商品规格
https://easydoc.net/s/78237135/ZUqEdvA4/GhnJ0L85 api文档
修改spu商品规格,可以直接把spuId对应的所有属性删除后,再将前端发送过来的属性添加即可。
/*** 根据spuId修改对应商品规格*/// /product/attr/update/{spuId}@PostMapping("/update/{spuId}")//@RequiresPermissions("gulimallproduct:attr:update")public R updateSpuAttr(@PathVariable("spuId") Long spuId, @RequestBody List<ProductAttrValueEntity> entities){productAttrValueService.updateSpuAttr(spuId, entities);return R.ok();}
/*** 更新spuId对应的所有属性* @param spuId* @param entities*/@Transactional@Overridepublic void updateSpuAttr(Long spuId, List<ProductAttrValueEntity> entities) {// 1、删除spuId之前对应的所有属性this.baseMapper.delete(new QueryWrapper<ProductAttrValueEntity>().eq("spu_id", spuId));// 2、更新属性,其实就是批量插入List<ProductAttrValueEntity> productAttrValueEntities = entities.stream().map(item -> {item.setSpuId(spuId);return item;}).collect(Collectors.toList());this.saveBatch(productAttrValueEntities);}
