Explorar el Código

优化创建和支付接口

yuhongqi hace 1 semana
padre
commit
f1fc236d0b

+ 4 - 1
fs-service/src/main/java/com/fs/hisStore/domain/FsShippingTemplatesRegionScrm.java

@@ -5,9 +5,12 @@ import java.math.BigDecimal;
 import com.baomidou.mybatisplus.annotation.TableName;
 import com.fs.common.annotation.Excel;
 import com.fs.common.core.domain.BaseEntity;
+import lombok.AllArgsConstructor;
 import lombok.Builder;
-import lombok.Data;
+import lombok.NoArgsConstructor;
 
+@NoArgsConstructor
+@AllArgsConstructor
 @Builder
 /**
  * 邮费区域对象 fs_shipping_templates_region

+ 31 - 0
fs-service/src/main/java/com/fs/live/constant/LiveOrderOptConstants.java

@@ -0,0 +1,31 @@
+package com.fs.live.constant;
+
+/**
+ * 直播订单优化版接口常量(测试数据标识便于批量清理)
+ */
+public final class LiveOrderOptConstants {
+
+    private LiveOrderOptConstants() {
+    }
+
+    /** 测试订单 order_create_type,清理:WHERE order_create_type = 99 */
+    public static final int ORDER_CREATE_TYPE_LIVE_TEST = 99;
+
+    /** 测试订单 mark,清理:WHERE mark = 'LIVE_TEST_DATA' */
+    public static final String TEST_ORDER_MARK = "LIVE_TEST_DATA";
+
+    /** 测试支付 remark,清理:WHERE remark = 'LIVE_TEST_PAYMENT' */
+    public static final String TEST_PAYMENT_REMARK = "LIVE_TEST_PAYMENT";
+
+    public static final String CACHE_LIVE_GOODS = "live:goods:";
+    public static final String CACHE_LIVE_FIRST_ENTRY = "live:firstEntry:";
+    public static final String CACHE_SYS_CONFIG = "sys:config:";
+    public static final String CACHE_MINIAPP_PAY = "fs:miniapp:pay:";
+    public static final String CACHE_CITY_SHIPPING_CHAIN = "fs:city:shipping:chain:";
+    public static final String CACHE_PRODUCT_ATTR_ID = "fs:product:attr:id:";
+    public static final String CACHE_PRODUCT_ATTR_LIST = "fs:product:attr:list:";
+    public static final String CACHE_SHIPPING_TEMPLATE = "fs:shipping:template:ids:";
+    public static final String CACHE_SHIPPING_REGION = "fs:shipping:region:";
+    public static final String CACHE_LIVE_COUPON_USER = "live:coupon:user:";
+    public static final String LOCK_ORDER_KEY_CREATING = "orderKey:creating:";
+}

+ 31 - 0
fs-service/src/main/java/com/fs/live/service/ILiveOrderOptService.java

@@ -0,0 +1,31 @@
+package com.fs.live.service;
+
+import com.fs.common.core.domain.R;
+import com.fs.live.domain.LiveOrder;
+import com.fs.live.param.LiveOrderPayParam;
+
+/**
+ * 直播订单优化版 Service(create/pay 性能与正确性优化,不影响原接口)
+ */
+public interface ILiveOrderOptService {
+
+    /**
+     * 优化版创建商城订单
+     */
+    R createStoreOrder(LiveOrder param);
+
+    /**
+     * 测试创建商城订单(order_create_type=99 + mark=LIVE_TEST_DATA,便于删除)
+     */
+    R createStoreOrderTest(LiveOrder param);
+
+    /**
+     * 优化版支付(锁内校验、配置缓存、无 Controller 大事务)
+     */
+    R handleStoreOrderPay(LiveOrderPayParam param);
+
+    /**
+     * 测试支付(payment.remark=LIVE_TEST_PAYMENT,不调第三方支付网关)
+     */
+    R handleStoreOrderPayTest(LiveOrderPayParam param);
+}

+ 5 - 0
fs-service/src/main/java/com/fs/live/service/ILiveOrderService.java

