[TOC] [TOC] [TOC] # 订单 ## 1 结算 ### 1.1 需求说明 入口:购物车点击去结算按钮 ,进入结算页面(订单确认页面),如图所示: ![image-20240727072230364](images/trade.gif) 分析页面需要的数据: 1、 用户地址信息列表管理(增删改查),结算页选中默认地址 2、 购物车中选择的商品列表,及商品的总金额 **查看接口文档:** 用户地址信息接口地址及返回结果: ```json #用户地址列表 get /user/userAddress/list 返回结果: { "msg": "操作成功", "code": 200, "data": [ { "id": 60, "userId": 1, "name": "晴天", "phone": "15023656352", "tagName": "家", "provinceCode": "110000", "cityCode": "110100", "districtCode": "110101", "address": "东直门1号", "fullAddress": "北京市北京市东城区东直门1号", "isDefault": 1 }, ... ] } #添加用户地址 post /user/userAddress 参数: { "id": null, "name": "cs", "phone": "15090909090", "provinceCode": "110000", "cityCode": "110100", "districtCode": "110102", "address": "111", "tagName": "家", "isDefault": 0 } 返回结果: { "msg": "操作成功", "code": 200 } #修改用户地址 put /user/userAddress 参数: { "id": 60 "name": "cs", "phone": "15090909090", "provinceCode": "110000", "cityCode": "110100", "districtCode": "110102", "address": "111", "tagName": "家", "isDefault": 0 } 返回结果: { "msg": "操作成功", "code": 200 } #删除用户地址 delete /user/userAddress/{id} 返回结果: { "msg": "操作成功", "code": 200 } ``` 结算接口地址及返回结果: ```json get /order/orderInfo/trade 返回结果: { "msg": "操作成功", "code": 200, "data": { "totalAmount": 8998.00, "orderItemList": [ { "orderId": null, "skuId": 9, "skuName": "华为笔记本 32G", "thumbImg": "http://139.198.127.41:9000/spzx/20230525/c8f2eae0d36b6270.jpg.avif", "skuPrice": 5999.00, "skuNum": 1 }, ... ], "tradeNo": "1d76f36b59414e869e843fc742e21469" } } ``` ### 1.2 地区管理 操作模块:spzx-user #### 1.2.1 RegionController 添加如下方法: ```java @Operation(summary = "查询地区信息树形列表") @GetMapping(value = "/treeSelect/{parentCode}") public AjaxResult treeSelect(@PathVariable String parentCode) { return success(regionService.treeSelect(parentCode)); } ``` #### 1.2.2 IRegionService ```java List treeSelect(String parentCode); ``` #### 1.2.3 RegionServiceImpl ```java @Override public List treeSelect(String parentCode) { return regionMapper.selectList(new LambdaQueryWrapper().eq(Region::getParentCode, parentCode)); } ``` ### 1.3 搭建订单管理模块 #### 1.3.1 新建模块 在spzx-modules模块下新建子模块spzx-order #### 1.3.2 pom.xml ```xml com.spzx spzx-modules 3.6.3 4.0.0 spzx-order spzx-order订单模块 com.alibaba.cloud spring-cloud-starter-alibaba-nacos-discovery com.alibaba.cloud spring-cloud-starter-alibaba-nacos-config com.alibaba.cloud spring-cloud-starter-alibaba-sentinel org.springframework.boot spring-boot-starter-actuator com.mysql mysql-connector-j com.spzx spzx-common-datascope com.spzx spzx-common-log ${project.artifactId} org.springframework.boot spring-boot-maven-plugin repackage org.apache.maven.plugins maven-compiler-plugin 17 17 ``` #### 1.3.3 banner.txt 在resources目录下新建banner.txt ```text Spring Boot Version: ${spring-boot.version} Spring Application Name: ${spring.application.name} _ _ (_) | | _ __ _ _ ___ _ _ _ ______ ___ _ _ ___ | |_ ___ _ __ ___ | '__|| | | | / _ \ | | | || ||______|/ __|| | | |/ __|| __| / _ \| '_ ` _ \ | | | |_| || (_) || |_| || | \__ \| |_| |\__ \| |_ | __/| | | | | | |_| \__,_| \___/ \__, ||_| |___/ \__, ||___/ \__| \___||_| |_| |_| __/ | __/ | |___/ |___/ ``` #### 1.3.4 bootstrap.yml 在resources目录下新建bootstrap.yml ```yaml # Tomcat server: port: 9207 # Spring spring: application: # 应用名称 name: spzx-order profiles: # 环境配置 active: dev main: allow-bean-definition-overriding: true #当遇到同样名字的时候,是否允许覆盖注册 cloud: nacos: discovery: # 服务注册地址 server-addr: 192.168.200.131:8848 config: # 配置中心地址 server-addr: 192.168.200.131:8848 # 配置文件格式 file-extension: yml # 共享配置 shared-configs: - application-${spring.profiles.active}.${spring.cloud.nacos.config.file-extension} ``` #### 1.3.5 spzx-order-dev.yml 在nacos上添加商品服务配置文件 ```yaml mybatis-plus: mapper-locations: classpath*:mapper/**/*Mapper.xml type-aliases-package: com.spzx.**.domain configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 查看日志 global-config: db-config: logic-delete-field: del_flag # 全局逻辑删除的实体字段名 logic-delete-value: 2 # 逻辑已删除值(默认为 1) logic-not-delete-value: 0 # 逻辑未删除值(默认为 0) # spring配置 spring: data: redis: host: 192.168.200.131 port: 6379 password: datasource: type: com.zaxxer.hikari.HikariDataSource driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://192.168.200.131:3306/spzx-order?characterEncoding=utf-8&useSSL=false username: root password: root hikari: connection-test-query: SELECT 1 connection-timeout: 60000 idle-timeout: 500000 max-lifetime: 540000 maximum-pool-size: 10 minimum-idle: 5 pool-name: GuliHikariPool ``` #### 1.3.6 logback.xml 在resources目录下新建logback.xml ```xml ${log.pattern} ${log.path}/info.log ${log.path}/info.%d{yyyy-MM-dd}.log 60 ${log.pattern} INFO ACCEPT DENY ${log.path}/error.log ${log.path}/error.%d{yyyy-MM-dd}.log 60 ${log.pattern} ERROR ACCEPT DENY ``` #### 1.3.7 SpzxOrderApplication 添加启动类 ```java package com.spzx.order; /** * 会员模块 * */ @EnableCustomConfig @EnableRyFeignClients @SpringBootApplication public class SpzxOrderApplication { public static void main(String[] args) { SpringApplication.run(SpzxOrderApplication.class, args); System.out.println("(♥◠‿◠)ノ゙ 系统模块启动成功 ლ(´ڡ`ლ)゙ \n" + " .-------. ____ __ \n" + " | _ _ \\ \\ \\ / / \n" + " | ( ' ) | \\ _. / ' \n" + " |(_ o _) / _( )_ .' \n" + " | (_,_).' __ ___(_ o _)' \n" + " | |\\ \\ | || |(_,_)' \n" + " | | \\ `' /| `-' / \n" + " | | \\ / \\ / \n" + " ''-' `'-' `-..-' "); } } ``` #### 1.3.8 配置网关 在spzx-gateway-dev.yml配置文件中添加会员服务的网关信息 ```yaml # 订单服务 - id: spzx-order uri: lb://spzx-order predicates: - Path=/order/** filters: - StripPrefix=1 ``` #### 1.3.9 生成代码 ##### 1 复制一个代码生成器 从其他模块中f复制GenMP,修改代码生成器中关于服务名的部分,配置需要生成的表 ##### 2 复制mapper到resources ##### 3 OrderInfo 补充属性 ```java @TableField(exist = false) private List orderItemList; ``` 将`orderStatus`的数据类型改成`Integer` ### 1.4 获取选中购物项数据接口 #### 1.4.1 远程调用接口开发 操作模块:spzx-cart ##### 1 CartController ```java @Operation(summary = "查询用户购物车列表中选中商品列表") @InnerAuth @GetMapping("/getCartCheckedList") public R> getCartCheckedList() { return R.ok(cartService.getCartCheckedList()); } ``` ##### 2 ICartService ```java List getCartCheckedList(); ``` ##### 3 CartServiceImpl ```java @Override public List getCartCheckedList() { Long userId = SecurityContextHolder.getUserId(); String cartKey = this.getCartKey(userId); List cartCachInfoList = redisTemplate.opsForHash().values(cartKey); List cartInfoList = new ArrayList<>(); if (!CollectionUtils.isEmpty(cartCachInfoList)) { cartInfoList = cartCachInfoList .stream() .filter(item -> item.getIsChecked().intValue() == 1) .collect(Collectors.toList()); } return cartInfoList; } ``` #### 1.4.2 openFeign接口定义 操作模块:spzx-api-cart ##### 1 RemoteCartService ```java package com.spzx.cart.api; @FeignClient( value = ServiceNameConstants.CART_SERVICE, fallbackFactory = RemoteCartFallbackFactory.class) public interface RemoteCartService { @GetMapping("/getCartCheckedList") R> getCartCheckedList( @RequestHeader(SecurityConstants.FROM_SOURCE) String source); } ``` ##### 2 ServiceNameConstants ```java /** * 购物车服务的serviceid */ public static final String CART_SERVICE = "spzx-cart"; ``` ##### 3 RemoteCartFallbackFactory ```java package com.spzx.cart.api.factory; /** * 购物车降级处理 * * @author atguigu */ @Component @Slf4j public class RemoteCartFallbackFactory implements FallbackFactory { @Override public RemoteCartService create(Throwable throwable) { log.error("购物车服务调用失败:{}", throwable.getMessage()); return new RemoteCartService() { @Override public R> getCartCheckedList(String source) { return R.fail("获取用户购物车选中数据失败:" + throwable.getMessage()); } }; } } ``` ##### 4 加载配置类 resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports ```java com.spzx.cart.api.factory.RemoteCartFallbackFactory ``` ### 1.5 后端业务接口 操作模块:spzx-order #### 1.5.1 TradeVo ```java package com.spzx.order.vo; @Data @Schema(description = "结算实体类") public class TradeVo { @Schema(description = "结算总金额") private BigDecimal totalAmount; @Schema(description = "结算商品列表") private List orderItemList; @Schema(description = "交易号") private String tradeNo; @Schema(description = "是否是立即购买") private Boolean isBuy = false; } ``` #### 1.5.2 OrderInfoController ```java package com.spzx.order.controller; @Tag(name = "订单管理") @RestController @RequestMapping("/orderInfo") public class OrderInfoController extends BaseController { @Autowired private IOrderInfoService orderInfoService; @Operation(summary = "订单结算") @GetMapping("/trade") public AjaxResult trade() { return success(orderInfoService.orderTradeData()); } } ``` #### 1.5.3 IOrderInfoService ```java TradeVo trade(); ``` #### 1.5.4 OrderInfoServiceImpl ```java @Autowired private RemoteCartService remoteCartService; @Autowired private RedisTemplate redisTemplate; /** * 订单结算页相关参数封装 * * @return */ @Override public TradeVo trade() { //0.创建订单结算VO对象 TradeVo tradeVo = new TradeVo(); //1.远程调用购物车服务,获取选中的商品列表,封装订单明细列表 R> r = cartService.getCartCheckedList(SecurityConstants.INNER); //1.1 验证远程调用是否成功 if (R.FAIL == r.getCode()) { throw new ServiceException("远程调用购物车服务失败,原因:" + r.getMsg()); } //1.2 获取到选中购物车商品列表 List cartInfoList = r.getData(); //1.3 将购物车商品列表转为订单明细列表 if (Collections.isEmpty(cartInfoList)) { throw new ServiceException("没有需要结算商品"); } //1.4 将购物车商品转为订单明细 List orderItemList = cartInfoList .stream() .map(cartInfo -> { OrderItem orderItem = new OrderItem(); BeanUtils.copyProperties(cartInfo, orderItem); return orderItem; }).collect(Collectors.toList()); tradeVo.setOrderItemList(orderItemList); //2.计算订单总金额 BigDecimal totalAmount = new BigDecimal(0); for (OrderItem orderItem : orderItemList) { totalAmount = totalAmount.add(orderItem.getSkuPrice().multiply(new BigDecimal(orderItem.getSkuNum()))); } tradeVo.setTotalAmount(totalAmount); //3.生成本次订单流水号-防止重复点击提交订单,存入Redis中 有效期5分钟 String tradeNo = this.generateTradeNo(); tradeVo.setTradeNo(tradeNo); //4.封装结算对象VO响应 return tradeVo; } /** * 生成当前用户结算订单流水号 * * @return */ @Override public String generateTradeNo() { String tradeKey = "user:tradeNo:" + SecurityContextHolder.getUserId(); String tradeNo = UUID.randomUUID().toString(); redisTemplate.opsForValue().set(tradeKey, tradeNo, 5, TimeUnit.MINUTES); return tradeNo; } ``` ## 2 下单 ### 2.1 需求说明 需求说明:用户在结算页面点击提交订单按钮,那么此时就需要保存订单信息(order_info)、订单项信息(order_item)及记录订单日志(order_log),下单成功重定向到订单支付页面 **查看接口文档:** 下单接口地址及返回结果: ```json post /order/orderInfo/submitOrder 参数: { "orderItemList": [ { "skuId": 6, "skuName": "小米 红米Note10 5G手机 颜色:黑色 内存:18G", "thumbImg": "http://139.198.127.41:9000/spzx/20230525/665832167-1_u_1.jpg", "skuPrice": 2999, "skuNum": 1 }, ... ], "userAddressId": 2, "feightFee": 0, "remark": "赶快发货" } 返回结果(订单id): { "code": 200, "message": "操作成功", "data": 1 } ``` ### 2.2 批量查询商品实时价格 获取最新商品sku价格与购物车价格比较,校验价格是否变化,价格变化就更新购物车价格 #### 2.2.1 批量查询商品实时价格 操作模块:`spzx-product` ##### 1 ProductSkuController ```java @Operation(summary = "批量获取商品sku最新价格信息") @InnerAuth @PostMapping(value = "/getSkuPriceList") public R> getSkuPriceList(@RequestBody List skuIdList) { return R.ok(productSkuService.getSkuPriceList(skuIdList)); } ``` ##### 2 IProductService ```java List getSkuPriceList(List skuIdList); ``` ##### 3 ProductServiceImpl ```java @Override public List getSkuPriceList(List skuIdList) { List productSkuList = baseMapper.selectList( new LambdaQueryWrapper() .in(ProductSku::getId, skuIdList) .select(ProductSku::getId, ProductSku::getSalePrice) ); return productSkuList.stream().map(item -> { SkuPriceVo skuPrice = new SkuPriceVo(); skuPrice.setSkuId(item.getId()); skuPrice.setSalePrice(item.getSalePrice()); return skuPrice; }).collect(Collectors.toList()); } ``` ### 2.3 获取商品最新价格接口 #### 2.3.1 远程调用接口开发 操作模块:spzx-product ##### ProductSkuController ```java @Operation(summary = "获取商品sku最新价格信息") @InnerAuth @GetMapping(value = "/getSkuPrice/{skuId}") public R getSkuPrice(@PathVariable("skuId") Long skuId) { return R.ok(productSkuService.getSkuPrice(skuId)); } ``` #### 2.3.2 openFeign接口定义 操作模块:spzx-api-product ##### 1 移动实体类 **将spzx-product模块SkuPriceVo类移动到spzx-api-product模块** ##### 2 RemoteProductService ```java @GetMapping(value = "/productSku/getSkuPrice/{skuId}") R getSkuPrice(@PathVariable("skuId") Long skuId, @RequestHeader(SecurityConstants.FROM_SOURCE) String source); ``` ##### 3 RemoteProductFallbackFactory ```java @Override public R getSkuPrice(Long skuId, String source) { return R.fail("获取商品sku价格失败:" + throwable.getMessage()); } ``` ### 2.4 更新购物车最新价格 操作模块:spzx-cart #### 2.4.1 远程调用接口开发 ##### 1 CartController ```java @Operation(summary="更新用户购物车列表价格") @InnerAuth @GetMapping("/updateCartPrice") public R updateCartPrice(){ return R.ok(cartService.updateCartPrice()); } ``` ##### 2 ICartService ```java Boolean updateCartPrice(); ``` ##### 3 CartServiceImpl ```java @Override public Boolean updateCartPrice() { String cartKey = getCartKey(SecurityContextHolder.getUserId()); BoundHashOperations hashOperations = redisTemplate.boundHashOps(cartKey); List cartCachInfoList = hashOperations.values(); if (!CollectionUtils.isEmpty(cartCachInfoList)) { for (CartInfo cartInfo : cartCachInfoList) { if (cartInfo.getIsChecked().intValue() == 1) { SkuPriceVo skuPrice = remoteProductService .getSkuPrice(cartInfo.getSkuId(), SecurityConstants.INNER) .getData(); //放入购物车时价格 cartInfo.setCartPrice(skuPrice.getSalePrice()); //实时价格 cartInfo.setSkuPrice(skuPrice.getSalePrice()); hashOperations.put(cartInfo.getSkuId().toString(), cartInfo); } } } return true; } ``` #### 2.4.2 openFeign接口定义 操作模块:spzx-api-cart ##### 1 RemoteCartService ```java @GetMapping("/updateCartPrice/{userId}") R updateCartPrice( @RequestHeader(SecurityConstants.FROM_SOURCE) String source ); ``` ##### 2 RemoteCartFallbackFactory ```java @Override public R updateCartPrice(String source) { return R.fail("更新购物车价格失败:" + throwable.getMessage()); } ``` ### 2.5 删除购物车选中商品 下单成功后,删除购物车选中的商品 操作模块:spzx-cart #### 2.5.1 远程调用接口开发 ##### 1 CartController ```java @Operation(summary="删除用户购物车列表中选中商品列表") @InnerAuth @GetMapping("/deleteCartCheckedList") public R deleteCartCheckedList(){ return R.ok(cartService.deleteCartCheckedList()); } ``` ##### 2 ICartService ```java Boolean deleteCartCheckedList(); ``` ##### 3 CartServiceImpl ```java @Override public Boolean deleteCartCheckedList() { String cartKey = getCartKey(SecurityContextHolder.getUserId()); BoundHashOperations hashOperations = redisTemplate.boundHashOps(cartKey); List cartCachInfoList = hashOperations.values(); if (!CollectionUtils.isEmpty(cartCachInfoList)) { for (CartInfo cartInfo : cartCachInfoList) { // 删除选中的商品 if (cartInfo.getIsChecked().intValue() == 1) { hashOperations.delete(cartInfo.getSkuId().toString()); } } } return true; } ``` #### 2.5.2 openFeign接口定义 操作模块:spzx-api-cart ##### 1 RemoteCartService ```java @GetMapping("/deleteCartCheckedList/{userId}") R deleteCartCheckedList( @RequestHeader(SecurityConstants.FROM_SOURCE)String source ); ``` ##### 2 RemoteCartFallbackFactory ```java @Override public R deleteCartCheckedList(String source) { return R.fail("删除用户购物车选中数据失败:" + throwable.getMessage()); } ``` ### 2.6 获取用户地址信息(已定义) #### 2.6.1 远程调用接口 操作模块:spzx-user ##### 1 UserAddressController ```java @InnerAuth @GetMapping(value = "/getUserAddress/{id}") public R getUserAddress(@PathVariable("id") Long id) { return R.ok(userAddressService.getById(id)); } ``` ##### 2 UserAddress 将spzx-user模块UserAddress实体类,移动到spzx-api-user模块 #### 2.6.2 openFeign接口定义 操作模块:spzx-api-user ##### 1 RemoteUserAddressService ```java package com.spzx.user.api; @FeignClient( contextId = "remoteUserAddressService" , value = ServiceNameConstants.USER_SERVICE, fallbackFactory = RemoteUserAddressFallbackFactory.class ) public interface RemoteUserAddressService { @GetMapping(value = "/userAddress/getUserAddress/{id}") R getUserAddress( @PathVariable("id") Long id, @RequestHeader(SecurityConstants.FROM_SOURCE) String source ); } ``` ##### 2 RemoteUserAddressFallbackFactory ```java package com.spzx.user.api.factory; /** * 服务降级处理 * */ @Component @Slf4j public class RemoteUserAddressFallbackFactory implements FallbackFactory { @Override public RemoteUserAddressService create(Throwable throwable) { log.error("用户服务调用失败:{}", throwable.getMessage()); return new RemoteUserAddressService() { @Override public R getUserAddress(Long id, String source) { return R.fail("获取用户地址失败:" + throwable.getMessage()); } }; } } ``` ##### 3 加载配置类 resources/META-INF.spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports ```java com.spzx.user.api.factory.RemoteUserAddressFallbackFactory ``` ### 2.7 后端业务接口 操作模块:spzx-order #### 2.7.1 OrderForm ```java package com.spzx.order.form; @Data public class OrderForm { @Schema(description = "用户流水号") private String tradeNo; //送货地址id @Schema(description = "送货地址id") private Long userAddressId; //运费 @Schema(description = "运费") private BigDecimal feightFee; //备注 @Schema(description = "备注") private String remark; @Schema(description = "结算商品列表") private List orderItemList; @Schema(description = "是否是立即购买") private Boolean isBuy = false; } ``` #### 2.7.2 OrderInfoController ```java @Operation(summary = "用户提交订单") @PostMapping("/submitOrder") public AjaxResult submitOrder(@RequestBody OrderForm orderForm) { return success(orderInfoService.submitOrder(orderForm)); } ``` #### 2.7.3 IOrderInfoService ```java Long submitOrder(OrderForm orderForm); ``` #### 2.7.4 OrderInfoServiceImpl ```java @Autowired private RemoteProductService remoteProductService; @Autowired private RemoteUserAddressService remoteUserAddressService; @Autowired private OrderLogMapper orderLogMapper; /** * 保存订单 * * @param orderForm * @return */ @Override @Transactional(rollbackFor = Exception.class) public Long submitOrder(OrderForm orderForm) { //1.业务校验,验证流水号,防止订单重复提交或长时间停留订单结算页 String tradeKey = "user:tradeNo:" + SecurityContextHolder.getUserId(); //1.1 通过Lua脚本验证流水号 KEYS[1]:流水号Key ARGV[1]:用户提交流水号 String script = "if redis.call(\"get\",KEYS[1]) == ARGV[1]\n" + "then\n" + " return redis.call(\"del\",KEYS[1])\n" + "else\n" + " return 0\n" + "end\n"; RedisScript booleanRedisScript = new DefaultRedisScript<>(script, Boolean.class); Boolean flag = (Boolean) redisTemplate.execute(booleanRedisScript, Arrays.asList(tradeKey), orderForm.getTradeNo()); if (!flag) { throw new ServiceException("流水号校验失败,请重新提交"); } //2.验证订单中明细商品价格是否变更 //2.1 根据订单明细列表中skuID列表,远程调用"商品服务"得到实时商品价格列表 List skuIdList = orderForm.getOrderItemList().stream() .map(OrderItem::getSkuId) .collect(Collectors.toList()); R> r = remoteProductService.getSkuPriceList(skuIdList, SecurityConstants.INNER); if (R.FAIL == r.getCode()) { throw new ServiceException("远程调用商品服务失败,原因:" + r.getMsg()); } List skuPriceVoList = r.getData(); Map skuPriceMap = skuPriceVoList.stream().collect(Collectors.toMap(SkuPriceVo::getSkuId, SkuPriceVo::getSalePrice)); //2.2 判断商品价格是否有变更,如果有变更 String priceErrorMsg = ""; for (OrderItem orderItem : orderForm.getOrderItemList()) { if (orderItem.getSkuPrice().compareTo(skuPriceMap.get(orderItem.getSkuId())) != 0) { priceErrorMsg += orderItem.getSkuName() + "\n"; } } if (StringUtils.isNotBlank(priceErrorMsg)) { //2.2.1 如果是通过购物车车途径提交订单,则远程调用"购物车服务"更新购物车中商品价格 if (!orderForm.getIsBuy()) { remoteCartService.updateCartPrice(SecurityConstants.INNER); } //2.2.2 则提示价格有变更,重新结算,引导用户购物车页面 throw new ServiceException(priceErrorMsg + "以上商品价格有变动"); } //4.如果商品价格未变更:保存订单及明细,日志 Long orderId = this.saveOrder(orderForm); //5.TODO 锁定商品库存,基于RabbitMQ异步锁定库存 //6.保存订单成功后, 如果是通过购物车车途径提交订单,清理用户购物车 if (!orderForm.getIsBuy()) { remoteCartService.deleteCartCheckedList(SecurityConstants.INNER); } //7.响应订单ID,对接支付页面 //TODO :RabbitMQ(死信、延迟插件)发送延迟消息 作用:自动将超时未支付订单关闭(延迟关单) return orderId; } /** * 保存订单相关信息 * * @param orderForm * @return */ @Override public Long saveOrder(OrderForm orderForm) { //1.保存订单信息 Long userId = SecurityContextHolder.getUserId(); String userName = SecurityContextHolder.getUserName(); OrderInfo orderInfo = new OrderInfo(); //1.1 用户信息 orderInfo.setUserId(userId); orderInfo.setNickName(userName); //1.2 生成订单编号 形式:日期+全局唯一ID(雪花算法) String orderNo = DateUtils.dateTime() + IdWorker.getIdStr(); orderInfo.setOrderNo(orderNo); //1.3 遍历订单明细列表计算订单总金额 BigDecimal totalAmount = orderForm.getOrderItemList().stream() .map(OrderItem::getSkuPrice) //从0开始累加结合中每个元素值 .reduce(BigDecimal.ZERO, BigDecimal::add); orderInfo.setTotalAmount(totalAmount); orderInfo.setOriginalTotalAmount(totalAmount); //1.4 订单状态:待付款 orderInfo.setOrderStatus(0); orderInfo.setCreateBy(userName); orderInfo.setRemark(orderForm.getRemark()); //1.5 封装订单收件人信息 远程调用"用户服务"得到收件人信息 R r = remoteUserAddressService.getUserAddress(orderForm.getUserAddressId(), SecurityConstants.INNER); if (R.FAIL == r.getCode()) { throw new ServiceException("远程调用用户服务失败,原因:" + r.getMsg()); } UserAddress userAddress = r.getData(); orderInfo.setReceiverName(userAddress.getName()); orderInfo.setReceiverPhone(userAddress.getPhone()); orderInfo.setReceiverTagName(userAddress.getTagName()); orderInfo.setReceiverProvince(userAddress.getProvinceCode()); orderInfo.setReceiverCity(userAddress.getCityCode()); orderInfo.setReceiverDistrict(userAddress.getDistrictCode()); orderInfo.setReceiverAddress(userAddress.getAddress()); save(orderInfo); Long orderId = orderInfo.getId(); //2.保存订单明细信息 List orderItemList = orderForm.getOrderItemList(); if (!CollectionUtils.isEmpty(orderItemList)) { for (OrderItem orderItem : orderItemList) { orderItem.setOrderId(orderId); orderItem.setCreateBy(userName); orderItemMapper.insert(orderItem); } } //3.保存订单操作日志 OrderLog orderLog = new OrderLog(); orderLog.setOrderId(orderId); orderLog.setOperateUser(userId.toString()); orderLog.setProcessStatus(0); orderLog.setNote(orderForm.getRemark()); orderLog.setCreateBy(userName); orderLogMapper.insert(orderLog); return orderId; } ``` ## 3 立即购买 ### 3.1、需求说明 入口:商品详情页,点击“立即购买”按钮,立即购买直接进入结算页,不经过购物车,结算页返回数据与正常下单结算数据一致,提交订单接口不变,如图所示: buy **查看接口文档:** 立即购买接口地址及返回结果: ```json get /order/orderInfo/buy/{skuId} 返回结果: { "msg": "操作成功", "code": 200, "data": { "totalAmount": 5999.00, "orderItemList": [ { "orderId": null, "skuId": 9, "skuName": "华为笔记本 32G", "thumbImg": "http://139.198.127.41:9000/spzx/20230525/c8f2eae0d36b6270.jpg.avif", "skuPrice": 5999.00, "skuNum": 1 } ], "tradeNo": "1d76f36b59414e869e843fc742e21469" } } ``` ### 3.2、后端业务接口 操作模块:`spzx-order` #### 3.2.1、OrderInfoController ```java @Operation(summary = "立即购买") @GetMapping("/buy/{skuId}") public AjaxResult buy(@PathVariable Long skuId) { return success(orderInfoService.buy(skuId)); } ``` #### 3.2.2、IOrderInfoService ```java TradeVo buy(Long skuId); ``` #### 3.2.3、OrderInfoServiceImpl ```java /** * 订单结算页面渲染 * * @param skuId * @return */ @Override public TradeVo buy(Long skuId) { //1.创建订单确认VO对象 TradeVo tradeVo = new TradeVo(); //2.封装订单明细 //2.1 远程调用"商品服务"查询商品信息 R r = remoteProductService.getProductSku(skuId, SecurityConstants.INNER); if (R.FAIL == r.getCode()) { throw new ServiceException("远程调用商品服务失败,原因:" + r.getMsg()); } ProductSku productSku = r.getData(); //2.2 封装订单明细列表 OrderItem orderItem = new OrderItem(); orderItem.setSkuId(skuId); orderItem.setSkuName(productSku.getSkuName()); orderItem.setThumbImg(productSku.getThumbImg()); orderItem.setSkuPrice(productSku.getSalePrice()); orderItem.setSkuNum(1); List orderItemList = Arrays.asList(orderItem); tradeVo.setOrderItemList(orderItemList); //3.封装订单总金额 tradeVo.setTotalAmount(orderItem.getSkuPrice()); //4.生成流水号 String tradeNo = this.generateTradeNo(); tradeVo.setTradeNo(tradeNo); //5.设置立即购买为:true tradeVo.setIsBuy(true); return tradeVo; } ``` ## 4 支付页 ### 4.1 需求说明 提交订单成功,跳转到支付页面,根据订单id获取订单详细信息,展示订单支付信息 **查看接口文档:** 根据订单id获取订单信息接口地址及返回结果: ```json get /order/orderInfo/getOrderInfo/{orderId} 返回结果: { "msg": "操作成功", "code": 200, "data": { "id": 2, "createTime": "2024-02-28 08:29:36", "userId": 1, "nickName": "13700032456", "orderNo": "f1866bad38bc4627958542d72a15ca9c", "couponId": null, "totalAmount": 9997.00, "couponAmount": 0.00, "originalTotalAmount": 9997.00, "feightFee": 0.00, "orderStatus": 0, "receiverName": "晴天", "receiverPhone": "15023656352", "receiverTagName": "家", "receiverProvince": "110000", "receiverCity": "110100", "receiverDistrict": "110101", "receiverAddress": "北京市北京市东城区东直门1号", "paymentTime": null, "deliveryTime": null, "receiveTime": null, "cancelTime": null, "cancelReason": null, "orderItemList": null } } ``` ### 4.2 后端业务接口 #### 4.2.1 OrderInfoController ```java @Operation(summary = "获取订单信息") @GetMapping("/getOrderInfo/{orderId}") public AjaxResult getOrderInfo(@PathVariable Long orderId) { OrderInfo orderInfo = orderInfoService.getById(orderId); return success(orderInfo); } ``` ## 5 我的订单 ### 5.1 需求说明 我的订单根据订单状态展示列表,如图所示: order **查看接口文档:** 我的订单接口地址及返回结果: ```json get /order/orderInfo/userOrderInfoList/{pageNum}/{pageSize}?orderStatus={orderStatus} 返回结果: { "total": 2, "rows": [ { "id": 2, "createTime": "2024-02-28 08:29:36", "userId": 1, "nickName": "13700032456", "orderNo": "f1866bad38bc4627958542d72a15ca9c", "couponId": null, "totalAmount": 9997.00, "couponAmount": 0.00, "originalTotalAmount": 9997.00, "feightFee": 0.00, "orderStatus": 0, "receiverName": "晴天", "receiverPhone": "15023656352", "receiverTagName": "家", "receiverProvince": "110000", "receiverCity": "110100", "receiverDistrict": "110101", "receiverAddress": "北京市北京市东城区东直门1号", "paymentTime": null, "deliveryTime": null, "receiveTime": null, "cancelTime": null, "cancelReason": null, "orderItemList": [ { "id": 4, "orderId": 2, "skuId": 7, "skuName": "华为笔记本 8G", "thumbImg": "http://139.198.127.41:9000/spzx/20230525/4b5a68a9bfbd0795.jpg.avif", "skuPrice": 3999.00, "skuNum": 2 }, ... ] }, ... ], "code": 200, "msg": "查询成功" } ``` ### 5.2 后端业务接口 #### 5.2.1 OrderInfoController ```java @Operation(summary = "获取用户订单分页列表") @GetMapping("/userOrderInfoList/{pageNum}/{pageSize}") public TableDataInfo list( @Parameter(name = "pageNum", description = "当前页码", required = true) @PathVariable Integer pageNum, @Parameter(name = "pageSize", description = "每页记录数", required = true) @PathVariable Integer pageSize, @Parameter(name = "orderStatus", description = "订单状态", required = false) @RequestParam(required = false, defaultValue = "") Integer orderStatus) { PageHelper.startPage(pageNum, pageSize); List list = orderInfoService.selectUserOrderInfoList(orderStatus); return getDataTable(list); } ``` #### 5.2.2 IOrderInfoService ```java List selectUserOrderInfoList(Integer orderStatus); ``` #### 5.2.3 OrderInfoServiceI ```java @Override public List selectUserOrderInfoList(Integer orderStatus) { // 获取当前登录用户的id Long userId = SecurityContextHolder.getUserId(); List orderInfoList = baseMapper.selectList(new LambdaQueryWrapper() .eq(OrderInfo::getUserId, userId) .eq(orderStatus != null, OrderInfo::getOrderStatus, orderStatus) .orderByDesc(OrderInfo::getCreateTime)); if(!CollectionUtils.isEmpty(orderInfoList)) { List orderIdList = orderInfoList.stream().map(OrderInfo::getId).collect(Collectors.toList()); //查询orderItem List orderItemList = orderItemMapper.selectList( new LambdaQueryWrapper().in(OrderItem::getOrderId, orderIdList) ); Map> orderIdToOrderItemListMap = orderItemList.stream().collect( Collectors.groupingBy(OrderItem::getOrderId) ); //组装orderItemList orderInfoList.forEach(item -> { item.setOrderItemList(orderIdToOrderItemListMap.get(item.getId())); }); } return orderInfoList; } ``` ## 6 订单详情 ### 6.1 需求说明 `我的订单`点击`详情`,如图所示: orderInfo **查看接口文档:** 我的订单接口地址及返回结果: ```json get /order/orderInfo/getOrderInfo/{orderId} 返回结果: { "msg": "操作成功", "code": 200, "data": { "id": 2, "createTime": "2024-02-28 08:29:36", "userId": 1, "nickName": "13700032456", "orderNo": "f1866bad38bc4627958542d72a15ca9c", "couponId": null, "totalAmount": 9997.00, "couponAmount": 0.00, "originalTotalAmount": 9997.00, "feightFee": 0.00, "orderStatus": 0, "receiverName": "晴天", "receiverPhone": "15023656352", "receiverTagName": "家", "receiverProvince": "110000", "receiverCity": "110100", "receiverDistrict": "110101", "receiverAddress": "北京市北京市东城区东直门1号", "paymentTime": null, "deliveryTime": null, "receiveTime": null, "cancelTime": null, "cancelReason": null, "orderItemList": [ { "id": 4, "orderId": 2, "skuId": 7, "skuName": "华为笔记本 8G", "thumbImg": "http://139.198.127.41:9000/spzx/20230525/4b5a68a9bfbd0795.jpg.avif", "skuPrice": 3999.00, "skuNum": 2 }, ... ] } } ``` ### 6.2 后端业务接口 #### 6.2.1 OrderInfoController 调整`获取订单信息`接口即可 ```java @Operation(summary = "获取订单信息") @GetMapping("/getOrderInfo/{orderId}") public AjaxResult getOrderInfo(@PathVariable Long orderId) { OrderInfo orderInfo = orderInfoService.selectOrderInfoById(orderId); return success(orderInfo); } ``` ## 7 用户取消订单 ### 7.1 需求说明 点击`取消订单`按钮可以取消订单。 1709899135809 操作模块:`spzx-order` ### 7.2 OrderInfoController ```java @Operation(summary = "取消订单") @RequiresLogin @GetMapping("/cancelOrder/{orderId}") public AjaxResult cancelOrder(@PathVariable Long orderId) { orderInfoService.cancelOrder(orderId); return success(); } ``` ### 7.3 IOrderInfoService ```java void cancelOrder(Long orderId); ``` ### 7.4 OrderInfoServiceImpl 业务逻辑和延迟关单一致。 ```java @Override public void cancelOrder(Long orderId) { OrderInfo orderInfo = baseMapper.selectById(orderId); if(orderInfo != null && orderInfo.getOrderStatus().intValue() == 0) {//待付款 orderInfo.setOrderStatus(-1);//已取消 orderInfo.setCancelTime(new Date()); orderInfo.setCancelReason("用户取消订单"); baseMapper.updateById(orderInfo); //记录日志 OrderLog orderLog = new OrderLog(); orderLog.setOrderId(orderInfo.getId()); orderLog.setProcessStatus(-1); orderLog.setNote("用户取消订单"); orderLogMapper.insert(orderLog); //发送MQ消息通知商品系统解锁库存:TODO //rabbitService.sendMessage(MqConst.EXCHANGE_PRODUCT, MqConst.ROUTING_UNLOCK, orderInfo.getOrderNo()); } } ```