用户下单

需求分析和设计

image-1

  • 业务本质:在电商系统中,用户通过下单通知商家购买行为,触发商家备货和发货流程。外卖系统中特指用户购买菜品/套餐后通知商家。

  • 订单数据构成:

    • 商品信息:来源于购物车数据(商品种类+数量)
    • 金额计算:菜品费用(单价×数量)+其他费用(餐盒费1/商品+固定配送费1/商品+固定配送费1/商品+固定配送费6)
    • 用户信息:从地址簿获取(姓名+地址+手机号)
  • 数据关联性:购物车功能是前置条件,地址簿功能是必要支撑。

  • 完整流程:

    • 添加商品至购物车
    • 点击”去结算”跳转订单页
    • 选择收货地址并确认商品/费用
    • 点击”去支付”生成未支付订单
    • 完成支付跳转成功页
  • 功能定位:用户下单是连接购物车与支付的关键环节,需全局考虑业务关联。

接口设计

  • 请求方式:POST(本质是新增订单数据)

  • 路径设计:/user/order/submit(用户端操作+语义化)

  • 核心参数:

    • addressBookId:必传,获取用户地址信息
    • deliveryStatus:必传,配送状态(1立即送出/0指定时间)
    • tablewareNumber:必传,餐具数量
    • remark:必传,用户备注
  • 可选参数:

    • packAmount:打包费(可后端计算)
    • orderAmount:总金额(可后端计算)
    • estimatedDeliveryTime:预计送达时间(默认下单时间+1h)

接口响应结果

  • 必要返回字段:
    • orderNumber:订单号(前端展示)
    • orderTime:下单时间(用于倒计时计算)
    • orderAmount:订单金额(展示用)
    • id:订单ID(支付环节使用)
  • 设计原则:页面展示所需数据必须返回,隐藏业务字段需考虑后续流程。

数据库设计

订单表

  • 核心字段:
    • number:唯一标识订单的订单号,用户可通过此查询订单
    • status:用数字1-6表示不同状态(1待付款/2待接单/3已接单/4派送中/5已完成/6已取消)
    • user_id:逻辑外键,关联用户表,标识订单所属用户
    • address_book_id:逻辑外键,关联地址表,存储收货地址信息
    • pay_status:支付状态(0未支付/1已支付/2退款)
  • 冗余字段设计:
    • phone/address/consignee:虽然可通过地址表查询,但直接存储可简化订单展示查询
    • 设计原因:避免多表关联查询,提升业务处理效率
  • 状态管理字段:
    • cancel_reason:记录用户或商家取消订单的原因
    • rejection_reason:存储商家拒单的具体原因
    • cancel_time:精确记录订单取消时间
  • 业务关键字段:
    • amount:使用decimal(10,2)存储订单总金额,确保精度
    • estimated_delivery_time:用户关心的预计送达时间
    • tableware_status:餐具提供方式(1按餐量提供/0选择具体数量)

订单明细表

  • 关联设计:
    • order_id:逻辑外键关联订单表,建立一对多关系(一个订单有多个明细)
    • dish_id/setmeal_id:分别关联菜品和套餐表,支持两种商品类型
  • 冗余字段优化:
    • name/image:存储商品名称和图片路径,避免展示时联表查询
    • 设计优势:明细展示只需单表查询,提升系统响应速度
  • 商品信息记录:
    • dish_flavor:存储用户选择的菜品口味定制信息
    • number:记录同个商品的下单数量
    • amount:存储商品单价(注意与订单表总金额区分)
  • 业务扩展性:
    • 通过dish_id和setmeal_id的灵活使用,同时支持单品和套餐订单
    • 口味字段设计为varchar(50),适应不同菜品的多样化需求