@@ -190,6 +190,11 @@ public interface ILiveOrderService {
 
     LiveOrderComputeDTO computedOrder(long l, LiveOrderComputedParam param);
 
+    /**
+     * 缓存优化版订单金额计算
+     */
+    LiveOrderComputeDTO computedOrderCache(long userId, LiveOrderComputedParam param);
+
     LiveOrder selectOrderIdByOrderCode(String platformCode);
 
     R auditPayRemain(Long orderId);

+ 1002 - 0
fs-service/src/main/java/com/fs/live/service/impl/LiveOrderOptServiceImpl.java

@@ -0,0 +1,1002 @@
+package com.fs.live.service.impl;
+
+import cn.hutool.core.util.NumberUtil;
+import cn.hutool.core.util.ObjectUtil;
+import cn.hutool.json.JSONUtil;
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.TypeReference;
+import com.fs.common.core.domain.R;
+import com.fs.common.core.redis.RedisCache;
+import com.fs.common.core.redis.service.StockDeductService;
+import com.fs.common.exception.CustomException;
+import com.fs.common.utils.ServletUtils;
+import com.fs.common.utils.SnowflakeUtil;
+import com.fs.common.utils.StringUtils;
+import com.fs.common.utils.ip.IpUtils;
+import com.fs.config.cloud.CloudHostProper;
+import com.fs.course.domain.FsCoursePlaySourceConfig;
+import com.fs.course.mapper.FsCoursePlaySourceConfigMapper;
+import com.fs.his.domain.FsPayConfig;
+import com.fs.his.domain.MerchantAppConfig;
+import com.fs.his.mapper.MerchantAppConfigMapper;
+import com.fs.hisStore.config.StoreConfig;
+import com.fs.hisStore.constants.StoreConstants;
+import com.fs.hisStore.domain.*;
+import com.fs.hisStore.dto.FsStoreCartDTO;
+import com.fs.hisStore.dto.TemplateDTO;
+import com.fs.hisStore.enums.OrderInfoEnum;
+import com.fs.hisStore.enums.OrderLogEnum;
+import com.fs.hisStore.enums.ShippingTempEnum;
+import com.fs.hisStore.mapper.FsStoreOrderItemScrmMapper;
+import com.fs.hisStore.mapper.FsStoreOrderScrmMapper;
+import com.fs.hisStore.mapper.FsStorePaymentScrmMapper;
+import com.fs.hisStore.mapper.FsStoreProductAttrValueScrmMapper;
+import com.fs.hisStore.service.IFsCityScrmService;
+import com.fs.hisStore.service.IFsStoreOrderScrmService;
+import com.fs.hisStore.service.IFsStoreOrderStatusScrmService;
+import com.fs.hisStore.service.IFsStoreProductPurchaseLimitScrmService;
+import com.fs.hisStore.service.IFsStoreProductScrmService;
+import com.fs.hisStore.service.IFsUserScrmService;
+import com.fs.huifuPay.domain.HuiFuCreateOrder;
+import com.fs.huifuPay.domain.HuifuCreateOrderResult;
+import com.fs.huifuPay.sdk.opps.core.utils.HuiFuUtils;
+import com.fs.huifuPay.service.HuiFuService;
+import com.fs.live.constant.LiveOrderOptConstants;
+import com.fs.live.domain.LiveCouponUser;
+import com.fs.live.domain.LiveGoods;
+import com.fs.live.domain.LiveOrder;
+import com.fs.live.domain.LiveUserFirstEntry;
+import com.fs.live.mapper.LiveGoodsMapper;
+import com.fs.live.param.LiveOrderPayParam;
+import com.fs.live.service.ILiveCouponUserService;
+import com.fs.live.service.ILiveOrderOptService;
+import com.fs.live.service.ILiveService;
+import com.fs.live.service.ILiveUserFirstEntryService;
+import com.fs.live.vo.LiveGoodsUploadMqVo;
+import com.fs.system.service.ISysConfigService;
+import com.github.binarywang.wxpay.bean.order.WxPayMpOrderResult;
+import com.github.binarywang.wxpay.bean.request.WxPayUnifiedOrderRequest;
+import com.github.binarywang.wxpay.config.WxPayConfig;
+import com.github.binarywang.wxpay.exception.WxPayException;
+import com.github.binarywang.wxpay.service.WxPayService;
+import lombok.Data;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.rocketmq.spring.core.RocketMQTemplate;
+import org.redisson.api.RLock;
+import org.redisson.api.RedissonClient;
+import org.springframework.aop.framework.AopContext;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.math.BigDecimal;
+import java.text.SimpleDateFormat;
+import java.util.*;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+
+import static com.fs.hisStore.constants.UserAppsLockConstant.LOCK_KEY_PAY;
+
+/**
+ * 直播订单优化版实现
+ */
+@Slf4j
+@Service
+public class LiveOrderOptServiceImpl implements ILiveOrderOptService {
+
+    @Autowired
+    private RedisCache redisCache;
+    @Autowired
+    private RedissonClient redissonClient;
+    @Autowired
+    private ISysConfigService configService;
+    @Autowired
+    private IFsStoreProductScrmService fsStoreProductService;
+    @Autowired
+    private FsStoreProductAttrValueScrmMapper fsStoreProductAttrValueMapper;
+    @Autowired
+    private LiveGoodsMapper liveGoodsMapper;
+    @Autowired
+    private ILiveService liveService;
+    @Autowired
+    private ILiveUserFirstEntryService liveUserFirstEntryService;
+    @Autowired
+    private IFsStoreProductPurchaseLimitScrmService purchaseLimitService;
+    @Autowired
+    private StockDeductService stockDeductService;
+    @Autowired
+    private CloudHostProper cloudHostProper;
+    @Autowired
+    private RocketMQTemplate rocketMQTemplate;
+    @Autowired
+    private FsStoreOrderScrmMapper fsStoreOrderScrmMapper;
+    @Autowired
+    private FsStoreOrderItemScrmMapper fsStoreOrderItemScrmMapper;
+    @Autowired
+    private IFsStoreOrderStatusScrmService orderStatusService;
+    @Autowired
+    private ILiveCouponUserService liveCouponUserService;
+    @Autowired
+    private IFsStoreOrderScrmService fsStoreOrderScrmService;
+    @Autowired
+    private IFsUserScrmService userService;
+    @Autowired
+    private FsStorePaymentScrmMapper fsStorePaymentScrmMapper;
+    @Autowired
+    private FsCoursePlaySourceConfigMapper fsCoursePlaySourceConfigMapper;
+    @Autowired
+    private MerchantAppConfigMapper merchantAppConfigMapper;
+    @Autowired
+    private HuiFuService huiFuService;
+    @Autowired
+    private WxPayService wxPayService;
+    @Autowired
+    private IFsCityScrmService fsCityService;
+    @Autowired
+    private com.fs.hisStore.service.IFsShippingTemplatesScrmService shippingTemplatesService;
+    @Autowired
+    private com.fs.hisStore.service.IFsShippingTemplatesRegionScrmService shippingTemplatesRegionService;
+
+    @Override
+    @Transactional(rollbackFor = Throwable.class)
+    public R createStoreOrder(LiveOrder param) {
+        return doCreateStoreOrder(param, false);
+    }
+
+    @Override
+    @Transactional(rollbackFor = Throwable.class)
+    public R createStoreOrderTest(LiveOrder param) {
+        return doCreateStoreOrder(param, true);
+    }
+
+    private R doCreateStoreOrder(LiveOrder liveOrder, boolean testMode) {
+        long start = System.currentTimeMillis();
+        try {
+            if (!testMode) {
+                String orderKey = redisCache.getCacheObject("orderKey:" + liveOrder.getOrderKey());
+                if (StringUtils.isEmpty(orderKey)) {
+                    return R.error("订单已过期");
+                }
+                String creatingKey = LiveOrderOptConstants.LOCK_ORDER_KEY_CREATING + liveOrder.getOrderKey();
+                if (!redisCache.setIfAbsent(creatingKey, "1", 30, TimeUnit.SECONDS)) {
+                    return R.error("订单正在创建中,请勿重复提交");
+                }
+            }
+
+            R validateResult = validateCreateParam(liveOrder);
+            if (validateResult != null) {
+                return validateResult;
+            }
+
+            Long userId = Long.parseLong(liveOrder.getUserId());
+            Integer purchaseNum = Integer.parseInt(liveOrder.getTotalNum());
+
+            LiveGoods goods = getLiveGoodsCached(liveOrder.getLiveId(), liveOrder.getProductId());
+            if (goods == null) {
+                return R.error("当前商品不存在");
+            }
+
+            if (!testMode) {
+                R stockResult = deductStockIfNeeded(liveOrder, goods);
+                if (stockResult != null) {
+                    return stockResult;
+                }
+            }
+
+            if (liveService.selectLiveByLiveId(liveOrder.getLiveId()) == null) {
+                return R.error("当前直播不存在");
+            }
+
+            FsStoreProductScrm fsStoreProduct = fsStoreProductService.selectFsStoreRedisProductById(liveOrder.getProductId());
+            if (fsStoreProduct == null) {
+                return R.error("商品不存在,购买失败");
+            }
+            if (fsStoreProduct.getIsShow() == 0 || goods.getStatus() == 0) {
+                return R.error("商品已下架,购买失败");
+            }
+            if (!"1".equals(fsStoreProduct.getIsAudit())) {
+                return R.error("商品已下架,购买失败");
+            }
+
+            FsStoreProductAttrValueScrm attrValue = null;
+            if (liveOrder.getAttrValueId() != null) {
+                attrValue = getProductAttrValueCached(liveOrder.getAttrValueId());
+            }
+
+            checkPurchaseLimit(userId, fsStoreProduct, purchaseNum);
+
+            if (!testMode) {
+                publishStockUpdate(goods.getGoodsId(), purchaseNum);
+            }
+
+            FsStoreOrderScrm storeOrder = buildStoreOrder(liveOrder, fsStoreProduct, attrValue, goods, testMode);
+            LiveCouponUser couponToUse = resolveCoupon(liveOrder, userId, storeOrder);
+
+            Integer flag = fsStoreOrderScrmMapper.insertFsStoreOrder(storeOrder);
+            if (flag == null || flag == 0) {
+                return R.error("订单创建失败");
+            }
+
+            if (couponToUse != null) {
+                couponToUse.setStatus(1);
+                couponToUse.setUseTime(new Date());
+                liveCouponUserService.updateLiveCouponUser(couponToUse);
+                redisCache.deleteObject(LiveOrderOptConstants.CACHE_LIVE_COUPON_USER + couponToUse.getId());
+            }
+
+            insertOrderItem(storeOrder, fsStoreProduct, attrValue, liveOrder.getTotalNum());
+            orderStatusService.create(storeOrder.getId(), OrderLogEnum.CREATE_ORDER.getValue(),
+                    OrderLogEnum.CREATE_ORDER.getDesc());
+
+            StoreConfig config = getStoreConfigCached();
+            String redisKey = StoreConstants.REDIS_ORDER_OUTTIME_UNPAY + storeOrder.getId();
+            if (config != null && config.getUnPayTime() != null && config.getUnPayTime() > 0) {
+                redisCache.setCacheObject(redisKey, storeOrder.getId(), config.getUnPayTime(), TimeUnit.MINUTES);
+            } else {
+                redisCache.setCacheObject(redisKey, storeOrder.getId(), 30, TimeUnit.MINUTES);
+            }
+
+            if (!testMode) {
+                redisCache.deleteObject("orderKey:" + liveOrder.getOrderKey());
+                redisCache.deleteObject(LiveOrderOptConstants.LOCK_ORDER_KEY_CREATING + liveOrder.getOrderKey());
+            }
+
+            Calendar calendar = Calendar.getInstance();
+            calendar.setTime(storeOrder.getCreateTime());
+            if (config != null && config.getUnPayTime() != null) {
+                calendar.add(Calendar.MINUTE, config.getUnPayTime());
+            }
+            String payLimitTime = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(calendar.getTime());
+
+            log.info("[createOpt] testMode={} orderId={} cost={}ms", testMode, storeOrder.getId(),
+                    System.currentTimeMillis() - start);
+            return R.ok("下单成功").put("order", storeOrder).put("payLimitTime", payLimitTime);
+        } catch (CustomException e) {
+            throw e;
+        } catch (Exception e) {
+            log.error("[createOpt] 订单创建失败 testMode={}", testMode, e);
+            return R.error("订单创建失败:" + e.getMessage());
+        }
+    }
+
+    @Override
+    public R handleStoreOrderPay(LiveOrderPayParam param) {
+        return doPay(param, false);
+    }
+
+    @Override
+    public R handleStoreOrderPayTest(LiveOrderPayParam param) {
+        return doPay(param, true);
+    }
+
+    private R doPay(LiveOrderPayParam param, boolean testMode) {
+        long start = System.currentTimeMillis();
+        Long orderId = param.getOrderId();
+        RLock lock = redissonClient.getLock(String.format(LOCK_KEY_PAY, orderId));
+        try {
+            boolean locked = lock.tryLock(500, 30000, TimeUnit.MILLISECONDS);
+            if (!locked) {
+                return R.error("订单正在处理中,请勿重复提交");
+            }
+
+            FsStoreOrderScrm order = fsStoreOrderScrmService.selectFsStoreOrderById(orderId);
+            if (order == null) {
+                return R.error("订单不存在");
+            }
+            if (order.getStatus() != null && order.getStatus() != OrderInfoEnum.STATUS_0.getValue()) {
+                return R.error("当前订单已支付");
+            }
+            if (testMode && !isTestOrder(order)) {
+                return R.error("测试支付仅支持测试订单(order_create_type=99)");
+            }
+
+            String payingFlag = redisCache.getCacheObject("isPaying:" + orderId);
+            if (StringUtils.isNotEmpty(payingFlag) && payingFlag.equals(order.getId().toString())) {
+                return R.error("正在支付中...");
+            }
+
+            R preCheck = checkExistingPayments(orderId);
+            if (preCheck != null) {
+                return preCheck;
+            }
+
+            FsUserScrm user = getUserCached(order.getUserId());
+            if (user == null) {
+                return R.error("用户OPENID不存在");
+            }
+
+            applyPayTypeAmount(order, param);
+            fsStoreOrderScrmService.updateFsStoreOrder(order);
+
+            if (testMode) {
+                R testResult = createTestPayment(order, user, param);
+                log.info("[payTest] orderId={} cost={}ms", orderId, System.currentTimeMillis() - start);
+                return testResult;
+            }
+
+            R payResult = invokeThirdPartyPay(order, user, param);
+            log.info("[payOpt] orderId={} cost={}ms", orderId, System.currentTimeMillis() - start);
+            return payResult;
+        } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+            return R.error("支付处理被中断,请稍后重试");
+        } catch (CustomException e) {
+            redisCache.deleteObject("isPaying:" + orderId);
+            throw e;
+        } catch (Exception e) {
+            redisCache.deleteObject("isPaying:" + orderId);
+            log.error("[payOpt] 支付异常 orderId={}", orderId, e);
+            return R.error("支付失败:" + e.getMessage());
+        } finally {
+            if (lock.isHeldByCurrentThread()) {
+                lock.unlock();
+            }
+        }
+    }
+
+    private R validateCreateParam(LiveOrder liveOrder) {
+        if (liveOrder.getLiveId() == null) return R.error("直播ID不能为空");
+        if (liveOrder.getProductId() == null) return R.error("购物商品ID不能为空");
+        if (liveOrder.getUserName() == null) return R.error("用户名不能为空");
+        if (liveOrder.getUserPhone() == null) return R.error("用户手机号不能为空");
+        if (liveOrder.getUserAddress() == null) return R.error("用户地址不能为空");
+        if (liveOrder.getTotalNum() == null) return R.error("商品数量不能为空");
+        if (StringUtils.isEmpty(liveOrder.getTotalNum())) {
+            liveOrder.setTotalNum("1");
+        }
+        return null;
+    }
+
+    private R deductStockIfNeeded(LiveOrder liveOrder, LiveGoods goods) {
+        com.fs.hisStore.config.StoreConfig config = getHisStoreConfigCached();
+        if (config == null || !Boolean.TRUE.equals(config.getCheckStock())) {
+            return null;
+        }
+        try {
+            CompletableFuture<Boolean> future = stockDeductService.deductStockAsync(
+                    liveOrder.getProductId(), liveOrder.getLiveId(),
+                    Integer.parseInt(liveOrder.getTotalNum()), Long.parseLong(liveOrder.getUserId()));
+            if (!future.get()) {
+                return R.error("抱歉,这款商品已被抢光,暂时无库存~");
+            }
+        } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+            return R.error("订单创建失败:" + e.getMessage());
+        } catch (ExecutionException e) {
+            return R.error("订单创建失败:" + e.getMessage());
+        }
+        return null;
+    }
+
+    private void publishStockUpdate(Long goodsId, Integer purchaseNum) {
+        LiveGoodsUploadMqVo vo = LiveGoodsUploadMqVo.builder().goodsId(goodsId).goodsNum(purchaseNum).build();
+        try {
+            if ("北京卓美".equals(cloudHostProper.getCompanyName())) {
+                rocketMQTemplate.syncSend("live-goods-upload", JSON.toJSONString(vo));
+            } else {
+                liveGoodsMapper.updateStock(goodsId, purchaseNum);
+            }
+        } catch (Exception e) {
+            log.error("更新库存失败 vo={}", vo, e);
+            throw new CustomException("库存更新失败");
+        }
+    }
+
+    private FsStoreOrderScrm buildStoreOrder(LiveOrder liveOrder, FsStoreProductScrm fsStoreProduct,
+                                             FsStoreProductAttrValueScrm attrValue, LiveGoods goods, boolean testMode) {
+        FsStoreOrderScrm storeOrder = new FsStoreOrderScrm();
+        storeOrder.setUserId(Long.parseLong(liveOrder.getUserId()));
+        storeOrder.setRealName(liveOrder.getUserName());
+        storeOrder.setUserPhone(liveOrder.getUserPhone());
+        storeOrder.setUserAddress(liveOrder.getUserAddress());
+        storeOrder.setTotalNum(Long.parseLong(liveOrder.getTotalNum()));
+        storeOrder.setStoreId(liveOrder.getStoreId());
+        storeOrder.setMark(liveOrder.getRemark());
+        storeOrder.setRemark(String.valueOf(liveOrder.getLiveId()));
+
+        if (testMode) {
+            storeOrder.setOrderCreateType(LiveOrderOptConstants.ORDER_CREATE_TYPE_LIVE_TEST);
+            storeOrder.setMark(LiveOrderOptConstants.TEST_ORDER_MARK);
+        } else if (liveOrder.getOrderCreateType() != null) {
+            storeOrder.setOrderCreateType(liveOrder.getOrderCreateType());
+        }
+
+        String storeHouseCode = resolveStoreHouseCode(fsStoreProduct.getProductId());
+        storeOrder.setStoreHouseCode(storeHouseCode);
+
+        LiveUserFirstEntry firstEntry = getFirstEntryCached(liveOrder.getLiveId(), Long.parseLong(liveOrder.getUserId()));
+        if (ObjectUtil.isNotEmpty(firstEntry)) {
+            storeOrder.setCompanyId(firstEntry.getCompanyId());
+            storeOrder.setCompanyUserId(firstEntry.getCompanyUserId());
+            storeOrder.setTuiUserId(firstEntry.getCompanyUserId());
+        }
+
+        String orderSn = SnowflakeUtil.nextIdStr();
+        storeOrder.setOrderCode(orderSn);
+
+        BigDecimal totalPrice = fsStoreProduct.getPrice().multiply(new BigDecimal(liveOrder.getTotalNum()));
+        BigDecimal payPrice = totalPrice;
+        if (attrValue != null) {
+            payPrice = attrValue.getPrice().multiply(new BigDecimal(liveOrder.getTotalNum()));
+        }
+
+        BigDecimal deliveryMoney = BigDecimal.ZERO;
+        if (liveOrder.getCityId() != null) {
+            deliveryMoney = handleDeliveryMoneyCached(liveOrder.getCityId(), fsStoreProduct,
+                    liveOrder.getTotalNum(), attrValue);
+            if (deliveryMoney.compareTo(BigDecimal.valueOf(-1)) == 0) {
+                throw new CustomException("偏远地区暂不可购买");
+            }
+            payPrice = payPrice.add(deliveryMoney);
+        }
+
+        BigDecimal discountMoney = BigDecimal.ZERO;
+        if (liveOrder.getCouponUserId() != null) {
+            LiveCouponUser couponUser = getCouponUserCached(liveOrder.getCouponUserId());
+            if (couponUser != null && couponUser.getStatus() == 0
+                    && couponUser.getUserId() != null
+                    && couponUser.getUserId().toString().equals(liveOrder.getUserId())) {
+                if (couponUser.getUseMinPrice().compareTo(payPrice) < 1) {
+                    discountMoney = couponUser.getCouponPrice();
+                    storeOrder.setCouponId(couponUser.getId());
+                    storeOrder.setCouponPrice(couponUser.getCouponPrice());
+                }
+            }
+        }
+
+        storeOrder.setTotalPrice(totalPrice);
+        storeOrder.setTotalPostage(deliveryMoney);
+        storeOrder.setPayPostage(deliveryMoney);
+        storeOrder.setPayDelivery(deliveryMoney);
+        storeOrder.setCouponPrice(discountMoney);
+        storeOrder.setDeductionPrice(BigDecimal.ZERO);
+        storeOrder.setPaid(0);
+        storeOrder.setPayType(StringUtils.isEmpty(liveOrder.getPayType()) ? "1" : liveOrder.getPayType());
+        storeOrder.setUseIntegral(BigDecimal.ZERO);
+        storeOrder.setBackIntegral(BigDecimal.ZERO);
+        storeOrder.setGainIntegral(BigDecimal.ZERO);
+        storeOrder.setCost(BigDecimal.ZERO);
+        storeOrder.setIsChannel(1);
+        storeOrder.setShippingType(1);
+        storeOrder.setCreateTime(new Date());
+        storeOrder.setIsPrescribe(0);
+        storeOrder.setOrderType(2);
+        storeOrder.setLiveId(liveOrder.getLiveId());
+        storeOrder.setStatus(0);
+
+        StoreConfig config = getStoreConfigCached();
+        if (config != null && config.getServiceFee() != null) {
+            storeOrder.setServiceFee(config.getServiceFee());
+        }
+
+        BigDecimal finalPay = payPrice.subtract(discountMoney);
+        storeOrder.setPayPrice(finalPay);
+        storeOrder.setPayMoney(finalPay);
+        return storeOrder;
+    }
+
+    private LiveCouponUser resolveCoupon(LiveOrder liveOrder, Long userId, FsStoreOrderScrm storeOrder) {
+        if (liveOrder.getCouponUserId() == null) {
+            return null;
+        }
+        LiveCouponUser couponUser = getCouponUserCached(liveOrder.getCouponUserId());
+        if (couponUser == null || couponUser.getStatus() != 0) {
+            return null;
+        }
+        if (!couponUser.getUserId().equals(userId)) {
+            throw new CustomException("非法操作");
+        }
+        if (storeOrder.getCouponId() == null) {
+            return null;
+        }
+        return couponUser;
+    }
+
+    private void insertOrderItem(FsStoreOrderScrm storeOrder, FsStoreProductScrm fsStoreProduct,
+                                 FsStoreProductAttrValueScrm attrValue, String totalNum) {
+        FsStoreCartDTO cartDTO = new FsStoreCartDTO();
+        cartDTO.setProductId(fsStoreProduct.getProductId());
+        cartDTO.setPrice(attrValue != null ? attrValue.getPrice() : fsStoreProduct.getPrice());
+        cartDTO.setSku(attrValue != null && attrValue.getSku() != null ? attrValue.getSku() : "");
+        cartDTO.setProductName(fsStoreProduct.getProductName());
+        cartDTO.setNum(Integer.parseInt(totalNum));
+        cartDTO.setImage(fsStoreProduct.getImage());
+        if (attrValue != null) {
+            cartDTO.setBarCode(attrValue.getBarCode());
+            cartDTO.setGroupBarCode(attrValue.getGroupBarCode());
+        }
+
+        FsStoreOrderItemScrm orderItem = new FsStoreOrderItemScrm();
+        orderItem.setOrderId(storeOrder.getId());
+        orderItem.setOrderCode(storeOrder.getOrderCode());
+        orderItem.setProductId(fsStoreProduct.getProductId());
+        orderItem.setProductAttrValueId(attrValue != null ? attrValue.getId() : null);
+        orderItem.setJsonInfo(JSONUtil.toJsonStr(cartDTO));
+        orderItem.setNum(Integer.parseInt(totalNum));
+        orderItem.setIsAfterSales(0);
+        orderItem.setIsPrescribe(0);
+
+        List<FsStoreOrderItemScrm> items = Collections.singletonList(orderItem);
+        storeOrder.setItemJson(JSONUtil.toJsonStr(items));
+        fsStoreOrderItemScrmMapper.insertFsStoreOrderItem(orderItem);
+        fsStoreOrderScrmMapper.updateFsStoreOrder(storeOrder);
+    }
+
+    private R checkExistingPayments(Long orderId) {
+        List<FsStorePaymentScrm> paymentList = fsStorePaymentScrmMapper.selectFsStorePaymentByOrderId(orderId);
+        if (paymentList == null || paymentList.isEmpty()) {
+            return null;
+        }
+        for (FsStorePaymentScrm payment : paymentList) {
+            if (payment.getStatus() != null && payment.getStatus() == 1) {
+                return R.error("当前订单已支付");
+            }
+        }
+        return null;
+    }
+
+    private void applyPayTypeAmount(FsStoreOrderScrm order, LiveOrderPayParam param) {
+        if (order.getIsEditMoney() != null && order.getIsEditMoney() == 1) {
+            return;
+        }
+        com.fs.store.config.StoreConfig storeConfig = getStoreConfigForPayCached();
+        if (param.getPayType().equals(1)) {
+            order.setPayType("1");
+            order.setPayMoney(order.getPayPrice());
+            if (!"广州郑多燕".equals(cloudHostProper.getCompanyName())) {
+                order.setPayDelivery(BigDecimal.ZERO);
+            } else {
+                order.setPayType(order.getPayPrice().compareTo(order.getTotalPrice()) == 0 ? "1" : "5");
+            }
+        } else if (param.getPayType().equals(2)) {
+            order.setPayType("2");
+            BigDecimal payMoney = order.getPayPrice().multiply(new BigDecimal(storeConfig.getPayRate()))
+                    .divide(new BigDecimal(100), 2, BigDecimal.ROUND_HALF_UP);
+            if (payMoney.compareTo(new BigDecimal("0.01")) < 0) {
+                throw new CustomException("物流代收计算支付金额为0,不允许选择物流代收");
+            }
+            order.setPayDelivery(order.getPayPrice().subtract(payMoney));
+            order.setPayMoney(payMoney);
+        } else if (param.getPayType().equals(3)) {
+            order.setPayType("3");
+            BigDecimal amount = redisCache.getCacheObject("orderAmount:" + order.getId());
+            BigDecimal payMoney = amount != null ? amount : BigDecimal.ZERO;
+            BigDecimal payPostage = order.getPayPostage();
+            if (payPostage == null || payPostage.compareTo(BigDecimal.ZERO) <= 0) {
+                payPostage = storeConfig.getPayPostage() != null ? storeConfig.getPayPostage() : BigDecimal.ZERO;
+                order.setPayPrice(order.getPayPrice().add(payPostage));
+            }
+            order.setPayPostage(payPostage);
+            payMoney = payMoney.add(payPostage);
+            order.setPayMoney(payMoney);
+            order.setPayDelivery(order.getPayPrice().subtract(payMoney));
+        }
+    }
+
+    private R createTestPayment(FsStoreOrderScrm order, FsUserScrm user, LiveOrderPayParam param) {
+        String payCode = SnowflakeUtil.nextIdStr();
+        FsStorePaymentScrm storePayment = buildBasePayment(order, user, param, payCode);
+        storePayment.setRemark(LiveOrderOptConstants.TEST_PAYMENT_REMARK);
+        storePayment.setTradeNo("TEST-" + payCode);
+        fsStorePaymentScrmMapper.insertFsStorePayment(storePayment);
+        redisCache.setCacheObject("isPaying:" + order.getId(), order.getId().toString(), 1, TimeUnit.MINUTES);
+        Map<String, Object> mockPay = new HashMap<>();
+        mockPay.put("testMode", true);
+        mockPay.put("paymentId", storePayment.getPaymentId());
+        mockPay.put("payCode", payCode);
+        return R.ok().put("payType", param.getPayType()).put("result", mockPay).put("isTest", true);
+    }
+
+    private R invokeThirdPartyPay(FsStoreOrderScrm order, FsUserScrm user, LiveOrderPayParam param) {
+        if (StringUtils.isBlank(param.getAppId())) {
+            return R.error("appId不能为空");
+        }
+        if (!isOnlinePayType(order.getPayType()) && order.getPayMoney().compareTo(BigDecimal.ZERO) <= 0) {
+            if ("3".equals(order.getPayType())) {
+                IFsStoreOrderScrmService proxy = (IFsStoreOrderScrmService) AopContext.currentProxy();
+                proxy.payConfirm(2, order.getId(), null, null, null, null);
+                return R.ok().put("payType", param.getPayType());
+            }
+            return R.error("支付金额异常");
+        }
+        if (order.getPayMoney().compareTo(BigDecimal.ZERO) <= 0) {
+            return R.error("支付金额异常");
+        }
+
+        MiniAppPayBundle bundle = getMiniAppPayBundleCached(param.getAppId());
+        String payCode = SnowflakeUtil.nextIdStr();
+        FsStorePaymentScrm storePayment = buildBasePayment(order, user, param, payCode);
+        storePayment.setRemark("直播订单支付");
+        storePayment.setPayMode(bundle.getMerchantAppConfig().getMerchantType());
+        storePayment.setMerConfigId(bundle.getMerchantAppConfig().getId());
+        storePayment.setAppId(bundle.getPlayConfig().getAppid() == null ? "" : bundle.getPlayConfig().getAppid());
+        fsStorePaymentScrmMapper.insertFsStorePayment(storePayment);
+
+        MerchantAppConfig merchantAppConfig = bundle.getMerchantAppConfig();
+        if ("hf".equals(merchantAppConfig.getMerchantType())) {
+            return invokeHuiFuPay(order, user, param, storePayment, bundle.getPayConfig());
+        }
+        if ("wx".equals(merchantAppConfig.getMerchantType())) {
+            return invokeWxPay(order, user, param, storePayment, bundle);
+        }
+        return R.error("不支持的支付方式");
+    }
+
+    private R invokeHuiFuPay(FsStoreOrderScrm order, FsUserScrm user, LiveOrderPayParam param,
+                             FsStorePaymentScrm storePayment, FsPayConfig fsPayConfig) {
+        HuiFuCreateOrder o = new HuiFuCreateOrder();
+        o.setTradeType("T_MINIAPP");
+        o.setOpenid(user.getMaOpenId());
+        o.setReqSeqId("live-" + storePayment.getPayCode());
+        o.setTransAmt(storePayment.getPayMoney().toString());
+        o.setGoodsDesc("直播订单支付");
+        o.setAppId(param.getAppId());
+        try {
+            HuiFuUtils.doDiv(o, order.getCompanyId(), storePayment.getMerConfigId());
+            HuiFuUtils.saveDivItem(o, order.getOrderCode(), storePayment.getPayCode());
+        } catch (Exception e) {
+            log.error("分账出错 orderId={}", order.getId(), e);
+        }
+        HuifuCreateOrderResult result = huiFuService.createOrder(o);
+        if (result.getResp_code() != null
+                && ("00000000".equals(result.getResp_code()) || "00000100".equals(result.getResp_code()))) {
+            FsStorePaymentScrm mt = new FsStorePaymentScrm();
+            mt.setPaymentId(storePayment.getPaymentId());
+            mt.setTradeNo(result.getHf_seq_id());
+            mt.setAppId(param.getAppId());
+            mt.setBusinessCode(order.getOrderCode());
+            fsStorePaymentScrmMapper.updateFsStorePayment(mt);
+            redisCache.setCacheObject("isPaying:" + order.getId(), order.getId().toString(), 1, TimeUnit.MINUTES);
+            Map<String, Object> resultMap = JSON.parseObject(result.getPay_info(), new TypeReference<Map<String, Object>>() {});
+            String pkg = (String) resultMap.get("package");
+            resultMap.put("packageValue", pkg);
+            return R.ok().put("payType", param.getPayType()).put("result", resultMap);
+        }
+        fsStorePaymentScrmMapper.deleteFsStorePaymentById(storePayment.getPaymentId());
+        return R.error(result.getResp_desc());
+    }
+
+    private R invokeWxPay(FsStoreOrderScrm order, FsUserScrm user, LiveOrderPayParam param,
+                          FsStorePaymentScrm storePayment, MiniAppPayBundle bundle) {
+        FsPayConfig fsPayConfig = bundle.getPayConfig();
+        WxPayConfig payConfig = new WxPayConfig();
+        payConfig.setAppId(bundle.getPlayConfig().getAppid());
+        payConfig.setMchId(fsPayConfig.getWxMchId());
+        payConfig.setMchKey(fsPayConfig.getWxMchKey());
+        payConfig.setKeyPath(fsPayConfig.getKeyPath());
+        payConfig.setNotifyUrl(fsPayConfig.getNotifyUrlScrm());
+        wxPayService.setConfig(payConfig);
+        WxPayUnifiedOrderRequest orderRequest = new WxPayUnifiedOrderRequest();
+        orderRequest.setOpenid(user.getMaOpenId());
+        orderRequest.setBody("直播订单支付");
+        orderRequest.setOutTradeNo("live-" + storePayment.getPayCode());
+        orderRequest.setTotalFee(WxPayUnifiedOrderRequest.yuanToFen(storePayment.getPayMoney().toString()));
+        orderRequest.setTradeType("JSAPI");
+        orderRequest.setSpbillCreateIp(IpUtils.getIpAddr(ServletUtils.getRequest()));
+        try {
+            WxPayMpOrderResult orderResult = wxPayService.createOrder(orderRequest);
+            redisCache.setCacheObject("isPaying:" + order.getId(), order.getId().toString(), 1, TimeUnit.MINUTES);
+            return R.ok().put("result", orderResult).put("type", "wx").put("isPay", 0).put("payType", param.getPayType());
+        } catch (WxPayException e) {
+            fsStorePaymentScrmMapper.deleteFsStorePaymentById(storePayment.getPaymentId());
+            throw new CustomException("支付失败" + e.getMessage());
+        }
+    }
+
+    private FsStorePaymentScrm buildBasePayment(FsStoreOrderScrm order, FsUserScrm user,
+                                                LiveOrderPayParam param, String payCode) {
+        FsStorePaymentScrm storePayment = new FsStorePaymentScrm();
+        storePayment.setCompanyId(order.getCompanyId());
+        storePayment.setCompanyUserId(order.getCompanyUserId());
+        storePayment.setStatus(0);
+        storePayment.setPayCode(payCode);
+        storePayment.setPayMoney(order.getPayMoney());
+        storePayment.setCreateTime(new Date());
+        storePayment.setPayTypeCode("weixin");
+        storePayment.setBusinessType(2);
+        storePayment.setOpenId(user.getMaOpenId());
+        storePayment.setUserId(user.getUserId());
+        storePayment.setBusinessOrderId(order.getId().toString());
+        storePayment.setOrderId(order.getId());
+        storePayment.setBusinessCode(order.getOrderCode());
+        return storePayment;
+    }
+
+    private boolean isOnlinePayType(String payType) {
+        return "1".equals(payType) || "2".equals(payType) || "3".equals(payType) || "5".equals(payType);
+    }
+
+    private boolean isTestOrder(FsStoreOrderScrm order) {
+        return order.getOrderCreateType() != null
+                && order.getOrderCreateType() == LiveOrderOptConstants.ORDER_CREATE_TYPE_LIVE_TEST;
+    }
+
+    private String resolveStoreHouseCode(Long productId) {
+        if (productId != null && (productId.equals(3168L) || productId.equals(3184L) || productId.equals(3185L))) {
+            return "YDSP001";
+        }
+        return "CQDS001";
+    }
+
+    private void checkPurchaseLimit(Long userId, FsStoreProductScrm product, Integer num) {
+        if (product.getSinglePurchaseLimit() != null && product.getSinglePurchaseLimit() > 0
+                && num > product.getSinglePurchaseLimit()) {
+            throw new CustomException("该商品单次最多购买" + product.getSinglePurchaseLimit() + "件");
+        }
+        if (product.getPurchaseLimit() == null || product.getPurchaseLimit() <= 0) {
+            return;
+        }
+        FsStoreProductPurchaseLimitScrm purchaseLimit = purchaseLimitService.selectByProductIdAndUserId(
+                product.getProductId(), userId);
+        int purchasedNum = purchaseLimit != null ? purchaseLimit.getNum() : 0;
+        if (purchasedNum + num > product.getPurchaseLimit()) {
+            throw new CustomException("该商品限购" + product.getPurchaseLimit() + "件,您已购买"
+                    + (purchasedNum + num) + "件,无法继续购买");
+        }
+    }
+
+    // ---------- 缓存 ----------
+
+    private LiveGoods getLiveGoodsCached(Long liveId, Long productId) {
+        String key = LiveOrderOptConstants.CACHE_LIVE_GOODS + liveId + ":" + productId;
+        LiveGoods cached = redisCache.getCacheObject(key);
+        if (cached != null) {
+            return cached;
+        }
+        LiveGoods goods = liveGoodsMapper.selectLiveGoodsByProductId(liveId, productId);
+        if (goods != null) {
+            redisCache.setCacheObject(key, goods, 60, TimeUnit.SECONDS);
+        }
+        return goods;
+    }
+
+    private LiveUserFirstEntry getFirstEntryCached(Long liveId, Long userId) {
+        String key = LiveOrderOptConstants.CACHE_LIVE_FIRST_ENTRY + liveId + ":" + userId;
+        LiveUserFirstEntry cached = redisCache.getCacheObject(key);
+        if (cached != null) {
+            return cached;
+        }
+        LiveUserFirstEntry entry = liveUserFirstEntryService.selectEntityByLiveIdUserId(liveId, userId);
+        if (entry != null) {
+            redisCache.setCacheObject(key, entry, 10, TimeUnit.MINUTES);
+        }
+        return entry;
+    }
+
+    private com.fs.hisStore.config.StoreConfig getHisStoreConfigCached() {
+        String key = LiveOrderOptConstants.CACHE_SYS_CONFIG + "his.store";
+        com.fs.hisStore.config.StoreConfig cached = redisCache.getCacheObject(key);
+        if (cached != null) {
+            return cached;
+        }
+        String json = configService.selectConfigByKey("his.store");
+        if (StringUtils.isEmpty(json)) {
+            return null;
+        }
+        com.fs.hisStore.config.StoreConfig config = JSON.parseObject(json, com.fs.hisStore.config.StoreConfig.class);
+        if (config != null) {
+            redisCache.setCacheObject(key, config, 10, TimeUnit.MINUTES);
+        }
+        return config;
+    }
+
+    private StoreConfig getStoreConfigCached() {
+        String key = LiveOrderOptConstants.CACHE_SYS_CONFIG + "store.config";
+        StoreConfig cached = redisCache.getCacheObject(key);
+        if (cached != null) {
+            return cached;
+        }
+        String json = configService.selectConfigByKey("store.config");
+        if (StringUtils.isEmpty(json)) {
+            return null;
+        }
+        StoreConfig config = JSONUtil.toBean(json, StoreConfig.class);
+        if (config != null) {
+            redisCache.setCacheObject(key, config, 10, TimeUnit.MINUTES);
+        }
+        return config;
+    }
+
+    private com.fs.store.config.StoreConfig getStoreConfigForPayCached() {
+        String key = LiveOrderOptConstants.CACHE_SYS_CONFIG + "his.store.pay";
+        com.fs.store.config.StoreConfig cached = redisCache.getCacheObject(key);
+        if (cached != null) {
+            return cached;
+        }
+        String config = configService.selectConfigByKey("his.store");
+        com.fs.store.config.StoreConfig storeConfig = JSONUtil.toBean(config, com.fs.store.config.StoreConfig.class);
+        if (storeConfig != null) {
+            redisCache.setCacheObject(key, storeConfig, 10, TimeUnit.MINUTES);
+        }
+        return storeConfig;
+    }
+
+    private FsUserScrm getUserCached(Long userId) {
+        String key = LiveOrderOptConstants.CACHE_SYS_CONFIG + "user:pay:" + userId;
+        FsUserScrm cached = redisCache.getCacheObject(key);
+        if (cached != null) {
+            return cached;
+        }
+        FsUserScrm user = userService.selectFsUserById(userId);
+        if (user != null) {
+            redisCache.setCacheObject(key, user, 10, TimeUnit.MINUTES);
+        }
+        return user;
+    }
+
+    private MiniAppPayBundle getMiniAppPayBundleCached(String appId) {
+        String key = LiveOrderOptConstants.CACHE_MINIAPP_PAY + appId;
+        MiniAppPayBundle cached = redisCache.getCacheObject(key);
+        if (cached != null) {
+            return cached;
+        }
+        FsCoursePlaySourceConfig playConfig = fsCoursePlaySourceConfigMapper.selectCoursePlaySourceConfigByAppId(appId);
+        if (playConfig == null) {
+            throw new CustomException("未找到appId对应的小程序配置: " + appId);
+        }
+        Long merchantConfigId = playConfig.getMerchantConfigId();
+        if (merchantConfigId == null || merchantConfigId <= 0) {
+            throw new CustomException("小程序没有配置商户信息");
+        }
+        MerchantAppConfig merchantAppConfig = merchantAppConfigMapper.selectMerchantAppConfigById(merchantConfigId);
+        FsPayConfig payConfig = JSON.parseObject(merchantAppConfig.getDataJson(), FsPayConfig.class);
+        MiniAppPayBundle bundle = new MiniAppPayBundle();
+        bundle.setPlayConfig(playConfig);
+        bundle.setMerchantAppConfig(merchantAppConfig);
+        bundle.setPayConfig(payConfig);
+        redisCache.setCacheObject(key, bundle, 30, TimeUnit.MINUTES);
+        return bundle;
+    }
+
+    private FsStoreProductAttrValueScrm getProductAttrValueCached(Long attrValueId) {
+        String key = LiveOrderOptConstants.CACHE_PRODUCT_ATTR_ID + attrValueId;
+        FsStoreProductAttrValueScrm cached = redisCache.getCacheObject(key);
+        if (cached != null) {
+            return cached;
+        }
+        FsStoreProductAttrValueScrm attrValue = fsStoreProductAttrValueMapper.selectFsStoreProductAttrValueById(attrValueId);
+        if (attrValue != null) {
+            redisCache.setCacheObject(key, attrValue, 10, TimeUnit.MINUTES);
+        }
+        return attrValue;
+    }
+
+    private LiveCouponUser getCouponUserCached(Long couponUserId) {
+        String key = LiveOrderOptConstants.CACHE_LIVE_COUPON_USER + couponUserId;
+        LiveCouponUser cached = redisCache.getCacheObject(key);
+        if (cached != null) {
+            return cached;
+        }
+        LiveCouponUser couponUser = liveCouponUserService.selectLiveCouponUserById(couponUserId);
+        if (couponUser != null) {
+            redisCache.setCacheObject(key, couponUser, 5, TimeUnit.MINUTES);
+        }
+        return couponUser;
+    }
+
+    private BigDecimal handleDeliveryMoneyCached(Long cityId, FsStoreProductScrm fsStoreProduct,
+                                                 String totalNumSize, FsStoreProductAttrValueScrm knownAttrValue) {
+        BigDecimal storePostage = BigDecimal.ZERO;
+        BigDecimal badCode = BigDecimal.valueOf(-1);
+        if (ObjectUtil.isNull(fsStoreProduct.getTempId())) {
+            return storePostage;
+        }
+        List<String> citys = resolveShippingMatchCityIdsCached(cityId);
+        String ids = String.valueOf(fsStoreProduct.getTempId());
+        List<FsShippingTemplatesScrm> shippingTemplatesList = getShippingTemplatesCached(ids);
+        String cityIds = String.join(",", citys);
+        List<FsShippingTemplatesRegionScrm> shippingTemplatesRegionList = getShippingRegionsCached(ids, cityIds);
+        if (shippingTemplatesList != null && !shippingTemplatesList.isEmpty()
+                && (shippingTemplatesRegionList == null || shippingTemplatesRegionList.isEmpty())) {
+            return badCode;
+        }
+        Map<Long, Integer> shippingTemplatesMap = shippingTemplatesList.stream()
+                .collect(Collectors.toMap(FsShippingTemplatesScrm::getId, FsShippingTemplatesScrm::getType));
+        Map<Long, FsShippingTemplatesRegionScrm> shippingTemplatesRegionMap = shippingTemplatesRegionList.stream()
+                .collect(Collectors.toMap(FsShippingTemplatesRegionScrm::getTempId, r -> r, (a, b) -> b));
+        Long tempId = Long.valueOf(fsStoreProduct.getTempId());
+        Integer templateType = shippingTemplatesMap.get(tempId);
+        FsStoreProductAttrValueScrm productAttrValue = knownAttrValue != null
+                ? knownAttrValue : getFirstProductAttrValueCached(fsStoreProduct.getProductId());
+        if (productAttrValue == null) {
+            return storePostage;
+        }
+        Integer totalNum = Integer.valueOf(totalNumSize);
+        double num;
+        if (ShippingTempEnum.TYPE_1.getValue().equals(templateType)) {
+            num = totalNum.doubleValue();
+        } else if (ShippingTempEnum.TYPE_2.getValue().equals(templateType)) {
+            num = NumberUtil.mul(totalNum, productAttrValue.getWeight()).doubleValue();
+        } else if (ShippingTempEnum.TYPE_3.getValue().equals(templateType)) {
+            num = NumberUtil.mul(totalNum, productAttrValue.getVolume()).doubleValue();
+        } else {
+            num = totalNum.doubleValue();
+        }
+        FsShippingTemplatesRegionScrm region = shippingTemplatesRegionMap.get(tempId);
+        if (region == null) {
+            return badCode;
+        }
+        BigDecimal price = NumberUtil.round(NumberUtil.mul(totalNum, fsStoreProduct.getPrice()), 2);
+        TemplateDTO templateDTO = TemplateDTO.builder()
+                .number(num).price(price)
+                .first(region.getFirst().doubleValue()).firstPrice(region.getFirstPrice())
+                .continues(region.getContinues().doubleValue()).continuePrice(region.getContinuePrice())
+                .tempId(tempId).cityId(cityId.toString()).build();
+        if (Double.compare(templateDTO.getNumber(), templateDTO.getFirst()) <= 0) {
+            return NumberUtil.round(NumberUtil.add(storePostage, templateDTO.getFirstPrice()), 2);
+        }
+        BigDecimal firstPrice = NumberUtil.add(storePostage, templateDTO.getFirstPrice());
+        if (templateDTO.getContinues() <= 0) {
+            return firstPrice;
+        }
+        double average = Math.ceil(NumberUtil.div(NumberUtil.sub(templateDTO.getNumber(), templateDTO.getFirst()),
+                templateDTO.getContinues().doubleValue()));
+        return NumberUtil.add(firstPrice, NumberUtil.mul(average, templateDTO.getContinuePrice()));
+    }
+
+    private List<String> resolveShippingMatchCityIdsCached(Long cityId) {
+        String cacheKey = LiveOrderOptConstants.CACHE_CITY_SHIPPING_CHAIN + cityId;
+        List<String> cached = redisCache.getCacheObject(cacheKey);
+        if (cached != null) {
+            return cached;
+        }
+        List<String> cityIds = fsCityService.resolveShippingMatchCityIds(String.valueOf(cityId));
+        redisCache.setCacheObject(cacheKey, cityIds, 24, TimeUnit.HOURS);
+        return cityIds;
+    }
+
+    private List<FsShippingTemplatesScrm> getShippingTemplatesCached(String ids) {
+        String cacheKey = LiveOrderOptConstants.CACHE_SHIPPING_TEMPLATE + ids;
+        List<FsShippingTemplatesScrm> cached = redisCache.getCacheObject(cacheKey);
+        if (cached != null) {
+            return cached;
+        }
+        List<FsShippingTemplatesScrm> templates = shippingTemplatesService.selectFsShippingTemplatesByIds(ids);
+        if (templates != null) {
+            redisCache.setCacheObject(cacheKey, templates, 1, TimeUnit.HOURS);
+        }
+        return templates;
+    }
+
+    private List<FsShippingTemplatesRegionScrm> getShippingRegionsCached(String tempIds, String cityIds) {
+        String cacheKey = LiveOrderOptConstants.CACHE_SHIPPING_REGION + tempIds + ":" + cityIds;
+        try {
+            List<FsShippingTemplatesRegionScrm> cached = redisCache.getCacheObject(cacheKey);
+            if (cached != null) {
+                return cached;
+            }
+        } catch (Exception e) {
+            redisCache.deleteObject(cacheKey);
+        }
+        List<FsShippingTemplatesRegionScrm> regions = shippingTemplatesRegionService
+                .selectFsShippingTemplatesRegionListByTempIdsAndCityIds(tempIds, cityIds);
+        if (regions != null) {
+            redisCache.setCacheObject(cacheKey, regions, 1, TimeUnit.HOURS);
+        }
+        return regions;
+    }
+
+    private FsStoreProductAttrValueScrm getFirstProductAttrValueCached(Long productId) {
+        String cacheKey = LiveOrderOptConstants.CACHE_PRODUCT_ATTR_LIST + productId;
+        List<FsStoreProductAttrValueScrm> cached = redisCache.getCacheObject(cacheKey);
+        if (cached == null) {
+            cached = fsStoreProductAttrValueMapper.selectFsStoreProductAttrValueByProductId(productId);
+            if (cached != null) {
+                redisCache.setCacheObject(cacheKey, cached, 10, TimeUnit.MINUTES);
+            }
+        }
+        if (cached == null || cached.isEmpty()) {
+            return null;
+        }
+        return cached.get(0);
+    }
+
+    @Data
+    private static class MiniAppPayBundle implements java.io.Serializable {
+        private FsCoursePlaySourceConfig playConfig;
+        private MerchantAppConfig merchantAppConfig;
+        private FsPayConfig payConfig;
+    }
+}

