用户下单
需求分析和设计

-
业务本质:在电商系统中,用户通过下单通知商家购买行为,触发商家备货和发货流程。外卖系统中特指用户购买菜品/套餐后通知商家。
-
订单数据构成:
- 商品信息:来源于购物车数据(商品种类+数量)
- 金额计算:菜品费用(单价×数量)+其他费用(餐盒费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;
}订单支付

跳过微信支付
小程序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);
}