🌈个人主页:一条泥憨鱼(欢迎各位大佬莅临)
🎬精选专栏传送门:
❄️《数据结构》 ❄️《AI与Agent那些事》
❄️《从0开始学计算机网络》 ❄️《后端开发》
需求分析和设计
代码开发
第1层:Controller — 接收请求
OrderController.java
@RestController("userOrderController") @RequestMapping("/user/order") @Api(tags = "用户端订单相关接口") @Slf4j public class OrderController { @Autowired private OrderService orderService; @PostMapping("/submit") @ApiOperation("用户下单") public Result<OrderSubmitVO> submit(@RequestBody OrdersSubmitDTO ordersSubmitDTO){ log.info("用户下单,参数为{}", ordersSubmitDTO); OrderSubmitVO orderSubmitVO = orderService.submitOrder(ordersSubmitDTO); return Result.success(orderSubmitVO); } }@PostMapping("/submit")— 接口路径为POST /user/order/submit@RequestBody OrdersSubmitDTO— 前端 JSON 自动反序列化为 DTO 对象@Slf4j— Lombok 自动生成log对象,用于记录日志Result.success(...)— 统一响应格式,包裹返回数据
第2层:DTO — 前端提交的数据结构
OrdersSubmitDTO.java
@Data public class OrdersSubmitDTO implements Serializable { private Long addressBookId; // 地址簿id private int payMethod; // 付款方式(1微信 2支付宝) private String remark; // 备注 private LocalDateTime estimatedDeliveryTime; // 预计送达时间 private Integer deliveryStatus; // 配送状态(1立即送出 0选择具体时间) private Integer tablewareNumber; // 餐具数量 private Integer tablewareStatus; // 餐具数量状态 private Integer packAmount; // 打包费 private BigDecimal amount; // 总金额 }这里前端提交的是用户下单时选择的信息,不包含具体菜品(菜品从购物车取)。
第3层:Service 核心业务逻辑 — 分步详解
OrderServiceImpl.java 的submitOrder方法,整个流程分为6 个步骤:
步骤 ①:校验地址簿是否存在
AddressBook addressBook = addressBookMapper.getById(ordersSubmitDTO.getAddressBookId()); if (addressBook == null) { throw new AddressBookBusinessException(MessageConstant.ADDRESS_BOOK_IS_NULL); }- 根据前端传来的
addressBookId查询地址表 - 如果地址不存在,抛出业务异常,阻止下单
- 同时这个
addressBook对象后面还要用来提取收货人姓名、手机号
步骤 ②:获取当前用户ID + 校验购物车
Long userId = BaseContext.getCurrentId(); ShoppingCart shoppingCart = new ShoppingCart(); shoppingCart.setUserId(userId); List<ShoppingCart> shoppingCartList = shoppingCartMapper.list(shoppingCart); if (shoppingCartList == null || shoppingCartList.size() == 0) { throw new ShoppingCartBusinessException(MessageConstant.SHOPPING_CART_IS_NULL); }BaseContext.getCurrentId()是从ThreadLocal中取出当前登录用户的 ID(由 JWT 拦截器在请求进入时存入)- 用
userId查询购物车表,得到该用户所有购物车商品 - 购物车为空则抛出异常,阻止下单
- 这个
shoppingCartList后面要用来生成订单明细
步骤 ③:组装 Orders 对象并插入订单表
Orders orders = new Orders(); BeanUtils.copyProperties(ordersSubmitDTO, orders); // 拷贝DTO同名属性 orders.setOrderTime(LocalDateTime.now()); // 下单时间 = 当前时间 orders.setPayStatus(Orders.UN_PAID); // 支付状态 = 未支付(0) orders.setStatus(Orders.PENDING_PAYMENT); // 订单状态 = 待付款(1) orders.setNumber(String.valueOf(System.currentTimeMillis())); // 订单号 = 时间戳 orders.setPhone(addressBook.getPhone()); // 从地址中取手机号 orders.setConsignee(addressBook.getConsignee()); // 从地址中取收货人 orders.setUserId(userId); // 设置用户ID orderMapper.insert(orders); // 插入数据库BeanUtils.copyProperties(ordersSubmitDTO, orders)— Spring 的工具类,将 DTO 中同名属性自动拷贝到 Orders 实体(如payMethod、amount、remark等)setNumber(String.valueOf(System.currentTimeMillis()))— 用时间戳作为订单号,简单但有碰撞风险(生产环境应用雪花算法)orderMapper.insert(orders)对应的 XML 中配置了useGeneratedKeys="true" keyProperty="id",插入后orders.getId()会自动回填主键值,后续步骤要用
步骤 ④:遍历购物车,生成订单明细列表
List<OrderDetail> orderDetailList = new ArrayList<>(); for (ShoppingCart cart : shoppingCartList) { OrderDetail orderDetail = new OrderDetail(); BeanUtils.copyProperties(cart, orderDetail); // 拷贝购物车属性 orderDetail.setOrderId(orders.getId()); // 关联订单ID orderDetailList.add(orderDetail); } orderDetailMapper.insertBatch(orderDetailList); // 批量插入BeanUtils.copyProperties(cart, orderDetail)自动拷贝同名属性orderDetail.setOrderId(orders.getId())是手动设置的,因为购物车没有orderId,需要关联到刚创建的订单insertBatch使用 MyBatis 的<foreach>动态拼接多行 VALUES,一条 SQL 插入所有明细
步骤 ⑤:清空购物车
shoppingCartMapper.deleteByUserId(userId);下单成功后,删除该用户购物车中的所有数据。对应的 SQL:
DELETE FROM shopping_cart WHERE user_id = #{userId}步骤 ⑥:封装 VO 返回结果
OrderSubmitVO orderSubmitVO = OrderSubmitVO.builder() .id(orders.getId()) // 订单ID .orderTime(orders.getOrderTime()) // 下单时间 .orderNumber(orders.getNumber()) // 订单号 .orderAmount(orders.getAmount()) // 订单金额 .build(); return orderSubmitVO;OrderSubmitVO 返回给前端 4 个字段:
| 字段 | 说明 |
|---|---|
| id | 订单ID |
| orderNumber | 订单号(时间戳) |
| orderAmount | 订单总金额 |
| orderTime | 下单时间 |
前端拿到这些数据后,跳转到支付页面。
四、完整数据流图
┌─────────────────────────────────────────────────────────────────┐ │ 前端 POST /user/order/submit │ │ Body: { addressBookId, payMethod, amount, remark, ... } │ └──────────────────────────┬──────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────┐ │ OrderController.submit(OrdersSubmitDTO) │ │ → orderService.submitOrder(ordersSubmitDTO) │ └──────────────────────────┬──────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────┐ │ OrderServiceImpl.submitOrder() [@Transactional 事务保护] │ │ │ │ ① 查地址 → addressBookMapper.getById(...) │ │ ├─ null → 抛异常 AddressBookBusinessException │ │ └─ 有值 → 继续 │ │ │ │ ② 查购物车 → shoppingCartMapper.list(userId) │ │ ├─ 空 → 抛异常 ShoppingCartBusinessException │ │ └─ 有数据 → 继续 │ │ │ │ ③ 组装 Orders → orderMapper.insert(orders) │ │ └─ orders.id 自动回填(自增主键) │ │ │ │ ④ 遍历购物车 → 生成 OrderDetail 列表 │ │ └─ orderDetailMapper.insertBatch(list) │ │ │ │ ⑤ 清空购物车 → shoppingCartMapper.deleteByUserId(userId) │ │ │ │ ⑥ 封装 OrderSubmitVO → return │ └──────────────────────────┬──────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────┐ │ 前端收到 Result<OrderSubmitVO> │ │ → 跳转支付页面 │ └─────────────────────────────────────────────────────────────────┘五、关键设计要点总结
| 要点 | 说明 |
|---|---|
@Transactional | 整个方法在一个事务中,步骤③④⑤任意一步失败都会回滚,保证数据一致性 |
useGeneratedKeys="true" | 插入 orders 后自动回填主键 ID,供步骤④使用 |
BeanUtils.copyProperties | 减少手写 setter,DTO→Entity、Cart→OrderDetail 都复用了此工具 |
BaseContext(ThreadLocal) | 从 JWT 令牌解析出的用户 ID 存入 ThreadLocal,Service 层直接取用,无需层层传参 |
| 批量插入 | insertBatch用一条 SQL 插入所有订单明细,避免循环单条插入的性能问题 |
| Builder 模式 | OrderSubmitVO.builder()...build()链式构建对象,代码清晰可读 |