+ 255 - 0
fs-service/src/main/java/com/fs/live/service/impl/LiveOrderServiceImpl.java

@@ -2392,6 +2392,261 @@ public class LiveOrderServiceImpl implements ILiveOrderService {
                 .build();
     }
 
+    private static final String CACHE_CITY_SHIPPING_CHAIN = "fs:city:shipping:chain:";
+    private static final String CACHE_PRODUCT_ATTR_ID = "fs:product:attr:id:";
+    private static final String CACHE_PRODUCT_ATTR_LIST = "fs:product:attr:list:";
+    private static final String CACHE_SHIPPING_TEMPLATE = "fs:shipping:template:ids:";
+    private static final String CACHE_SHIPPING_REGION = "fs:shipping:region:";
+    private static final String CACHE_LIVE_COUPON_USER = "live:coupon:user:";
+    private static final String CACHE_LIVE_COMPUTED = "live:computed:";
+
+    @Override
+    public LiveOrderComputeDTO computedOrderCache(long userId, LiveOrderComputedParam param) {
+        String resultCacheKey = buildLiveComputedCacheKey(userId, param);
+        LiveOrderComputeDTO cachedResult = redisCache.getCacheObject(resultCacheKey);
+        if (cachedResult != null) {
+            return cachedResult;
+        }
+
+        String orderKey = redisCache.getCacheObject("orderKey:" + param.getOrderKey());
+        if (StringUtils.isEmpty(orderKey)) {
+            log.error("订单已失效");
+            return null;
+        }
+
+        FsStoreProductScrm fsStoreProduct = fsStoreProductService.selectFsStoreRedisProductById(param.getProductId());
+        if (fsStoreProduct == null) {
+            log.error("商品不存在");
+            return null;
+        }
+        if (StringUtils.isEmpty(param.getTotalNum())) {
+            log.error("商品数量不能为空");
+            return null;
+        }
+        int purchaseNum = Integer.parseInt(param.getTotalNum());
+        if (fsStoreProduct.getSinglePurchaseLimit() != null && fsStoreProduct.getSinglePurchaseLimit() > 0
+                && purchaseNum > fsStoreProduct.getSinglePurchaseLimit()) {
+            throw new CustomException("该商品单次最多购买" + fsStoreProduct.getSinglePurchaseLimit() + "件");
+        }
+
+        FsStoreProductAttrValueScrm fsStoreProductAttrValue = null;
+        if (param.getAttrValueId() != null) {
+            fsStoreProductAttrValue = getProductAttrValueCached(param.getAttrValueId());
+        }
+
+        BigDecimal payPrice = fsStoreProduct.getPrice().multiply(new BigDecimal(param.getTotalNum()));
+        if (fsStoreProductAttrValue != null) {
+            payPrice = fsStoreProductAttrValue.getPrice().multiply(new BigDecimal(param.getTotalNum()));
+        }
+        BigDecimal totalPrice = payPrice;
+        BigDecimal payDelivery = BigDecimal.ZERO;
+        BigDecimal deductionPrice = BigDecimal.ZERO;
+        BigDecimal badCode = BigDecimal.valueOf(-1);
+
+        if (param.getCityId() != null) {
+            payDelivery = handleDeliveryMoneyCached(param.getCityId(), fsStoreProduct, param.getTotalNum(), fsStoreProductAttrValue);
+            if (payDelivery.compareTo(badCode) == 0) {
+                throw new ServiceException("偏远地区暂不可购买");
+            }
+            payPrice = payPrice.add(payDelivery);
+        }
+
+        if (param.getCouponUserId() != null) {
+            LiveCouponUser couponUser = getCouponUserCached(param.getCouponUserId());
+            if (couponUser != null && couponUser.getStatus() == 0
+                    && couponUser.getUserId() != null && couponUser.getUserId().longValue() == userId) {
+                if (couponUser.getUseMinPrice().compareTo(payPrice) < 1) {
+                    payPrice = payPrice.subtract(couponUser.getCouponPrice());
+                    deductionPrice = couponUser.getCouponPrice();
+                }
+            }
+        }
+
+        LiveOrderComputeDTO result = LiveOrderComputeDTO.builder()
+                .payPrice(payPrice)
+                .payDelivery(payDelivery)
+                .deductionPrice(deductionPrice)
+                .totalPrice(totalPrice)
+                .build();
+        redisCache.setCacheObject(resultCacheKey, result, 10, TimeUnit.SECONDS);
+        return result;
+    }
+
+    private String buildLiveComputedCacheKey(long userId, LiveOrderComputedParam param) {
+        return CACHE_LIVE_COMPUTED + userId + ":"
+                + StringUtils.defaultString(param.getOrderKey()) + ":"
+                + param.getProductId() + ":"
+                + StringUtils.defaultString(param.getTotalNum()) + ":"
+                + Objects.toString(param.getCityId(), "") + ":"
+                + Objects.toString(param.getAttrValueId(), "") + ":"
+                + Objects.toString(param.getCouponUserId(), "");
+    }
+
+    private List<String> resolveShippingMatchCityIdsCached(Long cityId) {
+        String cacheKey = CACHE_CITY_SHIPPING_CHAIN + cityId;
+        List<String> cached = redisCache.getCacheObject(cacheKey);
+        if (cached != null) {
+            return cached;
+        }
+        List<String> cityIds = fsCityService.resolveShippingMatchCityIds(String.valueOf(cityId));
+        redisCache.setCacheObject(cacheKey, cityIds, 24, TimeUnit.HOURS);
+        return cityIds;
+    }
+
+    private FsStoreProductAttrValueScrm getProductAttrValueCached(Long attrValueId) {
+        String cacheKey = CACHE_PRODUCT_ATTR_ID + attrValueId;
+        FsStoreProductAttrValueScrm cached = redisCache.getCacheObject(cacheKey);
+        if (cached != null) {
+            return cached;
+        }
+        FsStoreProductAttrValueScrm attrValue = attrValueScrmMapper.selectFsStoreProductAttrValueById(attrValueId);
+        if (attrValue != null) {
+            redisCache.setCacheObject(cacheKey, attrValue, 10, TimeUnit.MINUTES);
+        }
+        return attrValue;
+    }
+
+    private FsStoreProductAttrValueScrm getFirstProductAttrValueCached(Long productId) {
+        List<FsStoreProductAttrValueScrm> attrValues = getProductAttrValuesCached(productId);
+        if (attrValues == null || attrValues.isEmpty()) {
+            return null;
+        }
+        return attrValues.get(0);
+    }
+
+    private List<FsStoreProductAttrValueScrm> getProductAttrValuesCached(Long productId) {
+        String cacheKey = CACHE_PRODUCT_ATTR_LIST + productId;
+        List<FsStoreProductAttrValueScrm> cached = redisCache.getCacheObject(cacheKey);
+        if (cached != null) {
+            return cached;
+        }
+        List<FsStoreProductAttrValueScrm> attrValues = fsStoreProductAttrValueMapper.selectFsStoreProductAttrValueByProductId(productId);
+        if (attrValues != null) {
+            redisCache.setCacheObject(cacheKey, attrValues, 10, TimeUnit.MINUTES);
+        }
+        return attrValues;
+    }
+
+    private List<FsShippingTemplatesScrm> getShippingTemplatesCached(String ids) {
+        String cacheKey = CACHE_SHIPPING_TEMPLATE + ids;
+        List<FsShippingTemplatesScrm> cached = redisCache.getCacheObject(cacheKey);
+        if (cached != null) {
+            return cached;
+        }
+        List<FsShippingTemplatesScrm> templates = shippingTemplatesService.selectFsShippingTemplatesByIds(ids);
+        if (templates != null) {
+            redisCache.setCacheObject(cacheKey, templates, 1, TimeUnit.HOURS);
+        }
+        return templates;
+    }
+
+    private List<FsShippingTemplatesRegionScrm> getShippingRegionsCached(String tempIds, String cityIds) {
+        String cacheKey = CACHE_SHIPPING_REGION + tempIds + ":" + cityIds;
+        try {
+            List<FsShippingTemplatesRegionScrm> cached = redisCache.getCacheObject(cacheKey);
+            if (cached != null) {
+                return cached;
+            }
+        } catch (Exception e) {
+            log.warn("运费区域缓存反序列化失败,清除后重查: key={}, error={}", cacheKey, e.getMessage());
+            redisCache.deleteObject(cacheKey);
+        }
+        List<FsShippingTemplatesRegionScrm> regions = shippingTemplatesRegionService
+                .selectFsShippingTemplatesRegionListByTempIdsAndCityIds(tempIds, cityIds);
+        if (regions != null) {
+            redisCache.setCacheObject(cacheKey, regions, 1, TimeUnit.HOURS);
+        }
+        return regions;
+    }
+
+    private LiveCouponUser getCouponUserCached(Long couponUserId) {
+        String cacheKey = CACHE_LIVE_COUPON_USER + couponUserId;
+        LiveCouponUser cached = redisCache.getCacheObject(cacheKey);
+        if (cached != null) {
+            return cached;
+        }
+        LiveCouponUser couponUser = liveCouponUserService.selectLiveCouponUserById(couponUserId);
+        if (couponUser != null) {
+            redisCache.setCacheObject(cacheKey, couponUser, 5, TimeUnit.MINUTES);
+        }
+        return couponUser;
+    }
+
+    private BigDecimal handleDeliveryMoneyCached(Long cityId, FsStoreProductScrm fsStoreProduct, String totalNumSize,
+                                                 FsStoreProductAttrValueScrm knownAttrValue) {
+        BigDecimal storePostage = BigDecimal.ZERO;
+        BigDecimal badCode = BigDecimal.valueOf(-1);
+        if (ObjectUtil.isNull(fsStoreProduct.getTempId())) {
+            return storePostage;
+        }
+        List<String> citys = resolveShippingMatchCityIdsCached(cityId);
+        String ids = String.valueOf(fsStoreProduct.getTempId());
+        if (StringUtils.isBlank(ids)) {
+            return storePostage;
+        }
+        List<FsShippingTemplatesScrm> shippingTemplatesList = getShippingTemplatesCached(ids);
+        String cityIds = String.join(",", citys);
+        List<FsShippingTemplatesRegionScrm> shippingTemplatesRegionList = getShippingRegionsCached(ids, cityIds);
+        if (shippingTemplatesList != null && !shippingTemplatesList.isEmpty()
+                && (shippingTemplatesRegionList == null || shippingTemplatesRegionList.isEmpty())) {
+            return badCode;
+        }
+        Map<Long, Integer> shippingTemplatesMap = shippingTemplatesList.stream()
+                .collect(Collectors.toMap(FsShippingTemplatesScrm::getId, FsShippingTemplatesScrm::getType));
+        Map<Long, FsShippingTemplatesRegionScrm> shippingTemplatesRegionMap = shippingTemplatesRegionList.stream()
+                .collect(Collectors.toMap(FsShippingTemplatesRegionScrm::getTempId,
+                        region -> region,
+                        (key1, key2) -> key2));
+        Long tempId = Long.valueOf(fsStoreProduct.getTempId());
+        Integer templateType = shippingTemplatesMap.get(tempId);
+        FsStoreProductAttrValueScrm productAttrValue = knownAttrValue != null
+                ? knownAttrValue
+                : getFirstProductAttrValueCached(fsStoreProduct.getProductId());
+        if (productAttrValue == null) {
+            return storePostage;
+        }
+        Integer totalNum = Integer.valueOf(totalNumSize);
+        double num;
+        if (ShippingTempEnum.TYPE_1.getValue().equals(templateType)) {
+            num = totalNum.doubleValue();
+        } else if (ShippingTempEnum.TYPE_2.getValue().equals(templateType)) {
+            num = NumberUtil.mul(totalNum, productAttrValue.getWeight()).doubleValue();
+        } else if (ShippingTempEnum.TYPE_3.getValue().equals(templateType)) {
+            num = NumberUtil.mul(totalNum, productAttrValue.getVolume()).doubleValue();
+        } else {
+            num = totalNum.doubleValue();
+        }
+        FsShippingTemplatesRegionScrm shippingTemplatesRegion = shippingTemplatesRegionMap.get(tempId);
+        if (shippingTemplatesRegion == null) {
+            log.error("没有找到运费模板");
+            return badCode;
+        }
+        BigDecimal price = NumberUtil.round(NumberUtil.mul(totalNum, fsStoreProduct.getPrice()), 2);
+        TemplateDTO templateDTO = TemplateDTO.builder()
+                .number(num)
+                .price(price)
+                .first(shippingTemplatesRegion.getFirst().doubleValue())
+                .firstPrice(shippingTemplatesRegion.getFirstPrice())
+                .continues(shippingTemplatesRegion.getContinues().doubleValue())
+                .continuePrice(shippingTemplatesRegion.getContinuePrice())
+                .tempId(tempId)
+                .cityId(cityId.toString())
+                .build();
+        if (Double.compare(templateDTO.getNumber(), templateDTO.getFirst()) <= 0) {
+            storePostage = NumberUtil.round(NumberUtil.add(storePostage, templateDTO.getFirstPrice()), 2);
+        } else {
+            BigDecimal firstPrice = NumberUtil.add(storePostage, templateDTO.getFirstPrice());
+            if (templateDTO.getContinues() <= 0) {
+                storePostage = firstPrice;
+            } else {
+                double average = Math.ceil(NumberUtil.div(NumberUtil.sub(templateDTO.getNumber(), templateDTO.getFirst()),
+                        templateDTO.getContinues().doubleValue()));
+                storePostage = NumberUtil.add(firstPrice, NumberUtil.mul(average, templateDTO.getContinuePrice()));
+            }
+        }
+        return storePostage;
+    }
+
     @Override
     public LiveOrder selectOrderIdByOrderCode(String platformCode) {
         return baseMapper.selectFsUserVipOrderByOrderCode(platformCode);

+ 9 - 0
fs-user-app/src/main/java/com/fs/app/controller/live/LiveOrderController.java

@@ -280,6 +280,15 @@ public class LiveOrderController extends AppBaseController
         return R.ok().put("data",dto);
     }
 
