[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 orderTradeData() { return success(orderInfoService.orderTradeData()); } } ``` #### 1.5.3 IOrderInfoService ```java TradeVo orderTradeData(); ``` #### 1.5.4 OrderInfoServiceImpl ```java @Autowired private RemoteCartService remoteCartService; @Autowired private RedisTemplate redisTemplate; @Override public TradeVo orderTradeData() { // 获取当前登录用户的id Long userId = SecurityContextHolder.getUserId(); R> cartInfoListResult = remoteCartService.getCartCheckedList(SecurityConstants.INNER); if (R.FAIL == cartInfoListResult.getCode()) { throw new ServiceException(cartInfoListResult.getMsg()); } List cartInfoList = cartInfoListResult.getData(); if (CollectionUtils.isEmpty(cartInfoList)) { throw new ServiceException("购物车无选中商品"); } //将集合泛型从购物车改为订单明细 List orderItemList = null; BigDecimal totalAmount = new BigDecimal(0); if (!CollectionUtils.isEmpty(cartInfoList)) { orderItemList = cartInfoList.stream().map(cartInfo -> { OrderItem orderItem = new OrderItem(); BeanUtils.copyProperties(cartInfo, orderItem); orderItem.setSkuNum(cartInfo.getSkuNum()); return orderItem; }).collect(Collectors.toList()); //订单总金额 for (OrderItem orderItem : orderItemList) { totalAmount = totalAmount.add(orderItem.getSkuPrice().multiply(new BigDecimal(orderItem.getSkuNum()))); } } //渲染订单确认页面-生成用户流水号(防止页面重复提交和页面等待超时) String tradeNo = this.generateTradeNo(userId); TradeVo tradeVo = new TradeVo(); tradeVo.setTotalAmount(totalAmount); tradeVo.setOrderItemList(orderItemList); tradeVo.setTradeNo(tradeNo); return tradeVo; } /** * 渲染订单确认页面-生成用户流水号 * * @param userId * @return */ private String generateTradeNo(Long userId) { //1.构建流水号Key String userTradeKey = "user:tradeNo:" + userId; //2.构建流水号value String tradeNo = UUID.randomUUID().toString().replaceAll("-", ""); //3.将流水号存入Redis 暂存5分钟 redisTemplate.opsForValue().set(userTradeKey, 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 获取商品最新价格接口 #### 2.2.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.2.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.3 更新购物车最新价格 操作模块:spzx-cart #### 2.3.1 远程调用接口开发 ##### 1 CartController ```java @Operation(summary="更新用户购物车列表价格") @InnerAuth @GetMapping("/updateCartPrice/{userId}") public R updateCartPrice(@PathVariable("userId") Long userId){ return R.ok(cartService.updateCartPrice(userId)); } ``` ##### 2 ICartService ```java Boolean updateCartPrice(Long userId); ``` ##### 3 CartServiceImpl ```java @Override public Boolean updateCartPrice(Long userId) { String cartKey = getCartKey(userId); 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.3.2 openFeign接口定义 操作模块:spzx-api-cart ##### 1 RemoteCartService ```java @GetMapping("/updateCartPrice/{userId}") R updateCartPrice( @PathVariable("userId") Long userId, @RequestHeader(SecurityConstants.FROM_SOURCE) String source ); ``` ##### 2 RemoteCartFallbackFactory ```java @Override public R updateCartPrice(Long userId, String source) { return R.fail("更新购物车价格失败:" + throwable.getMessage()); } ``` ### 2.4 删除购物车选中商品 下单成功后,删除购物车选中的商品 操作模块:spzx-cart #### 2.4.1 远程调用接口开发 ##### 1 CartController ```java @Operation(summary="删除用户购物车列表中选中商品列表") @InnerAuth @GetMapping("/deleteCartCheckedList/{userId}") public R deleteCartCheckedList(@PathVariable("userId") Long userId){ return R.ok(cartService.deleteCartCheckedList(userId)); } ``` ##### 2 ICartService ```java Boolean deleteCartCheckedList(Long userId); ``` ##### 3 CartServiceImpl ```java @Override public Boolean deleteCartCheckedList(Long userId) { String cartKey = getCartKey(userId); 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.4.2 openFeign接口定义 操作模块:spzx-api-cart ##### 1 RemoteCartService ```java @GetMapping("/deleteCartCheckedList/{userId}") R deleteCartCheckedList( @PathVariable("userId") Long userId, @RequestHeader(SecurityConstants.FROM_SOURCE)String source ); ``` ##### 2 RemoteCartFallbackFactory ```java @Override public R deleteCartCheckedList(Long userId, String source) { return R.fail("删除用户购物车选中数据失败:" + throwable.getMessage()); } ``` ### 2.5 获取用户地址信息(已定义) #### 2.5.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.5.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.6 批量查询商品实时价格 获取最新商品sku价格与购物车价格比较,校验价格是否变化,价格变化就更新购物车价格 #### 2.6.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.6.2 批量查询商品实时价格openFeign接口定义 操作模块:`spzx-api-product` ##### 1 RemoteProductService ```java @PostMapping(value = "/productSku/getSkuPriceList") R> getSkuPriceList( @RequestBody List skuIdList, @RequestHeader(SecurityConstants.FROM_SOURCE) String source ); ``` ##### 2 RemoteProductFallbackFactory ```java @Override public R> getSkuPriceList(List skuIdList, String source) { return R.fail("获取商品sku价格列表失败:" + throwable.getMessage()); } ``` ### 2.6 后端业务接口 操作模块:spzx-order #### 2.6.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.6.2 OrderInfoController ```java @Operation(summary = "用户提交订单") @PostMapping("/submitOrder") public AjaxResult submitOrder(@RequestBody OrderForm orderForm) { return success(orderInfoService.submitOrder(orderForm)); } ``` #### 2.6.3 IOrderInfoService ```java Long submitOrder(OrderForm orderForm); ``` #### 2.6.4 OrderInfoServiceImpl ```java @Autowired private RemoteProductService remoteProductService; @Autowired private RemoteUserAddressService remoteUserAddressService; @Autowired private OrderLogMapper orderLogMapper; /** * 验证页面提交流水号是否有效 * * @param userId * @param tradeNo * @return */ private Boolean checkTradeNo(String userId, String tradeNo) { String userTradeKey = "user:tradeNo:" + userId; String redisTradeNo = (String) redisTemplate.opsForValue().get(userTradeKey); return tradeNo.equals(redisTradeNo); } /** * 删除流水号 * * @param userId */ private void deleteTradeNo(String userId) { String userTradeKey = "user:tradeNo:" + userId; redisTemplate.delete(userTradeKey); } @Transactional(rollbackFor = Exception.class) @Override public Long submitOrder(OrderForm orderForm) { //获取用户的userId Long userId = SecurityContextHolder.getUserId(); //1 防止订单重复提交 //判断订单号是否存在: // 如果存在则说明用户第一次提交此订单 // 如果五分中内提交了第二次订单,则订单号不存在,则说明用户已经提交过该订单,不能重复提交 // 如果五分中之后第一次提交,则订单号也不存在,说明操作超时 /*if(!this.checkTradeNo(userId.toString(), orderForm.getTradeNo())){ throw new ServiceException("请勿重复提交订单"); } //删除redis中的订单号 this.deleteTradeNo(userId.toString());*/ //使用lua解决原子性的问题 String userTradeKey = "user:tradeNo:" + userId; // 将键的值更为 processed 表示已处理 return 1 表示处理成功 // return -1 键存在但值不匹配,表示重复提交 // return 0 键不存在,订单过期 String scriptText = """ if redis.call('get', KEYS[1]) == ARGV[1] then redis.call('set', KEYS[1], 'processed') return 1 else if redis.call('exists', KEYS[1]) == 1 then redis.call('del',KEYS[1]) return -1 else return 0 end end """; DefaultRedisScript redisScript = new DefaultRedisScript<>(); redisScript.setScriptText(scriptText); redisScript.setResultType(Long.class); Long flag = (Long)redisTemplate.execute(redisScript, Arrays.asList(userTradeKey), orderForm.getTradeNo()); if (flag == 0) { throw new ServiceException("操作超时,请退回重试"); } if(flag == -1){ throw new ServiceException("请勿重复提交订单"); } try { Thread.sleep(3000); } catch (InterruptedException e) { throw new RuntimeException(e); } //2 获取最新价格并判断价格是否改变 //批量获取最新价格 List orderItemList = orderForm.getOrderItemList(); List skuIdList = orderItemList.stream().map(orderItem -> orderItem.getSkuId()).collect(Collectors.toList()); R> skuPriceListResult =remoteProductService.getSkuPriceList(skuIdList, SecurityConstants.INNER); if(R.FAIL == skuPriceListResult.getCode()){ throw new ServiceException(skuPriceListResult.getMsg()); } List skuPriceList = skuPriceListResult.getData(); //判断价格是否发生变化 String priceCheckResult = ""; Map skuIdSalePriceMap = skuPriceList.stream().collect(Collectors.toMap(SkuPriceVo::getSkuId, SkuPriceVo::getSalePrice)); for (OrderItem orderItem : orderItemList) { if(orderItem.getSkuPrice().compareTo(skuIdSalePriceMap.get(orderItem.getSkuId())) != 0){ //价格发生了变化 priceCheckResult += orderItem.getSkuName() + "\n"; } } if(!StringUtils.isEmpty(priceCheckResult)){ if(!orderForm.getIsBuy()){ //更新购物车中的价格 remoteCartService.updateCartPrice(userId, SecurityConstants.INNER); } throw new ServiceException(priceCheckResult + "以上商品价格发生变化,请确认"); } //3 校验库存并锁定库存 TODO //4 下单 Long orderId = this.saveOrder(orderForm); //5 删除购物车选中商品 if(!orderForm.getIsBuy()){ remoteCartService.deleteCartCheckedList(userId, SecurityConstants.INNER); } return orderId; } private Long saveOrder(OrderForm orderForm) { Long userId = SecurityContextHolder.getUserId(); String userName = SecurityContextHolder.getUserName(); // 保存订单记录:order_info OrderInfo orderInfo = new OrderInfo(); orderInfo.setUserId(userId); orderInfo.setOrderNo(orderForm.getTradeNo());//订单号 orderInfo.setNickName(userName); orderInfo.setRemark(orderForm.getRemark()); orderInfo.setFeightFee(orderForm.getFeightFee()); //远程调用获取用户地址信息 R userAddressResult = remoteUserAddressService.getUserAddress(orderForm.getUserAddressId(), SecurityConstants.INNER); if(R.FAIL == userAddressResult.getCode()){ throw new ServiceException(userAddressResult.getMsg()); } //获取用户地址信息 UserAddress userAddress = userAddressResult.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.getFullAddress()); //实时获取总价格 //计算orderItem中商品的总价格 List orderItemList = orderForm.getOrderItemList(); BigDecimal totalAmount = orderItemList .stream() .map(orderItem -> orderItem.getSkuPrice().multiply(new BigDecimal(orderItem.getSkuNum()))) .reduce(BigDecimal.ZERO, BigDecimal::add); //设置商品总价格 orderInfo.setTotalAmount(totalAmount); orderInfo.setCouponAmount(new BigDecimal(0));//优惠券价格 orderInfo.setOriginalTotalAmount(totalAmount);//原价 //订单状态 orderInfo.setOrderStatus(0);//待付款 orderInfo.setCreateBy(userName); orderInfo.setUpdateBy(userName); baseMapper.insert(orderInfo); // 保存订单项记录:order_item for (OrderItem orderItem : orderItemList) { orderItem.setOrderId(orderInfo.getId()); orderItem.setCreateBy(userName); orderItem.setUpdateBy(userName); orderItemMapper.insert(orderItem); } // 保存订单日志记录:order_log OrderLog orderLog = new OrderLog(); orderLog.setOrderId(orderInfo.getId()); orderLog.setProcessStatus(0); orderLog.setOperateUser(userName); orderLog.setNote("提交订单"); orderLog.setCreateBy(userName); orderLog.setUpdateBy(userName); orderLogMapper.insert(orderLog); return orderInfo.getId(); } ``` ## 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 @Override public TradeVo buy(Long skuId) { //获取商品SKU信息 R productSkuResult = remoteProductService.getProductSku(skuId, SecurityConstants.INNER); if(R.FAIL == productSkuResult.getCode()){ throw new ServiceException(productSkuResult.getMsg()); } //获取实时价格 R skuPriceResult = remoteProductService.getSkuPrice(skuId, SecurityConstants.INNER); if(R.FAIL == skuPriceResult.getCode()){ throw new ServiceException(skuPriceResult.getMsg()); } ProductSku productSku = productSkuResult.getData(); SkuPriceVo skuPrice = skuPriceResult.getData(); List orderItemList = new ArrayList<>(); OrderItem orderItem = new OrderItem(); orderItem.setSkuId(skuId); orderItem.setSkuName(productSku.getSkuName()); orderItem.setSkuNum(1); orderItem.setSkuPrice(skuPrice.getSalePrice());//填充最新价格 orderItem.setThumbImg(productSku.getThumbImg()); orderItemList.add(orderItem); //订单总金额 BigDecimal totalAmount = skuPrice.getSalePrice();//填充最新价格 //渲染订单确认页面-生成用户流水号 String tradeNo = this.generateTradeNo(SecurityUtils.getUserId()); TradeVo tradeVo = new TradeVo(); tradeVo.setTotalAmount(totalAmount); tradeVo.setOrderItemList(orderItemList); tradeVo.setTradeNo(tradeNo); 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()); } } ```