public class OrdersSubmitDTO implements Serializable {  
    //地址簿id  
    private Long addressBookId;  
    //付款方式  
    private int payMethod;  
    //备注  
    private String remark;  
    //预计送达时间  
    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss")  
    private LocalDateTime estimatedDeliveryTime;  
    //配送状态  1立即送出  0选择具体时间  
    private Integer deliveryStatus;  
    //餐具数量  
    private Integer tablewareNumber;  
    //餐具数量状态  1按餐量提供  0选择具体数量  
    private Integer tablewareStatus;  
    //打包费  
    private Integer packAmount;  
    //总金额  
    private BigDecimal amount;  
}
public class Orders implements Serializable {  
  
    /**  
     * 订单状态 1待付款 2待接单 3已接单 4派送中 5已完成 6已取消  
     */  
    public static final Integer PENDING_PAYMENT = 1;  
    public static final Integer TO_BE_CONFIRMED = 2;  
    public static final Integer CONFIRMED = 3;  
    public static final Integer DELIVERY_IN_PROGRESS = 4;  
    public static final Integer COMPLETED = 5;  
    public static final Integer CANCELLED = 6;  
  
    /**  
     * 支付状态 0未支付 1已支付 2退款  
     */  
    public static final Integer UN_PAID = 0;  
    public static final Integer PAID = 1;  
    public static final Integer REFUND = 2;  
  
    private static final long serialVersionUID = 1L;  
  
    private Long id;  
  
    //订单号  
    private String number;  
  
    //订单状态 1待付款 2待接单 3已接单 4派送中 5已完成 6已取消 7退款  
    private Integer status;  
  
    //下单用户id  
    private Long userId;  
  
    //地址id  
    private Long addressBookId;  
  
    //下单时间  
    private LocalDateTime orderTime;  
  
    //结账时间  
    private LocalDateTime checkoutTime;  
  
    //支付方式 1微信,2支付宝  
    private Integer payMethod;  
  
    //支付状态 0未支付 1已支付 2退款  
    private Integer payStatus;  
  
    //实收金额  
    private BigDecimal amount;  
  
    //备注  
    private String remark;  
  
    //用户名  
    private String userName;  
  
    //手机号  
    private String phone;  
  
    //地址  
    private String address;  
  
    //收货人  
    private String consignee;  
  
    //订单取消原因  
    private String cancelReason;  
  
    //订单拒绝原因  
    private String rejectionReason;  
  
    //订单取消时间  
    private LocalDateTime cancelTime;  
  
    //预计送达时间  
    private LocalDateTime estimatedDeliveryTime;  
  
    //配送状态  1立即送出  0选择具体时间  
    private Integer deliveryStatus;  
  
    //送达时间  
    private LocalDateTime deliveryTime;  
  
    //打包费  
    private int packAmount;  
  
    //餐具数量  
    private int tablewareNumber;  
  
    //餐具数量状态  1按餐量提供  0选择具体数量  
    private Integer tablewareStatus;  
}
public OrderSubmitVO submitOrder(OrdersSubmitDTO ordersSubmitDTO) {  
  
    // 处理各种业务异常(地址簿为空、购物车数据为空)  
    AddressBook addressBook = addressBookMapper.getById(ordersSubmitDTO.getAddressBookId());  
    if (addressBook == null) {  
        throw new AddressBookBusinessException(MessageConstant.ADDRESS_BOOK_IS_NULL);  
    }  
    Long userId = BaseContext.getCurrentId();  
    ShoppingCart cart = ShoppingCart.builder()  
            .userId(userId)  
            .build();  
    List<ShoppingCart> shoppingCartList = shoppingCartMapper.list(cart);  
    if (shoppingCartList == null || shoppingCartList.size() == 0) {  
        throw new ShoppingCartBusinessException(MessageConstant.SHOPPING_CART_IS_NULL);  
    }  
  
    // 向订单表插入一条数据  
    Orders orders = new Orders();  
    BeanUtils.copyProperties(ordersSubmitDTO, orders);  
    orders.setOrderTime(LocalDateTime.now());  
    orders.setPayStatus(Orders.UN_PAID);  
    orders.setStatus(Orders.PENDING_PAYMENT);  
    orders.setNumber(String.valueOf(System.currentTimeMillis()));  
    orders.setPhone(addressBook.getPhone());  
    orders.setConsignee(addressBook.getConsignee());  
    orders.setUserId(userId);  
  
    orderMapper.insert(orders);  
  
    List<OrderDetail> orderDetailList = new ArrayList<>();  
    // 向订单明细表插入n条数据  
    for (ShoppingCart shoppingCart : shoppingCartList) {  
        OrderDetail orderDetail = new OrderDetail();  
        BeanUtils.copyProperties(shoppingCart, orderDetail);  
        orderDetail.setOrderId(orders.getId()); // 订单id,插入数据库时会自动填充  
        orderDetailList.add(orderDetail);  
    }  
    orderDetailMapper.insertBatch(orderDetailList);  
  
    // 清空当前用户的购物车数据  
    shoppingCartMapper.deleteByUserId(userId);  
  
    // 封装VO返回结果  
    OrderSubmitVO orderSubmitVO = OrderSubmitVO.builder()  
            .id(orders.getId())  
            .orderNumber(orders.getNumber())  
            .orderAmount(orders.getAmount())  
            .orderTime(orders.getOrderTime())  
            .build();  
  
    return orderSubmitVO;  
}

