电商下单系统技术
一、概述
本文档详细分析三种不同类型的Java电商下单实现方案,涵盖小程序点餐、传统电商商城和基于Trade交易模式的订单创建流程。每个方案都展示了不同的架构设计和业务处理模式。
二、代码结构概览
// 三种下单方案对比
1. 小程序点餐下单 - 基于Spring + MyBatis + Redisson
2. 商城下单方案A - 基于Spring + Redis事务管理
3. 商城下单方案B - 基于Spring Cloud + RocketMQ分布式事务
三、小程序点餐下单实现详解
3.1 方法签名与事务配置
@Override
@Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
public Map<String, Object> createOrder(Long uid, AppOrderParam param) {
// @Transactional注解说明:
// 1. propagation = Propagation.REQUIRED: 如果当前存在事务,则加入该事务;否则创建新事务
// 2. rollbackFor = Exception.class: 所有异常都触发回滚
// 3. 确保订单创建、库存扣减、优惠券使用等操作在同一事务中
}
3.2 核心业务逻辑分步解析
步骤1:基础参数校验
// 桌号点餐必须提供桌号
if(OrderLogEnum.ORDER_TAKE_DESK.getValue().equals(param.getOrderType())
&& StrUtil.isBlank(param.getDeskNumber())){
throw exception(STORE_ORDER_DESK_NOT); // 自定义异常抛出
}
// 参数列表转换
List<String> productIds = param.getProductId(); // 商品ID列表
List<String> numbers = param.getNumber(); // 商品数量列表
List<String> specs = param.getSpec(); // 商品规格列表
// 注意:这三个列表必须保持相同的索引顺序
步骤2:分布式锁保护库存检查
// 使用Redisson实现分布式锁,防止超卖
RLock lock = redissonClient.getLock(LOCK_KEY);
if (lock.tryLock()){ // 尝试获取锁,非阻塞方式
try {
for (int i = 0; i < productIds.size(); i++){
// 规格字符串处理:将"|"分隔符替换为","
String newSku = StrUtil.replace(specs.get(i), "|", ",");
// 核心库存检查方法
appStoreProductService.checkProductStock(
uid,
Long.valueOf(productIds.get(i)), // 商品ID
Integer.valueOf(numbers.get(i)), // 购买数量
newSku // SKU规格
);
// 累计商品总数
totalNum += Integer.valueOf(numbers.get(i));
// 查询商品规格信息获取单价
StoreProductAttrValueDO storeProductAttrValue = storeProductAttrValueService
.getOne(Wrappers.<StoreProductAttrValueDO>lambdaQuery()
.eq(StoreProductAttrValueDO::getSku, newSku)
.eq(StoreProductAttrValueDO::getProductId, productIds.get(i)));
// 计算商品总价:数量 × 单价
sumPrice = NumberUtil.add(sumPrice, NumberUtil.mul(
numbers.get(i),
storeProductAttrValue.getPrice().toString()
));
}
} finally {
lock.unlock(); // 必须确保锁被释放
}
}
步骤3:优惠券验证与计算
if(StrUtil.isNotBlank(param.getCouponId())){
CouponUserDO couponUserDO = appCouponUserService.getById(param.getCouponId());
if(couponUserDO != null){
// 优惠券使用条件检查:订单金额必须达到最低使用门槛
if(couponUserDO.getLeast().compareTo(sumPrice) > 0) {
throw exception(COUPON_NOT_CONDITION);
}
couponPrice = couponUserDO.getValue(); // 优惠金额
// 更新优惠券状态为"已使用"
couponUserDO.setStatus(ShopCommonEnum.IS_STATUS_1.getValue());
appCouponUserService.updateById(couponUserDO);
}
}
步骤4:价格计算策略
// 外卖订单:商品总价 + 运费 - 优惠券 - 抵扣金额
if(OrderLogEnum.ORDER_TAKE_OUT.getValue().equals(param.getOrderType())){
payPrice = NumberUtil.sub(
NumberUtil.add(sumPrice, postagePrice), // 总价+运费
couponPrice, // 优惠券
deductionPrice // 抵扣金额
);
} else {
// 堂食订单:无运费
payPrice = NumberUtil.sub(sumPrice, couponPrice, deductionPrice);
}
// 计算奖励积分(基于商品配置)
BigDecimal gainIntegral = this.getGainIntegral(productIds);
步骤5:订单数据组装
// 生成分布式唯一订单号(雪花算法)
String orderSn = IdUtil.getSnowflake(0, 0).nextIdStr();
// 创建取餐号记录(适用于快餐场景)
OrderNumberDO orderNumberDO = OrderNumberDO.builder().orderId(orderSn).build();
orderNumberMapper.insert(orderNumberDO);
// 设置订单基础信息
storeOrder.setOrderId(orderSn);
storeOrder.setUid(uid);
storeOrder.setTotalNum(totalNum);
storeOrder.setTotalPrice(sumPrice);
storeOrder.setPayPrice(payPrice);
storeOrder.setCouponPrice(couponPrice);
// 外卖订单地址处理
if(OrderLogEnum.ORDER_TAKE_OUT.getValue().equals(param.getOrderType())){
if (StrUtil.isEmpty(param.getAddressId())) {
throw exception(SELECT_ADDRESS);
}
UserAddressDO userAddress = appUserAddressService.getById(param.getAddressId());
storeOrder.setUserAddress(userAddress.getAddress() + " " + userAddress.getDetail());
}
// 支付状态初始化为未支付
storeOrder.setPaid(OrderInfoEnum.PAY_STATUS_0.getValue());
// 保存订单主表
boolean res = this.save(storeOrder);
if (!res) {
throw exception(ORDER_GEN_FAIL);
}
步骤6:库存扣减与后续处理
// 扣减库存,增加销量
this.deStockIncSale(productIds, numbers, specs);
// 异步保存购物车商品信息(提升性能)
storeOrderCartInfoService.saveCartInfo(
storeOrder.getId(),
storeOrder.getOrderId(),
productIds,
numbers,
specs
);
// 记录订单状态变更
storeOrderStatusService.create(
uid,
storeOrder.getId(),
OrderLogEnum.CREATE_ORDER.getValue(),
OrderLogEnum.CREATE_ORDER.getDesc()
);
// 非堂食订单加入延时取消队列(30分钟未支付自动取消)
if(!OrderLogEnum.ORDER_TAKE_DESK.getValue().equals(param.getOrderType())) {
RBlockingDeque<Object> blockingDeque = redissonClient
.getBlockingDeque(ShopConstants.REDIS_ORDER_OUTTIME_UNPAY_QUEUE);
RDelayedQueue<Object> delayedQueue = redissonClient.getDelayedQueue(blockingDeque);
delayedQueue.offer(
OrderMsg.builder().orderId(orderSn).build(),
ShopConstants.ORDER_OUTTIME_UNPAY,
TimeUnit.MINUTES
);
}
四、商城下单方案A详解
4.1 预订单缓存机制
// 从Redis获取预下单信息(防止重复提交)
String key = "user_order:" + request.getPreOrderNo();
String orderVoString = redisUtil.get(key).toString();
OrderInfoVo orderInfoVo = JSONObject.parseObject(orderVoString, OrderInfoVo.class);
// 设计目的:将复杂的订单计算前置,下单时直接使用计算结果
4.2 多重校验策略
// 1. 支付方式校验
if (!orderUtils.checkPayType(request.getPayType())) {
throw new CrmebException("暂不支持该支付方式");
}
// 2. 微信支付渠道校验
if (request.getPayType().equals(PayConstants.PAY_TYPE_WE_CHAT)) {
if (!OrderUtils.checkPayChannel(request.getPayChannel())) {
throw new CrmebException("支付渠道不存在!");
}
}
// 3. 库存校验(核心防超卖逻辑)
List<MyRecord> skuRecordList = validateProductStock(orderInfoVo, user);
// 4. 配送方式校验
if (request.getShippingType() == 1) { // 快递配送
if (request.getAddressId() <= 0) throw new CrmebException("请选择收货地址");
UserAddress userAddress = userAddressService.getById(request.getAddressId());
} else if (request.getShippingType() == 2) { // 到店自提
// 自提开关检查
String storeSelfMention = systemConfigService
.getValueByKey(SysConfigConstants.CONFIG_KEY_STORE_SELF_MENTION);
if ("false".equals(storeSelfMention)) {
throw new CrmebException("请先联系管理员开启门店自提");
}
}
// 5. 活动商品特殊校验(秒杀、拼团、砍价)
if (ObjectUtil.isNotNull(orderInfoVo.getSeckillId()) && orderInfoVo.getSeckillId() > 0) {
commonValidateSeckill(storeSeckill, seckillAttrValue, user, detailVo.getPayNum());
}
4.3 价格计算服务化
// 通过独立的价格计算服务处理复杂优惠逻辑
OrderComputedPriceRequest orderComputedPriceRequest = new OrderComputedPriceRequest();
orderComputedPriceRequest.setShippingType(request.getShippingType());
orderComputedPriceRequest.setAddressId(request.getAddressId());
orderComputedPriceRequest.setCouponId(request.getCouponId());
orderComputedPriceRequest.setUseIntegral(request.getUseIntegral());
// 计算最终价格(包含运费、优惠、积分抵扣等)
ComputedOrderPriceResponse computedOrderPriceResponse =
computedPrice(orderComputedPriceRequest, orderInfoVo, user);
4.4 订单详情构建
// 构建订单商品明细
List<StoreOrderInfo> storeOrderInfos = new ArrayList<>();
for (OrderInfoDetailVo detailVo : orderInfoVo.getOrderDetailList()) {
StoreOrderInfo soInfo = new StoreOrderInfo();
soInfo.setProductId(detailVo.getProductId());
soInfo.setInfo(JSON.toJSON(detailVo).toString()); // JSON序列化存储商品快照
soInfo.setUnique(detailVo.getAttrValueId().toString()); // SKU唯一标识
soInfo.setOrderNo(orderNo);
soInfo.setProductName(detailVo.getProductName());
soInfo.setPrice(detailVo.getPrice());
soInfo.setPayNum(detailVo.getPayNum());
soInfo.setIsReply(false); // 是否已评价
soInfo.setIsSub(detailVo.getIsSub()); // 是否子商品
soInfo.setProductType(detailVo.getProductType()); // 商品类型
storeOrderInfos.add(soInfo);
}
4.5 事务处理模板
// 使用编程式事务管理,更精细控制事务边界
Boolean execute = transactionTemplate.execute(e -> {
// 根据活动类型执行不同的库存扣减策略
if (storeOrder.getSeckillId() > 0) { // 秒杀商品
// 扣减秒杀活动库存
storeSeckillService.operationStock(activityId, num, "sub");
// 扣减秒杀商品规格库存
storeProductAttrValueService.operationStock(
activityAttrValueId, num, "sub", Constants.PRODUCT_TYPE_SECKILL
);
// 扣减普通商品库存
storeProductService.operationStock(productId, num, "sub");
} else if (storeOrder.getBargainId() > 0) { // 砍价商品
// 类似逻辑处理砍价
} else if (storeOrder.getCombinationId() > 0) { // 拼团商品
// 类似逻辑处理拼团
} else { // 普通商品
for (MyRecord skuRecord : skuRecordList) {
storeProductService.operationStock(
skuRecord.getInt("productId"),
skuRecord.getInt("num"),
"sub"
);
}
}
// 保存订单主表
storeOrderService.create(storeOrder);
// 设置订单ID并保存明细
storeOrderInfos.forEach(info -> info.setOrderId(storeOrder.getId()));
storeOrderInfoService.saveOrderInfos(storeOrderInfos);
// 更新优惠券状态
if (storeOrder.getCouponId() > 0) {
storeCouponUserService.updateById(finalStoreCouponUser);
}
// 生成订单日志
storeOrderStatusService.createLog(
storeOrder.getId(),
Constants.ORDER_STATUS_CACHE_CREATE_ORDER,
"订单生成"
);
// 清除购物车
if (CollUtil.isNotEmpty(orderInfoVo.getCartIdList())) {
storeCartService.deleteCartByIds(orderInfoVo.getCartIdList());
}
return Boolean.TRUE;
});
4.6 后置异步处理
// 清理预订单缓存
if (redisUtil.exists(key)) {
redisUtil.delete(key);
}
// 加入自动取消队列(Redis List实现)
redisUtil.lPush(Constants.ORDER_AUTO_CANCEL_KEY, storeOrder.getOrderId());
// 发送管理通知
sendAdminOrderNotice(storeOrder.getOrderId());
五、商城下单方案B(Trade模式)详解
5.1 交易对象构建模式
public Trade createTrade(TradeParams tradeParams) {
// 1. 确定购物车类型
CartTypeEnum cartTypeEnum = getCartType(tradeParams.getWay());
// 2. 读取购物车数据到TradeDTO
TradeDTO tradeDTO = this.readDTO(cartTypeEnum);
// 3. 设置交易参数
tradeDTO.setClientType(tradeParams.getClient());
tradeDTO.setStoreRemark(tradeParams.getRemark());
// 4. 地址校验
if(tradeDTO.getStoreAddress() == null){
if (tradeDTO.getMemberAddress() == null) {
throw new ServiceException(ResultCode.MEMBER_ADDRESS_NOT_EXIST);
}
}
// 5. 构建并返回交易对象
return tradeBuilder.createTrade(tradeDTO);
}
5.2 购物车渲染策略模式
public Trade createTrade(TradeDTO tradeDTO) {
// 根据购物车类型选择不同的渲染策略
if (isSingle(tradeDTO.getCartTypeEnum())) {
// 单商品交易渲染
renderCartBySteps(tradeDTO, RenderStepStatement.singleTradeRender);
} else if (tradeDTO.getCartTypeEnum().equals(CartTypeEnum.PINTUAN)) {
// 拼团交易渲染
renderCartBySteps(tradeDTO, RenderStepStatement.pintuanTradeRender);
} else {
// 普通交易渲染
renderCartBySteps(tradeDTO, RenderStepStatement.tradeRender);
}
// 渲染步骤可能包括:
// 1. 商品有效性检查
// 2. 价格计算
// 3. 优惠计算
// 4. 库存预检查
}
5.3 事务性交易创建
@Override
@Transactional(rollbackFor = Exception.class)
public Trade createTrade(TradeDTO tradeDTO) {
// 1. 预校验
createTradeCheck(tradeDTO);
// 2. 创建交易对象
Trade trade = new Trade(tradeDTO);
// 3. 缓存键生成
String key = CachePrefix.TRADE.getPrefix() + trade.getSn();
// 4. 优惠预处理
couponPretreatment(tradeDTO); // 优惠券计算
pointPretreatment(tradeDTO); // 积分计算
// 5. 保存交易
this.save(trade);
// 6. 生成订单(可能涉及分库分表)
orderService.intoDB(tradeDTO);
// 7. 活动处理(砍价等)
kanjiaPretreatment(tradeDTO);
// 8. 交易数据缓存(供后续流程使用)
cache.put(key, JSONUtil.toJsonStr(tradeDTO));
// 9. 发送订单创建消息(异步解耦)
String destination = rocketmqCustomProperties.getOrderTopic() + ":" +
OrderTagsEnum.ORDER_CREATE.name();
rocketMQTemplate.asyncSend(
destination,
key,
RocketmqSendCallbackBuilder.commonCallback()
);
return trade;
}
六、核心设计模式对比
6.1 事务管理策略
| 方案 | 事务管理 | 特点 |
|---|---|---|
| 小程序点餐 | 声明式事务(@Transactional) | 简单直观,适合单服务场景 |
| 商城方案A | 编程式事务(TransactionTemplate) | 更灵活,可定制事务边界 |
| 商城方案B | 声明式事务+消息队列 | 分布式事务,最终一致性 |
6.2 库存控制机制
| 方案 | 库存控制 | 适用场景 |
|---|---|---|
| 小程序点餐 | Redisson分布式锁 | 高并发秒杀场景 |
| 商城方案A | 数据库事务+行级锁 | 传统电商,事务性强 |
| 商城方案B | 未展示,可能使用Redis扣减 | 高并发分布式场景 |
6.3 订单号生成策略
// 方案A:时间戳+随机数
String orderNo = CrmebUtil.getOrderNo("order");
// 方案B:雪花算法
String orderSn = IdUtil.getSnowflake(0, 0).nextIdStr();
// 对比:
// 雪花算法:分布式唯一,趋势递增,无需中心化生成器
// 时间戳+随机数:简单,但可能冲突,需要额外校验
6.4 异常处理策略
// 统一异常抛出
throw exception(STORE_ORDER_DESK_NOT); // 小程序点餐
throw new CrmebException("预下单订单不存在"); // 商城方案A
throw new ServiceException(ResultCode.MEMBER_ADDRESS_NOT_EXIST); // 商城方案B
// 建议:统一异常编码和消息,便于前端处理和监控
七、性能优化建议
7.1 数据库优化
// 1. 批量插入优化
storeOrderInfoService.saveOrderInfos(storeOrderInfos); // 使用批量插入
// 2. 索引优化
// 订单表:uid + create_time 复合索引
// 订单明细表:order_id + product_id 复合索引
// 3. 读写分离
// 订单创建写主库,订单查询读从库
7.2 缓存优化
// 1. 预订单缓存
String key = "user_order:" + request.getPreOrderNo();
redisUtil.set(key, orderInfoVo, 30, TimeUnit.MINUTES); // 30分钟过期
// 2. 热点数据缓存
// 商品信息、用户信息、地址信息等
7.3 异步处理
// 1. 非核心业务异步化
storeOrderCartInfoService.saveCartInfo(...); // 购物车信息保存
// 2. 消息队列解耦
rocketMQTemplate.asyncSend(destination, key, callback); // 订单创建消息
// 3. 延迟任务
delayedQueue.offer(orderMsg, 30, TimeUnit.MINUTES); // 30分钟未支付取消
八、安全性考虑
8.1 防重复提交
// 1. 预订单号机制
String preOrderNo = generatePreOrderNo(); // 前端生成
redisUtil.set("pre_order:" + preOrderNo, "1", 5, TimeUnit.SECONDS); // 5秒有效期
// 2. 幂等性设计
if (redisUtil.exists("order_create:" + uid + ":" + preOrderNo)) {
throw new BusinessException("请勿重复提交");
}
8.2 数据一致性
// 1. 事务边界明确
@Transactional(rollbackFor = Exception.class)
public Map<String, Object> createOrder(...) {
// 订单创建、库存扣减、优惠券使用在同一事务
}
// 2. 最终一致性补偿
// 消息队列 + 定时任务检查
九、监控与日志
9.1 关键指标监控
// 1. 下单成功率
Metrics.counter("order.create.success").increment();
Metrics.counter("order.create.fail").increment();
// 2. 下单耗时
long startTime = System.currentTimeMillis();
// ... 下单逻辑
long cost = System.currentTimeMillis() - startTime;
Metrics.timer("order.create.time").record(cost, TimeUnit.MILLISECONDS);
// 3. 库存扣减失败率
Metrics.counter("stock.deduct.fail").increment();
9.2 详细日志记录
log.info("订单创建开始: uid={}, productIds={}", uid, productIds);
try {
// 业务逻辑
log.info("库存检查通过: uid={}", uid);
log.info("价格计算完成: totalPrice={}, payPrice={}", sumPrice, payPrice);
log.info("订单保存成功: orderSn={}", orderSn);
} catch (Exception e) {
log.error("订单创建失败: uid={}, error={}", uid, e.getMessage(), e);
throw e;
}
十、总结
三种方案各有侧重:
- 小程序点餐方案:适合高频、实时的餐饮场景,强调分布式锁和即时性
- 商城方案A:适合传统电商,注重事务完整性和多种营销活动
- 商城方案B:适合微服务架构,强调解耦和扩展性
在实际项目中,可以根据业务场景选择合适的方案,或结合多种方案的优点进行定制开发。
评论区