侧边栏壁纸
博主头像
峰峰火火博主等级

一条咸鱼罢了

  • 累计撰写 131 篇文章
  • 累计创建 97 个标签
  • 累计收到 59 条评论

目 录CONTENT

文章目录

电商下单系统技术

峰峰火火
2026-02-10 / 0 评论 / 0 点赞 / 8 阅读 / 3,221 字 / 正在检测是否收录...
温馨提示:
若内容或图片失效,请留言反馈。部分素材来自网络,若不小心影响到您的利益,请联系我们删除。

电商下单系统技术

一、概述

本文档详细分析三种不同类型的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;
}

十、总结

三种方案各有侧重:

  1. 小程序点餐方案:适合高频、实时的餐饮场景,强调分布式锁和即时性
  2. 商城方案A:适合传统电商,注重事务完整性和多种营销活动
  3. 商城方案B:适合微服务架构,强调解耦和扩展性

在实际项目中,可以根据业务场景选择合适的方案,或结合多种方案的优点进行定制开发。

0

评论区