订单支付

image-2

跳过微信支付

小程序pages/pay/index.js中224行,将

        (0, _api.paymentOrder)(params).then(function (res) {
          if (res.code === 1) {
            wx.requestPayment({
              nonceStr: res.data.nonceStr,
              package: res.data.packageStr,
              paySign: res.data.paySign,
              timeStamp: res.data.timeStamp,
              signType: res.data.signType,
              success:function(res){
                wx.showModal({
                  title: '提示',
                  content: '支付成功',
                  success:function(){
                    uni.redirectTo({url: '/pages/success/index?orderId=' + _this.orderId });
                  }
                })
                console.log('支付成功!')
              }
            })
          } else {
            wx.showModal({
              title: '提示',
              content: res.msg
            })
          }
        });

修改为

        (0, _api.paymentOrder)(params).then(function (res) {
          if (res.code === 1) {
            wx.showModal({
              title: '提示',
              content: '支付成功',
              success:function(){
                uni.redirectTo({url: '/pages/success/index?orderId=' + _this.orderId });
              }
            })
          } else {
            wx.showModal({
              title: '提示',
              content: res.msg
            })
          }
        });

后端将原orderService.payment(ordersPaymentDTO)替换为orderService.paymentSkipWeChatPay(ordersPaymentDTO)

@PutMapping("/payment")  
@ApiOperation("订单支付")  
public Result<OrderPaymentVO> payment(@RequestBody OrdersPaymentDTO ordersPaymentDTO) throws Exception {  
    log.info("订单支付:{}", ordersPaymentDTO);  
    OrderPaymentVO orderPaymentVO = orderService.paymentSkipWeChatPay(ordersPaymentDTO);  
    log.info("生成预支付交易单:{}", orderPaymentVO);  
    return Result.success(orderPaymentVO);  
}
public OrderPaymentVO paymentSkipWeChatPay(OrdersPaymentDTO ordersPaymentDTO) {  
    paySuccess(ordersPaymentDTO.getOrderNumber());  
    OrderPaymentVO vo = new OrderPaymentVO();  
    return vo;  
}

直接调用原来的支付成功的方法,修改订单状态

public void paySuccess(String outTradeNo) {  
  
    // 根据订单号查询订单  
    Orders ordersDB = orderMapper.getByNumber(outTradeNo);  
  
    // 根据订单id更新订单的状态、支付方式、支付状态、结账时间  
    Orders orders = Orders.builder()  
            .id(ordersDB.getId())  
            .status(Orders.TO_BE_CONFIRMED)  
            .payStatus(Orders.PAID)  
            .checkoutTime(LocalDateTime.now())  
            .build();  
  
    orderMapper.update(orders);  
}