+    @Login
+    @ApiOperation("计算订单金额(缓存优化)")
+    @PostMapping("/computeCache")
+    @RateLimiter(time = 10, count = 10000, msg = "当前下单的人比较多,请稍后再试")
+    public R computeCache(@Validated @RequestBody LiveOrderComputedParam param, HttpServletRequest request) {
+        LiveOrderComputeDTO dto = orderService.computedOrderCache(Long.parseLong(getUserId()), param);
+        return R.ok().put("data", dto);
+    }
+
     @Login
     @ApiOperation("计算中奖订单")
     @PostMapping("/computedReward")

+ 94 - 0
fs-user-app/src/main/java/com/fs/app/controller/live/LiveOrderOptController.java

@@ -0,0 +1,94 @@
+package com.fs.app.controller.live;
+
+import com.fs.app.annotation.Login;
+import com.fs.app.controller.AppBaseController;
+import com.fs.common.annotation.RateLimiter;
+import com.fs.common.core.domain.R;
+import com.fs.common.utils.StringUtils;
+import com.fs.live.domain.LiveOrder;
+import com.fs.live.param.LiveOrderPayParam;
+import com.fs.live.service.ILiveOrderOptService;
+import io.swagger.annotations.ApiOperation;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestHeader;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+/**
+ * 直播订单优化版 Controller(不影响原 LiveOrderController 接口)
+ */
+@Slf4j
+@RestController
+@RequestMapping("/app/live/liveOrderOpt")
+public class LiveOrderOptController extends AppBaseController {
+
+    @Autowired
+    private ILiveOrderOptService liveOrderOptService;
+
+    @Login
+    @ApiOperation("创建订单(优化版)")
+    @PostMapping("/create")
+    @RateLimiter(time = 10, count = 10000, msg = "当前下单的人比较多,请稍后再试")
+    public R create(@Validated @RequestBody LiveOrder param,
+                    @RequestHeader(value = "AppId", required = false) String appId) {
+        if (StringUtils.isNotEmpty(appId)) {
+            param.setAppId(appId);
+        }
+        if (StringUtils.isEmpty(param.getAppId())) {
+            return R.error("appId不能为空");
+        }
+        param.setUserId(getUserId());
+        log.info("[createOpt] userId={}", param.getUserId());
+        return liveOrderOptService.createStoreOrder(param);
+    }
+
+    @Login
+    @ApiOperation("创建测试订单(写入商城订单表,order_create_type=99)")
+    @PostMapping("/createTest")
+    public R createTest(@Validated @RequestBody LiveOrder param,
+                        @RequestHeader(value = "AppId", required = false) String appId) {
+        if (StringUtils.isNotEmpty(appId)) {
+            param.setAppId(appId);
+        }
+        if (StringUtils.isEmpty(param.getAppId())) {
+            return R.error("appId不能为空");
+        }
+        param.setUserId(getUserId());
+        log.info("[createTestOpt] userId={}", param.getUserId());
+        return liveOrderOptService.createStoreOrderTest(param);
+    }
+
+    @Login
+    @ApiOperation("支付(优化版)")
+    @PostMapping("/pay")
+    public R pay(@Validated @RequestBody LiveOrderPayParam param,
+                 @RequestHeader(value = "AppId", required = false) String appId) {
+        if (StringUtils.isNotEmpty(appId)) {
+            param.setAppId(appId);
+        }
+        if (StringUtils.isEmpty(param.getAppId())) {
+            return R.error("appId不能为空");
+        }
+        log.info("[payOpt] orderId={} payType={}", param.getOrderId(), param.getPayType());
+        return liveOrderOptService.handleStoreOrderPay(param);
+    }
+
+    @Login
+    @ApiOperation("测试支付(payment.remark=LIVE_TEST_PAYMENT,不调第三方支付)")
+    @PostMapping("/payTest")
+    public R payTest(@Validated @RequestBody LiveOrderPayParam param,
+                     @RequestHeader(value = "AppId", required = false) String appId) {
+        if (StringUtils.isNotEmpty(appId)) {
+            param.setAppId(appId);
+        }
+        if (StringUtils.isEmpty(param.getAppId())) {
+            return R.error("appId不能为空");
+        }
+        log.info("[payTestOpt] orderId={} payType={}", param.getOrderId(), param.getPayType());
+        return liveOrderOptService.handleStoreOrderPayTest(param);
+    }
+}