package com.jeesharp.business.modules.mall.application;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import com.jeesharp.business.modules.mall.application.support.MallCheckoutPricingSupport;
import com.jeesharp.business.modules.mall.application.support.MallCouponCheckoutContextSupport;
import com.jeesharp.business.modules.mall.application.support.MallFirstOrderPromoSupport;
import com.jeesharp.business.modules.mall.application.support.MallOrderPayAssembler;
import com.jeesharp.business.modules.mall.application.support.MallPayBatchDisplaySupport;
import com.jeesharp.business.modules.common.constants.PlatformTenantConstants;
import com.jeesharp.business.modules.mall.constants.MallOrderOperateTypeConstants;
import com.jeesharp.business.modules.mall.dto.MallCheckoutPricingRequest;
import com.jeesharp.business.modules.mall.dto.MallConfirmPayDTO;
import com.jeesharp.business.modules.mall.dto.MallOrderCancelDTO;
import com.jeesharp.business.modules.mall.dto.MallFreightPreviewDTO;
import com.jeesharp.business.modules.mall.dto.MallOrderPickupVerifyDTO;
import com.jeesharp.business.modules.mall.dto.MallOrderShipDTO;
import com.jeesharp.business.modules.mall.dto.MallOrderSubmitDTO;
import com.jeesharp.business.modules.mall.dto.MallOrderSubmitLineDTO;
import com.jeesharp.business.modules.mall.dto.MallPreparePayDTO;
import com.jeesharp.business.modules.mall.entity.MallMemberAddress;
import com.jeesharp.business.modules.mall.entity.MallOrder;
import com.jeesharp.business.modules.mall.entity.MallOrderItem;
import com.jeesharp.business.modules.mall.entity.MallLogisticsCompany;
import com.jeesharp.business.modules.mall.entity.MallOrderLog;
import com.jeesharp.business.modules.mall.entity.MallPayBatch;
import com.jeesharp.business.modules.mall.entity.MallPickupPoint;
import com.jeesharp.business.modules.mall.enums.MallDeliveryTypeEnum;
import com.jeesharp.business.modules.mall.enums.MallPayTypeEnum;
import com.jeesharp.business.modules.mall.enums.MallOrderStatusEnum;
import com.jeesharp.business.modules.mall.enums.MallPayBatchStatusEnum;
import com.jeesharp.business.modules.mall.enums.MallPickupPointStatusEnum;
import com.jeesharp.business.modules.mall.service.MallLogisticsCompanyService;
import com.jeesharp.business.modules.mall.service.MallMemberAddressService;
import com.jeesharp.business.modules.mall.service.MallOrderItemService;
import com.jeesharp.business.modules.mall.service.MallOrderLogService;
import com.jeesharp.business.modules.mall.service.MallOrderService;
import com.jeesharp.business.modules.mall.service.MallPayBatchService;
import com.jeesharp.business.modules.mall.service.MallPickupPointService;
import com.jeesharp.business.modules.mall.service.MallSettingService;
import com.jeesharp.business.modules.mall.vo.MallCheckoutPricingResultVO;
import com.jeesharp.business.modules.mall.vo.MallFreightPreviewVO;
import com.jeesharp.business.modules.mall.convert.MallOrderConvert;
import com.jeesharp.business.modules.mall.vo.MallOrderExportVO;
import com.jeesharp.business.modules.mall.vo.MallOrderListVO;
import com.jeesharp.business.modules.mall.vo.MallOrderQueryVO;
import com.jeesharp.business.modules.mall.vo.MallOrderReceivableEntryVO;
import com.jeesharp.framework.pojo.query.PageRespVO;
import com.jeesharp.business.modules.mall.vo.MallOrderSubmitResultVO;
import com.jeesharp.business.modules.mall.vo.MallPayPrepareVO;
import com.jeesharp.business.modules.mall.vo.MallPaySubItemVO;
import com.jeesharp.business.modules.mall.vo.MallPayStatusVO;
import com.jeesharp.business.modules.trade.api.TradeOrderFacadeApi;
import com.jeesharp.business.modules.trade.dto.TradeOrderCreateDTO;
import com.jeesharp.business.modules.trade.dto.TradeOrderLineCreateDTO;
import com.jeesharp.business.modules.trade.entity.TradeOrder;
import com.jeesharp.business.modules.trade.entity.TradeOrderItem;
import com.jeesharp.business.modules.trade.enums.TradeBizTypeEnum;
import com.jeesharp.business.modules.trade.enums.TradeOrderStatusEnum;
import com.jeesharp.business.modules.finance.api.MerchantReceivableApi;
import com.jeesharp.business.modules.finance.api.MerchantSettleConfigApi;
import com.jeesharp.business.modules.finance.api.ProfitSharingApi;
import com.jeesharp.business.modules.finance.api.dto.MerchantReceivableBriefDTO;
import com.jeesharp.business.modules.finance.merchantsettle.vo.MerchantSettleConfigSnapshotVO;
import com.jeesharp.business.modules.finance.merchantsettle.vo.PaySettleResolveVO;
import com.jeesharp.business.modules.coupon.api.CouponCheckoutApi;
import com.jeesharp.business.modules.coupon.dto.CouponCheckoutContextDTO;
import com.jeesharp.business.modules.coupon.dto.CouponFreezeCommandDTO;
import com.jeesharp.business.modules.member.api.MemberPointsOrderApi;
import com.jeesharp.business.modules.finance.paymentrecord.enums.PayRecordBizSceneEnum;
import com.jeesharp.business.modules.member.service.MemberService;
import com.jeesharp.business.modules.trade.vo.TradeOrderInfoVO;
import com.jeesharp.business.modules.trade.vo.TradeOrderQueryVO;
import com.jeesharp.framework.exception.BizErrorException;
import com.jeesharp.framework.pojo.Page;
import com.jeesharp.framework.utils.WebFrameworkUtils;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.env.Environment;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.ZoneId;
import java.util.Date;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;

/**
 * 商城订单应用服务（C 端下单 / 支付 / 取消的核心编排层）。
 *
 * <p><b>主流程速览</b></p>
 * <pre>
 * 1. 提交订单 submitMobileOrder
 *    → 算商品金额 + 运费 → 首单优惠 → 优惠券试算 → 落 mall_order（待支付）+ 扣库存
 *    → 若有券：couponCheckoutApi.freezeForOrder（券状态：未使用 → 冻结）
 *    → 可选双写 biz_trade_order（统一订单中心）
 *    → 0 元单直接 confirmZeroAmountPay + confirmOrderPaid
 *
 * 2. 预支付 preparePay（Application 编排 + MallOrderPayAssembler） + 网关 WechatJsapiPayOrderPrepayService（微信统一下单）
 *
 * 3. 支付成功 confirmPaySuccess
 *    → mall_order 置已支付 → couponCheckoutApi.confirmOrderPaid（冻结 → 已使用）
 *    → 首单赠券 → 同步统一单 markPaid
 *
 * 4. 履约 shipOrder / confirmReceive / verifyPickup → 同步统一单 markFulfilling / markFulfilled
 *
 * 5. 取消 cancelMobileOrder / 超时 cancelPendingOrderBySystem
 *    → 回滚库存 → couponCheckoutApi.releaseFrozen（冻结 → 未使用）
 * </pre>
 *
 * <p>双写 {@code biz_trade_order} 由配置 {@code jeesharp.trade.dual-write.mall-enabled} 控制。</p>
 *
 * @author jeesharp
 * @since 2026-05-08
 */
@Slf4j
@Service
@AllArgsConstructor
@Transactional(readOnly = true)
public class MallOrderApplicationService {

    private static final String PROP_DUAL_WRITE = "jeesharp.trade.dual-write.mall-enabled";
    private static final String PROP_READ_MALL_UNIFIED = "jeesharp.trade.read.mall-from-unified";
    private static final String ORDER_TIMEOUT_CONFIG_KEY = "order_timeout_minutes";
    private static final int DEFAULT_ORDER_TIMEOUT_MINUTES = 30;
    private static final int EXPIRED_CANCEL_BATCH_LIMIT = 200;

    private static final String PROFILE_PRO = "pro";

    private static final String LOGIN_REQUIRED_MESSAGE = "请先登录";

    private final MallOrderService mallOrderService;
    private final MallPayBatchService mallPayBatchService;
    private final MallOrderItemService mallOrderItemService;
    private final MallMemberAddressService mallMemberAddressService;
    private final MallPickupPointService mallPickupPointService;
    private final TradeOrderFacadeApi tradeOrderFacadeApi;
    private final MemberService memberService;
    private final MallOrderLogService mallOrderLogService;
    private final MallLogisticsCompanyService mallLogisticsCompanyService;
    private final MallFirstOrderPromoSupport mallFirstOrderPromoSupport;
    private final MallCheckoutPricingSupport mallCheckoutPricingSupport;
    private final MallCouponCheckoutContextSupport mallCouponCheckoutContextSupport;
    private final MallPayBatchDisplaySupport mallPayBatchDisplaySupport;
    private final MallOrderPayAssembler mallOrderPayAssembler;
    private final CouponCheckoutApi couponCheckoutApi;
    private final MerchantSettleConfigApi merchantSettleConfigApi;
    private final ProfitSharingApi profitSharingApi;
    private final MerchantReceivableApi merchantReceivableApi;
    private final MallSettingService mallSettingService;
    private final Environment environment;
    private final MemberPointsOrderApi memberPointsOrderApi;

    public MallOrder get(String id) {
        return mallOrderService.get(id);
    }

    /**
     * 解析合并支付批次编号（小程序列表展示用）。
     */
    public String resolvePayBatchNo(String payBatchId) {
        return mallPayBatchDisplaySupport.resolvePayBatchNo(payBatchId);
    }

    /**
     * 补全合并支付批次展示字段（批次号、同批子单数）。
     */
    public void fillPayBatchDisplay(MallOrder order) {
        mallPayBatchDisplaySupport.enrichForDetail(order);
    }

    /**
     * 管理端订单详情：主表 + 明细行。
     *
     * @param id 订单主键
     * @return 订单；不存在时为 {@code null}
     */
    public MallOrder getAdminDetail(String id) {
        MallOrder order = mallOrderService.get(id);
        if (order == null) {
            return null;
        }
        order.setItems(listOrderItems(id));
        enrichPickupPointDisplay(order);
        enrichPickupVerifyOperatorDisplay(order);
        mallPayBatchDisplaySupport.enrichForDetail(order);
        enrichReceivableEntries(order);
        return order;
    }

    /**
     * 管理端详情：附平台代收内部应收流水（无记录时为空列表）。
     */
    private void enrichReceivableEntries(MallOrder order) {
        if (order == null || StrUtil.isBlank(order.getId())) {
            return;
        }
        List<MerchantReceivableBriefDTO> briefList = merchantReceivableApi.listByMallOrderId(order.getId());
        if (CollUtil.isEmpty(briefList)) {
            order.setReceivableEntries(Collections.emptyList());
            return;
        }
        List<MallOrderReceivableEntryVO> entries = new ArrayList<>(briefList.size());
        for (MerchantReceivableBriefDTO brief : briefList) {
            if (brief == null) {
                continue;
            }
            MallOrderReceivableEntryVO vo = new MallOrderReceivableEntryVO();
            vo.setEntryType(brief.getEntryType());
            vo.setEntryTypeName(brief.getEntryTypeName());
            vo.setSettleMode(brief.getSettleMode());
            vo.setBaseAmount(brief.getBaseAmount());
            vo.setCommissionRate(brief.getCommissionRate());
            vo.setCommissionAmount(brief.getCommissionAmount());
            vo.setMerchantAmount(brief.getMerchantAmount());
            vo.setBizTime(brief.getBizTime());
            entries.add(vo);
        }
        order.setReceivableEntries(entries);
    }

    /**
     * 管理端详情：补全自提点名称、地址等展示字段。
     */
    private void enrichPickupPointDisplay(MallOrder order) {
        if (order == null || StrUtil.isBlank(order.getPickupPointId())) {
            return;
        }
        MallPickupPoint point = mallPickupPointService.get(order.getPickupPointId());
        if (point == null) {
            return;
        }
        order.setPickupPointName(point.getPointName());
        order.setPickupPointContactMobile(point.getContactMobile());
        order.setPickupPointBusinessHours(point.getBusinessHours());
        String address = StrUtil.join(" ",
                StrUtil.blankToDefault(point.getProvince(), ""),
                StrUtil.blankToDefault(point.getCity(), ""),
                StrUtil.blankToDefault(point.getDistrict(), ""),
                StrUtil.blankToDefault(point.getAddress(), "")).trim();
        order.setPickupPointAddress(StrUtil.isBlank(address) ? null : address);
    }

    /**
     * 已核销但历史单未落库核销人时，从订单日志中补全操作人展示。
     */
    private void enrichPickupVerifyOperatorDisplay(MallOrder order) {
        if (order == null || order.getPickupVerifyTime() == null) {
            return;
        }
        if (StrUtil.isNotBlank(order.getPickupVerifyByName())) {
            return;
        }
        List<MallOrderLog> logs = mallOrderLogService.getOrderLogs(order.getId());
        if (CollUtil.isEmpty(logs)) {
            return;
        }
        for (MallOrderLog log : logs) {
            if (log == null || StrUtil.isBlank(log.getOperatorName())) {
                continue;
            }
            String remark = StrUtil.blankToDefault(log.getRemark(), "");
            if (remark.contains("核销") || MallOrderStatusEnum.FULFILLED.getCode().equals(log.getAfterStatus())) {
                order.setPickupVerifyBy(log.getOperatorId());
                order.setPickupVerifyByName(log.getOperatorName());
                return;
            }
        }
    }

    /**
     * 小程序订单详情：按订单主键查询明细行（未删除）。
     *
     * @param orderId 商城订单 id
     * @return 明细列表
     */
    public List<MallOrderItem> listOrderItems(String orderId) {
        if (StrUtil.isBlank(orderId)) {
            return Collections.emptyList();
        }
        return mallOrderItemService.findByOrderIds(Collections.singletonList(orderId));
    }

    /**
     * 小程序订单列表：按多个订单 id 批量查明细并按 orderId 分组，避免逐单查询。
     *
     * @param orderIds 订单 id 列表
     * @return 订单 id → 明细列表
     */
    public Map<String, List<MallOrderItem>> mapOrderItemsByOrderIds(List<String> orderIds) {
        if (orderIds == null || orderIds.isEmpty()) {
            return Collections.emptyMap();
        }
        List<MallOrderItem> all = mallOrderItemService.findByOrderIds(orderIds);
        return all.stream()
            .filter(it -> StrUtil.isNotBlank(it.getOrderId()))
            .collect(Collectors.groupingBy(MallOrderItem::getOrderId, LinkedHashMap::new, Collectors.toList()));
    }

    /**
     * 当 {@code mall_order_item} 查不到行但已有关联统一单时，用 {@code biz_trade_order_item} 拼出列表展示用明细（不落库）。
     *
     * @param unifiedOrderIds 统一单主键列表
     * @return 统一单 id → 展示用明细（仅内存对象）
     */
    public Map<String, List<MallOrderItem>> mapFallbackMallItemsByUnifiedOrderIds(List<String> unifiedOrderIds) {
        if (unifiedOrderIds == null || unifiedOrderIds.isEmpty()) {
            return Collections.emptyMap();
        }
        List<TradeOrderItem> lines = tradeOrderFacadeApi.findItemsByTradeOrderIds(unifiedOrderIds);
        Map<String, List<MallOrderItem>> byUnified = new LinkedHashMap<>();
        for (TradeOrderItem line : lines) {
            String uid = line.getOrderId();
            if (StrUtil.isBlank(uid)) {
                continue;
            }
            MallOrderItem m = new MallOrderItem();
            m.setOrderId(uid);
            m.setSpuId(StrUtil.blankToDefault(line.getResourceRef(), ""));
            m.setSkuId(StrUtil.blankToDefault(line.getSkuRef(), ""));
            m.setSkuName(StrUtil.blankToDefault(line.getItemTitle(), "商品"));
            m.setSpecInfo("");
            m.setPrice(line.getUnitPrice());
            int qty = line.getQuantity() == null || line.getQuantity() < 1 ? 1 : line.getQuantity();
            m.setQuantity(qty);
            m.setTotalAmount(line.getLineAmount());
            m.setImage("");
            byUnified.computeIfAbsent(uid, k -> new ArrayList<>()).add(m);
        }
        return byUnified;
    }

    public Page<MallOrder> findPage(Page<MallOrder> page, MallOrderQueryVO query) {
        MallOrderQueryVO prepared = prepareOrderQuery(query);
        if (prepared == null) {
            Page<MallOrder> empty = new Page<>();
            empty.setRecords(Collections.emptyList());
            empty.setTotal(0L);
            empty.setPageNo(page.getPageNo());
            empty.setPageSize(page.getPageSize());
            return empty;
        }
        return mallOrderService.findPage(page, prepared);
    }

    /**
     * 管理端订单分页列表：转 ListVO 并批量补全会员编号、昵称、手机。
     */
    public PageRespVO<MallOrderListVO> findPageForAdmin(Page<MallOrder> page, MallOrderQueryVO query) {
        MallOrderQueryVO prepared = prepareOrderQuery(query);
        if (prepared == null) {
            return emptyAdminOrderPage(page);
        }
        Page<MallOrder> result = mallOrderService.findPage(page, prepared);
        PageRespVO<MallOrderListVO> pageVo = MallOrderConvert.INSTANCE.toPageRespVO(result);
        memberService.<MallOrderListVO>enrichMemberDisplay(pageVo.getRecords());
        mallPayBatchDisplaySupport.enrichForList(pageVo.getRecords());
        return pageVo;
    }

    private PageRespVO<MallOrderListVO> emptyAdminOrderPage(Page<MallOrder> page) {
        PageRespVO<MallOrderListVO> empty = new PageRespVO<>();
        empty.setRecords(Collections.emptyList());
        empty.setTotal(0L);
        empty.setPageNo(page.getPageNo());
        empty.setPageSize(page.getPageSize());
        empty.setPages(0L);
        return empty;
    }

    /**
     * 管理端导出：按当前筛选条件导出 Excel（含会员编号、昵称、手机）。
     *
     * @param query 与分页列表相同的查询条件（忽略分页条数）
     * @return 导出行
     */
    public List<MallOrderExportVO> findExportList(MallOrderQueryVO query) {
        MallOrderQueryVO prepared = prepareOrderQuery(query);
        if (prepared == null) {
            return Collections.emptyList();
        }
        List<MallOrderExportVO> list = mallOrderService.findExportList(prepared);
        for (MallOrderExportVO row : list) {
            row.setPayType(MallPayTypeEnum.getDescByCode(row.getPayType()));
        }
        return list;
    }

    /**
     * 会员关键词 → memberIdIn 筛选条件（编排，不含会员数据组装）。
     */
    private MallOrderQueryVO prepareOrderQuery(MallOrderQueryVO query) {
        MallOrderQueryVO q = query != null ? query : new MallOrderQueryVO();
        if (StrUtil.isNotBlank(q.getMemberKeyword())) {
            List<String> memberIds = memberService.findIdsByKeyword(q.getMemberKeyword().trim());
            q.setMemberKeyword(null);
            if (CollUtil.isEmpty(memberIds)) {
                return null;
            }
            q.setMemberIdIn(memberIds);
        }
        return q;
    }

    /**
     * C 端「我的商城订单」按统一单主序分页（需开启读切换）。
     */
    public Page<MallOrder> findPageForMobileFromTrade(String memberId, MallOrderQueryVO query) {
        if (query == null) {
            query = new MallOrderQueryVO();
        }
        TradeOrderQueryVO tq = new TradeOrderQueryVO();
        tq.setMemberId(memberId);
        tq.setBizType(TradeBizTypeEnum.MALL.getValue());
        tq.setPageNo(query.getPageNo());
        tq.setPageSize(query.getPageSize());
        Page<TradeOrder> page = new Page<>(tq);
        Page<TradeOrder> trPage = tradeOrderFacadeApi.findPage(page, tq);
        Page<MallOrder> out = new Page<>();
        out.setTotal(trPage.getTotal());
        out.setPageNo(trPage.getPageNo());
        out.setPageSize(trPage.getPageSize());
        List<MallOrder> rows = new ArrayList<>();
        for (TradeOrder tr : trPage.getRecords()) {
            if (StrUtil.isBlank(tr.getVerticalOrderId())) {
                continue;
            }
            MallOrder mo = mallOrderService.get(tr.getVerticalOrderId());
            if (mo != null) {
                mo.setUnifiedOrderId(tr.getId());
                rows.add(mo);
            }
        }
        out.setRecords(rows);
        return out;
    }

    public boolean readMallFromUnified() {
        return Boolean.parseBoolean(environment.getProperty(PROP_READ_MALL_UNIFIED, "false"));
    }

    /**
     * C 端预支付：合并支付批次优先（attach.orderId = batchId）；无批次时走子单预支付。
     */
    @Transactional(readOnly = false)
    public MallPayPrepareVO preparePay(MallPreparePayDTO dto, String memberId) {
        if (StrUtil.isBlank(memberId)) {
            throw new BizErrorException(LOGIN_REQUIRED_MESSAGE);
        }
        String batchId = resolvePrepareBatchId(dto, memberId);
        if (StrUtil.isNotBlank(batchId)) {
            return preparePayForBatch(batchId, dto, memberId);
        }
        if (dto == null || StrUtil.isBlank(dto.getOrderId())) {
            throw new BizErrorException("订单ID不能为空");
        }
        MallOrder order = mallOrderService.get(dto.getOrderId().trim());
        mallOrderService.assertPayPreparable(order, memberId);
        String payChannel = StrUtil.blankToDefault(dto.getPayChannel(), MallPayTypeEnum.WECHAT_JSAPI.getCode());
        mallOrderService.markPayPreparing(order, payChannel);
        String payOrderId = mallOrderPayAssembler.newPayOrderId();
        MallPayPrepareVO vo = mallOrderPayAssembler.buildSingleOrderPrepareVo(order, payChannel, payOrderId);
        applyPaySettle(vo, Collections.singletonList(order), vo.getAmountFen() == null ? 0L : vo.getAmountFen());
        return vo;
    }

    /**
     * 支付状态：id 可为 batchId 或子单 orderId（含 pay_batch_id 时返回批次维度状态）。
     */
    public MallPayStatusVO getPayStatus(String orderId, String memberId) {
        if (StrUtil.isBlank(memberId) || StrUtil.isBlank(orderId)) {
            return null;
        }
        String id = orderId.trim();
        MallPayBatch batch = mallPayBatchService.get(id);
        if (batch != null && memberId.equals(batch.getMemberId())) {
            return mallOrderPayAssembler.buildBatchPayStatusVo(batch);
        }
        MallOrder entity = mallOrderService.get(id);
        if (entity == null || !memberId.equals(entity.getMemberId())) {
            return null;
        }
        if (StrUtil.isNotBlank(entity.getPayBatchId())) {
            MallPayBatch payBatch = mallPayBatchService.get(entity.getPayBatchId());
            if (payBatch != null) {
                return mallOrderPayAssembler.buildBatchPayStatusVo(payBatch);
            }
        }
        return mallOrderPayAssembler.buildPayStatusVo(entity);
    }

    /**
     * 小程序取消待支付商城订单；含 pay_batch_id 时整批取消并释放全部冻结券。
     */
    @Transactional(readOnly = false)
    public void cancelMobileOrder(MallOrderCancelDTO dto, String memberId) {
        if (StrUtil.isBlank(memberId)) {
            throw new BizErrorException(LOGIN_REQUIRED_MESSAGE);
        }
        if (dto == null) {
            throw new BizErrorException("订单不存在");
        }
        String batchId = resolveCancelBatchId(dto);
        if (StrUtil.isNotBlank(batchId)) {
            cancelPayBatchByMember(batchId, memberId, StrUtil.blankToDefault(dto.getCancelReason(), "用户取消订单"));
            return;
        }
        if (StrUtil.isBlank(dto.getOrderId())) {
            throw new BizErrorException("订单不存在");
        }
        MallOrder before = mallOrderService.get(dto.getOrderId().trim());
        if (before == null) {
            throw new BizErrorException("订单不存在");
        }
        if (!memberId.equals(before.getMemberId())) {
            throw new BizErrorException("无权操作该订单");
        }
        Integer beforeStatus = before.getStatus();
        MallOrder order = mallOrderService.cancelPendingMobileOrder(dto.getOrderId().trim(), memberId);
        releaseMallOrderCoupons(order, "商城订单已取消");
        if (isMallDualWriteEnabled() && StrUtil.isNotBlank(order.getUnifiedOrderId())) {
            tradeOrderFacadeApi.cancelPending(order.getUnifiedOrderId());
        }
        mallOrderLogService.recordOrderStatusChange(
                order.getId(),
                order.getOrderNo(),
                beforeStatus,
                MallOrderStatusEnum.CANCELED.getCode(),
                MallOrderOperateTypeConstants.CANCEL,
                memberId,
                "会员",
                StrUtil.blankToDefault(dto.getCancelReason(), "用户取消订单"));
    }

    /**
     * 系统取消待支付订单（支付超时等），含批次时整批取消。
     */
    @Transactional(readOnly = false)
    public void cancelPendingOrderBySystem(String orderId, String reason) {
        if (StrUtil.isBlank(orderId)) {
            return;
        }
        MallOrder before = mallOrderService.get(orderId.trim());
        if (before == null) {
            return;
        }
        if (!MallOrderStatusEnum.PENDING_PAY.getCode().equals(before.getStatus())) {
            return;
        }
        if (StrUtil.isNotBlank(before.getPayBatchId())) {
            cancelPayBatchBySystem(before.getPayBatchId(), reason);
            return;
        }
        Integer beforeStatus = before.getStatus();
        MallOrder order = mallOrderService.cancelPendingOrderBySystem(orderId.trim());
        releaseMallOrderCoupons(order, reason);
        if (isMallDualWriteEnabled() && StrUtil.isNotBlank(order.getUnifiedOrderId())) {
            tradeOrderFacadeApi.cancelPending(order.getUnifiedOrderId());
        }
        mallOrderLogService.recordOrderStatusChange(
                order.getId(),
                order.getOrderNo(),
                beforeStatus,
                MallOrderStatusEnum.CANCELED.getCode(),
                MallOrderOperateTypeConstants.CLOSE,
                "SYSTEM",
                "系统",
                StrUtil.blankToDefault(reason, "支付超时自动取消"));
    }

    /**
     * 扫描并关闭超时的待支付商城订单。
     *
     * @return 本次成功取消的订单数
     */
    @Transactional(readOnly = false)
    public int cancelExpiredPendingOrders() {
        int timeoutMinutes = resolveOrderTimeoutMinutes();
        Date threshold = new Date(System.currentTimeMillis() - timeoutMinutes * 60_000L);
        List<MallOrder> expired = mallOrderService.listExpiredPendingPayOrders(
                MallOrderStatusEnum.PENDING_PAY.getCode(), threshold, EXPIRED_CANCEL_BATCH_LIMIT);
        if (CollUtil.isEmpty(expired)) {
            return 0;
        }
        int count = 0;
        Set<String> processedBatchIds = new HashSet<>();
        for (MallOrder order : expired) {
            if (order == null || StrUtil.isBlank(order.getId())) {
                continue;
            }
            try {
                if (StrUtil.isNotBlank(order.getPayBatchId())) {
                    if (processedBatchIds.add(order.getPayBatchId())) {
                        cancelPayBatchBySystem(order.getPayBatchId(), "支付超时自动取消");
                        count++;
                    }
                } else {
                    cancelPendingOrderBySystem(order.getId(), "支付超时自动取消");
                    count++;
                }
            } catch (Exception ex) {
                log.warn("商城订单支付超时取消失败 orderId={}", order.getId(), ex);
            }
        }
        return count;
    }

    private int resolveOrderTimeoutMinutes() {
        String raw = mallSettingService.getConfigValue(ORDER_TIMEOUT_CONFIG_KEY);
        if (StrUtil.isBlank(raw)) {
            return DEFAULT_ORDER_TIMEOUT_MINUTES;
        }
        try {
            int minutes = Integer.parseInt(raw.trim());
            return minutes > 0 ? minutes : DEFAULT_ORDER_TIMEOUT_MINUTES;
        } catch (NumberFormatException ignored) {
            return DEFAULT_ORDER_TIMEOUT_MINUTES;
        }
    }

    /**
     * 管理端快递发货：选择物流公司、填写运单号与发货说明，记录发货时间并置为履约中。
     */
    @Transactional(readOnly = false)
    public void shipOrder(MallOrderShipDTO dto) {
        MallOrder before = mallOrderService.get(dto.getOrderId());
        if (before == null) {
            throw new BizErrorException("订单不存在");
        }
        Integer beforeStatus = before.getStatus();
        String companyName = resolveLogisticsCompanyName(dto);
        Date shipTime = dto.getShipTime() != null
                ? Date.from(dto.getShipTime().atZone(ZoneId.of("Asia/Shanghai")).toInstant())
                : new Date();
        MallOrder order = mallOrderService.shipDeliveryOrder(
                dto.getOrderId(), companyName, dto.getLogisticsNo().trim(), shipTime);
        String operatorId = WebFrameworkUtils.getLoginUserId();
        String operatorName = WebFrameworkUtils.getLoginUserName();
        mallOrderLogService.recordOrderStatusChange(
                order.getId(),
                order.getOrderNo(),
                beforeStatus,
                MallOrderStatusEnum.FULFILLING.getCode(),
                "ADMIN",
                operatorId,
                operatorName,
                buildShipLogRemark(companyName, dto.getLogisticsNo(), dto.getShipReason()));
        trySyncUnifiedFulfillmentForMallOrder(order);
    }

    private String resolveLogisticsCompanyName(MallOrderShipDTO dto) {
        if (StrUtil.isNotBlank(dto.getLogisticsCompanyId())) {
            MallLogisticsCompany company = mallLogisticsCompanyService.getPlatformCompany(dto.getLogisticsCompanyId().trim());
            if (company == null) {
                throw new BizErrorException("物流公司不存在");
            }
            if (company.getStatus() == null || company.getStatus() != 1) {
                throw new BizErrorException("物流公司未启用");
            }
            if (StrUtil.isNotBlank(company.getCompanyName())) {
                return company.getCompanyName().trim();
            }
        }
        if (StrUtil.isNotBlank(dto.getLogisticsCompany())) {
            return dto.getLogisticsCompany().trim();
        }
        throw new BizErrorException("请选择或填写物流公司");
    }

    private static String buildShipLogRemark(String companyName, String logisticsNo, String shipReason) {
        StringBuilder sb = new StringBuilder("发货：");
        sb.append(companyName).append(' ').append(StrUtil.blankToDefault(logisticsNo, "").trim());
        if (StrUtil.isNotBlank(shipReason)) {
            sb.append("；原因：").append(shipReason.trim());
        }
        return sb.toString();
    }

    /**
     * 管理端自提核销：凭支付成功后的核销码将自提订单置为已完成。
     */
    @Transactional(readOnly = false)
    public void verifyPickupOrder(MallOrderPickupVerifyDTO dto) {
        String operatorId = WebFrameworkUtils.getLoginUserId();
        String operatorName = WebFrameworkUtils.getLoginUserName();
        MallOrder order = mallOrderService.verifyPickupByVerifyCode(dto.getVerifyCode(), operatorId, operatorName);
        mallOrderLogService.recordOrderStatusChange(
                order.getId(),
                order.getOrderNo(),
                MallOrderStatusEnum.PAID.getCode(),
                MallOrderStatusEnum.FULFILLED.getCode(),
                "ADMIN",
                operatorId,
                operatorName,
                "到店自提核销");
        triggerProfitSharingOnComplete(order.getId());
        trySyncUnifiedFulfillmentForMallOrder(order);
    }

    /**
     * 小程序会员确认收货（快递订单）。
     */
    @Transactional(readOnly = false)
    public void confirmReceiveMobileOrder(String orderId, String memberId) {
        MallOrder order = mallOrderService.confirmReceiveByMember(orderId, memberId);
        mallOrderLogService.recordOrderStatusChange(
                order.getId(),
                order.getOrderNo(),
                MallOrderStatusEnum.FULFILLING.getCode(),
                MallOrderStatusEnum.FULFILLED.getCode(),
                "MEMBER",
                memberId,
                null,
                "会员确认收货");
        triggerProfitSharingOnComplete(order.getId());
        trySyncUnifiedFulfillmentForMallOrder(order);
    }

    /**
     * 小程序结算试算：统一走 {@link MallCheckoutPricingSupport}（按商户运费、商户券、首单与平台券分摊）。
     */
    @Transactional(readOnly = true)
    public MallFreightPreviewVO previewFreight(MallFreightPreviewDTO dto, String memberId) {
        if (StrUtil.isBlank(memberId)) {
            throw new BizErrorException(LOGIN_REQUIRED_MESSAGE);
        }
        MallDeliveryTypeEnum deliveryKind = resolveDeliveryType(dto.getDeliveryType());
        MallMemberAddress addr = null;
        if (deliveryKind == MallDeliveryTypeEnum.DELIVERY) {
            if (StrUtil.isBlank(dto.getAddressId())) {
                throw new BizErrorException("收货地址不能为空");
            }
            addr = mallMemberAddressService.get(dto.getAddressId());
            if (addr == null || !memberId.equals(addr.getMemberId())) {
                throw new BizErrorException("收货地址不存在");
            }
        }
        MallCheckoutPricingRequest pricingRequest = buildPreviewPricingRequest(dto, memberId, deliveryKind, addr);
        MallCheckoutPricingResultVO priced = mallCheckoutPricingSupport.price(pricingRequest);
        return toFreightPreviewVO(priced);
    }

    /**
     * 小程序提交商城订单：按商户（tenant）拆子单，创建合并支付批次并扣减库存。
     */
    @Transactional(readOnly = false)
    public MallOrderSubmitResultVO submitMobileOrder(MallOrderSubmitDTO dto, String memberId) {
        if (StrUtil.isBlank(memberId)) {
            throw new BizErrorException(LOGIN_REQUIRED_MESSAGE);
        }
        MallDeliveryTypeEnum deliveryKind = resolveDeliveryType(dto.getDeliveryType());
        MallMemberAddress addr = null;
        MallPickupPoint pickupPoint = null;

        if (deliveryKind == MallDeliveryTypeEnum.DELIVERY) {
            if (StrUtil.isBlank(dto.getAddressId())) {
                throw new BizErrorException("收货地址不能为空");
            }
            addr = mallMemberAddressService.get(dto.getAddressId());
            if (addr == null || !memberId.equals(addr.getMemberId())) {
                throw new BizErrorException("收货地址不存在");
            }
        } else {
            if (StrUtil.isBlank(dto.getPickupPointId())) {
                throw new BizErrorException("请选择自提点");
            }
            pickupPoint = mallPickupPointService.get(dto.getPickupPointId());
            if (pickupPoint == null) {
                throw new BizErrorException("自提点不存在");
            }
            if (!MallPickupPointStatusEnum.ENABLED.getValue().equals(pickupPoint.getStatus())) {
                throw new BizErrorException("自提点已停用");
            }
            if (StrUtil.isNotBlank(dto.getAddressId())) {
                addr = mallMemberAddressService.get(dto.getAddressId());
                if (addr == null || !memberId.equals(addr.getMemberId())) {
                    throw new BizErrorException("收货地址不存在");
                }
            }
        }

        MallCheckoutPricingRequest pricingRequest = buildSubmitPricingRequest(dto, memberId, deliveryKind, addr);
        MallCheckoutPricingResultVO priced = mallCheckoutPricingSupport.price(pricingRequest);
        if (priced.getTotalAmount() == null || priced.getTotalAmount().compareTo(BigDecimal.ZERO) < 0) {
            throw new BizErrorException("订单金额无效");
        }

        MallPayBatch batch = new MallPayBatch();
        batch.setBatchNo(composePayBatchNo());
        batch.setMemberId(memberId);
        batch.setGoodsAmount(priced.getGoodsAmount());
        batch.setFreightAmount(priced.getFreightAmount());
        batch.setTotalAmount(priced.getTotalAmount());
        batch.setDiscountAmount(priced.getFirstOrderDiscountAmount());
        batch.setMerchantCouponAmount(priced.getMerchantCouponAmount());
        batch.setPlatformCouponAmount(priced.getPlatformCouponAmount());
        batch.setCouponAmount(priced.getCouponAmount());
        batch.setPayAmount(priced.getPayAmount());
        batch.setStatus(MallPayBatchStatusEnum.PENDING_PAY.getCode());
        batch.setDeliveryType(deliveryKind.getCode());
        batch.setBuyerRemark(StrUtil.blankToDefault(dto.getBuyerRemark(), ""));
        batch.setTenantCode(PlatformTenantConstants.PLATFORM_TENANT_CODE);
        batch.setTenantId(PlatformTenantConstants.PLATFORM_TENANT_ID);
        mallPayBatchService.createPayBatch(batch);

        List<MallOrder> createdOrders = new ArrayList<>();
        List<MallOrderItem> allItems = new ArrayList<>();
        for (MallCheckoutPricingResultVO.TenantPricingLine line : priced.getTenantLines()) {
            BigDecimal subTotalBefore = line.getGoodsAmount().add(line.getFreightAmount())
                    .setScale(2, RoundingMode.HALF_UP);
            BigDecimal subDiscount = line.getFirstOrderDiscountAmount() == null
                    ? BigDecimal.ZERO : line.getFirstOrderDiscountAmount();
            BigDecimal subMerchantCoupon = line.getMerchantCouponAmount() == null
                    ? BigDecimal.ZERO : line.getMerchantCouponAmount();
            BigDecimal subPlatformCoupon = line.getPlatformCouponAmount() == null
                    ? BigDecimal.ZERO : line.getPlatformCouponAmount();
            BigDecimal subCoupon = subMerchantCoupon.add(subPlatformCoupon).setScale(2, RoundingMode.HALF_UP);

            MallOrder order = new MallOrder();
            order.setMemberId(memberId);
            if (dto != null && StrUtil.isNotBlank(dto.getShareMemberId())) {
                order.setShareMemberId(dto.getShareMemberId());
                order.setShareAttributionJson(dto.getShareAttributionJson());
            }
            order.setPayBatchId(batch.getId());
            order.setOrderNo(composeMobileOrderNo());
            order.setGoodsAmount(line.getGoodsAmount());
            order.setFreightAmount(line.getFreightAmount());
            order.setTotalAmount(subTotalBefore);
            order.setPayAmount(line.getPayAmount());
            order.setDiscountAmount(subDiscount);
            order.setCouponAmount(subCoupon);
            order.setMerchantCouponAmount(subMerchantCoupon);
            order.setPlatformCouponAmount(subPlatformCoupon);
            order.setPointsAmount(BigDecimal.ZERO);
            order.setCoinsAmount(BigDecimal.ZERO);
            order.setBalanceAmount(BigDecimal.ZERO);
            order.setPointsUsed(0);
            order.setCoinsUsed(0);
            order.setTenantCode(line.getTenantCode());
            order.setTenantId(line.getTenantId());
            if (StrUtil.isNotBlank(order.getTenantCode()) || StrUtil.isNotBlank(order.getTenantId())) {
                order.closeTenantCodeAutoSet();
            }
            applyReceiverContact(order, addr, pickupPoint, deliveryKind);
            order.setBuyerRemark(StrUtil.blankToDefault(dto.getBuyerRemark(), ""));
            order.setStatus(MallOrderStatusEnum.PENDING_PAY.getCode());
            order.setDeliveryType(deliveryKind.getCode());
            if (deliveryKind == MallDeliveryTypeEnum.PICKUP) {
                order.setPickupPointId(Objects.requireNonNull(pickupPoint, "pickupPoint").getId());
                order.setPickupTime(dto.getPickupTime());
            }

            mallOrderService.createPendingMobileOrder(order, line.getLines());
            createdOrders.add(order);
            allItems.addAll(line.getLines());
        }

        freezeCouponsOnSubmit(dto, memberId, batch, priced, createdOrders);

        if (isMallDualWriteEnabled()) {
            trySyncUnifiedPendingOnSubmitBatch(batch, createdOrders, allItems);
            trySyncUnifiedMarkPaidIfZeroAmountBatch(batch);
        }

        List<String> orderIds = createdOrders.stream().map(MallOrder::getId).collect(Collectors.toList());
        if (priced.getPayAmount().compareTo(BigDecimal.ZERO) == 0) {
            for (MallOrder subOrder : createdOrders) {
                mallOrderService.confirmZeroAmountPay(subOrder.getId());
            }
            couponCheckoutApi.confirmAllFrozenForMallBatch(batch.getId(), orderIds);
            batch.setStatus(MallPayBatchStatusEnum.PAID.getCode());
            mallPayBatchService.updatePayBatch(batch);
        }

        MallOrder firstOrder = createdOrders.get(0);
        MallOrderSubmitResultVO vo = new MallOrderSubmitResultVO();
        vo.setBatchId(batch.getId());
        vo.setBatchNo(batch.getBatchNo());
        vo.setOrderIds(orderIds);
        vo.setOrderId(firstOrder.getId());
        vo.setOrderNo(firstOrder.getOrderNo());
        vo.setTotalAmount(priced.getPayAmount().toPlainString());
        vo.setPayAmount(priced.getPayAmount().toPlainString());
        return vo;
    }

    private MallCheckoutPricingRequest buildPreviewPricingRequest(MallFreightPreviewDTO dto, String memberId,
                                                                  MallDeliveryTypeEnum deliveryKind,
                                                                  MallMemberAddress addr) {
        MallCheckoutPricingRequest request = new MallCheckoutPricingRequest();
        request.setMemberId(memberId);
        request.setDeliveryType(deliveryKind);
        request.setAddress(addr);
        request.setItems(dto.getItems());
        request.setPlatformMemberCouponId(dto.getPlatformMemberCouponId());
        request.setLegacyMemberCouponId(dto.getMemberCouponId());
        if (CollUtil.isNotEmpty(dto.getMerchantCoupons())) {
            request.setMerchantCoupons(dto.getMerchantCoupons());
        }
        return request;
    }

    private MallCheckoutPricingRequest buildSubmitPricingRequest(MallOrderSubmitDTO dto, String memberId,
                                                                 MallDeliveryTypeEnum deliveryKind,
                                                                 MallMemberAddress addr) {
        MallCheckoutPricingRequest request = new MallCheckoutPricingRequest();
        request.setMemberId(memberId);
        request.setDeliveryType(deliveryKind);
        request.setAddress(addr);
        request.setItems(dto.getItems());
        request.setPlatformMemberCouponId(dto.getPlatformMemberCouponId());
        request.setLegacyMemberCouponId(dto.getMemberCouponId());
        if (CollUtil.isNotEmpty(dto.getMerchantCoupons())) {
            request.setMerchantCoupons(dto.getMerchantCoupons());
        }
        return request;
    }

    private MallFreightPreviewVO toFreightPreviewVO(MallCheckoutPricingResultVO priced) {
        MallFreightPreviewVO vo = new MallFreightPreviewVO();
        vo.setGoodsAmount(priced.getGoodsAmount());
        vo.setFreightAmount(priced.getFreightAmount());
        vo.setFirstOrderDiscountAmount(priced.getFirstOrderDiscountAmount());
        vo.setMerchantCouponAmount(priced.getMerchantCouponAmount());
        vo.setPlatformCouponAmount(priced.getPlatformCouponAmount());
        vo.setCouponDiscountAmount(priced.getCouponAmount());
        vo.setPayAmount(priced.getPayAmount());
        vo.setMerchantBreakdown(priced.getMerchantBreakdown());
        return vo;
    }

    private void freezeCouponsOnSubmit(MallOrderSubmitDTO dto, String memberId, MallPayBatch batch,
                                       MallCheckoutPricingResultVO priced, List<MallOrder> createdOrders) {
        String platformCouponId = resolvePlatformCouponIdForSubmit(dto);
        if (StrUtil.isNotBlank(platformCouponId)
                && priced.getPlatformCouponAmount() != null
                && priced.getPlatformCouponAmount().compareTo(BigDecimal.ZERO) > 0) {
            CouponCheckoutContextDTO platformCtx = mallCouponCheckoutContextSupport.buildContext(
                    memberId, priced.getGoodsAmount(), dto.getItems());
            CouponFreezeCommandDTO freezeCommand = new CouponFreezeCommandDTO();
            freezeCommand.setMemberId(memberId);
            freezeCommand.setMemberCouponId(platformCouponId);
            freezeCommand.setOrderId(batch.getId());
            freezeCommand.setOrderNo(batch.getBatchNo());
            freezeCommand.setCheckoutContext(platformCtx);
            freezeCommand.setExpectedDiscount(priced.getPlatformCouponAmount());
            freezeCommand.setFreezeReason("商城合并支付待支付");
            couponCheckoutApi.freezeForOrder(freezeCommand);
        }

        Map<String, MallOrder> orderByTenantKey = new LinkedHashMap<>();
        for (MallOrder order : createdOrders) {
            String key = StrUtil.blankToDefault(order.getTenantCode(), order.getTenantId());
            orderByTenantKey.put(key, order);
        }
        for (MallCheckoutPricingResultVO.TenantPricingLine line : priced.getTenantLines()) {
            if (StrUtil.isBlank(line.getMerchantMemberCouponId())
                    || line.getMerchantCouponAmount() == null
                    || line.getMerchantCouponAmount().compareTo(BigDecimal.ZERO) <= 0) {
                continue;
            }
            MallOrder subOrder = orderByTenantKey.get(line.getTenantKey());
            if (subOrder == null) {
                continue;
            }
            BigDecimal merchantBase = line.getGoodsAmount().add(line.getFreightAmount())
                    .setScale(2, RoundingMode.HALF_UP);
            CouponCheckoutContextDTO merchantCtx =
                    mallCouponCheckoutContextSupport.buildContext(memberId, merchantBase, line.getLineDtos());
            CouponFreezeCommandDTO freezeCommand = new CouponFreezeCommandDTO();
            freezeCommand.setMemberId(memberId);
            freezeCommand.setMemberCouponId(line.getMerchantMemberCouponId());
            freezeCommand.setOrderId(subOrder.getId());
            freezeCommand.setOrderNo(subOrder.getOrderNo());
            freezeCommand.setCheckoutContext(merchantCtx);
            freezeCommand.setExpectedDiscount(line.getMerchantCouponAmount());
            freezeCommand.setFreezeReason("商城订单待支付");
            couponCheckoutApi.freezeForOrder(freezeCommand);
        }
    }

    private String resolvePlatformCouponIdForSubmit(MallOrderSubmitDTO dto) {
        if (StrUtil.isNotBlank(dto.getPlatformMemberCouponId())) {
            return dto.getPlatformMemberCouponId().trim();
        }
        if (StrUtil.isBlank(dto.getMemberCouponId())) {
            return null;
        }
        String legacyId = dto.getMemberCouponId().trim();
        String tenantCode = couponCheckoutApi.resolveTemplateTenantCode(legacyId);
        if (PlatformTenantConstants.isPlatformTenant(tenantCode)) {
            return legacyId;
        }
        return null;
    }

    private String resolvePrepareBatchId(MallPreparePayDTO dto, String memberId) {
        if (dto == null) {
            return null;
        }
        if (StrUtil.isNotBlank(dto.getBatchId())) {
            return dto.getBatchId().trim();
        }
        if (StrUtil.isBlank(dto.getOrderId())) {
            return null;
        }
        MallOrder order = mallOrderService.get(dto.getOrderId().trim());
        if (order == null) {
            throw new BizErrorException("订单不存在");
        }
        if (!memberId.equals(order.getMemberId())) {
            throw new BizErrorException("无权操作该订单");
        }
        return StrUtil.trimToNull(order.getPayBatchId());
    }

    private MallPayPrepareVO preparePayForBatch(String batchId, MallPreparePayDTO dto, String memberId) {
        MallPayBatch batch = mallPayBatchService.get(batchId);
        mallPayBatchService.assertPayPreparable(batch, memberId);
        List<MallOrder> subOrders = mallOrderService.listByPayBatchId(batchId);
        if (CollUtil.isEmpty(subOrders)) {
            throw new BizErrorException("订单不存在");
        }
        String payChannel = StrUtil.blankToDefault(dto.getPayChannel(), MallPayTypeEnum.WECHAT_JSAPI.getCode());
        mallPayBatchService.markPayPreparing(batch, payChannel);
        for (MallOrder sub : subOrders) {
            mallOrderService.assertPayPreparable(sub, memberId);
        }
        String payOrderId = mallOrderPayAssembler.newPayOrderId();
        MallPayPrepareVO vo = mallOrderPayAssembler.buildBatchPrepareVo(batch, subOrders, payChannel, payOrderId);
        applyPaySettle(vo, subOrders, vo.getAmountFen() == null ? 0L : vo.getAmountFen());
        return vo;
    }

    private void applyPaySettle(MallPayPrepareVO vo, List<MallOrder> orders, long totalAmountFen) {
        if (vo == null) {
            return;
        }
        List<String> merchantTenantIds = orders == null ? Collections.emptyList()
                : orders.stream().map(MallOrder::getTenantId).filter(StrUtil::isNotBlank).collect(Collectors.toList());
        PaySettleResolveVO resolve = merchantSettleConfigApi.resolvePaySettle(merchantTenantIds);
        if (resolve != null) {
            vo.setSettleMode(resolve.getSettleMode());
            vo.setNeedProfitSharing(resolve.getNeedProfitSharing());
        }
        fillSubPayItems(vo, orders, totalAmountFen);
    }

    private void fillSubPayItems(MallPayPrepareVO vo, List<MallOrder> orders, long totalAmountFen) {
        List<MallPaySubItemVO> items = new ArrayList<>();
        if (orders == null || orders.isEmpty()) {
            vo.setSubPayItems(items);
            return;
        }
        long assigned = 0L;
        for (int i = 0; i < orders.size(); i++) {
            MallOrder order = orders.get(i);
            if (order == null || StrUtil.isBlank(order.getTenantId())) {
                continue;
            }
            MerchantSettleConfigSnapshotVO snap = merchantSettleConfigApi.resolveSnapshot(order.getTenantId());
            long subFen;
            if (environment.matchesProfiles(PROFILE_PRO)) {
                subFen = calcAmountFen(order.getPayAmount() == null ? BigDecimal.ZERO : order.getPayAmount());
            } else if (i == orders.size() - 1) {
                subFen = Math.max(1L, totalAmountFen - assigned);
            } else {
                subFen = Math.max(1L, totalAmountFen / orders.size());
                assigned += subFen;
            }
            MallPaySubItemVO item = new MallPaySubItemVO();
            item.setMerchantTenantId(order.getTenantId());
            item.setBizOrderId(order.getId());
            item.setAmountFen(subFen);
            item.setNeedProfitSharing(Boolean.TRUE.equals(snap.getNeedProfitSharing()));
            items.add(item);
        }
        vo.setSubPayItems(items);
    }

    private String resolveConfirmBatchId(MallConfirmPayDTO dto) {
        if (StrUtil.isNotBlank(dto.getBatchId())) {
            return dto.getBatchId().trim();
        }
        if (StrUtil.isBlank(dto.getOrderId())) {
            return null;
        }
        String id = dto.getOrderId().trim();
        MallPayBatch batch = mallPayBatchService.get(id);
        if (batch != null) {
            return id;
        }
        MallOrder order = mallOrderService.get(id);
        if (order != null && StrUtil.isNotBlank(order.getPayBatchId())) {
            return order.getPayBatchId().trim();
        }
        return null;
    }

    /**
     * 合并支付批次确认（微信合单/批次回调）：批次置已支付 → 逐个子单 confirmPaySuccess → 核销冻券 → 同步统一单。
     * <p>幂等：批次已 PAID 时仅补偿子单确认。</p>
     */
    private void confirmPaySuccessForBatch(MallConfirmPayDTO dto, MallPayBatch batch) {
        // 幂等：批次已支付时补偿未确认的子单
        if (MallPayBatchStatusEnum.PAID.getCode().equals(batch.getStatus())) {
            ensureSubOrdersPaidForBatch(batch, dto);
            return;
        }
        if (!MallPayBatchStatusEnum.PENDING_PAY.getCode().equals(batch.getStatus())) {
            throw new BizErrorException("当前状态不可确认支付");
        }
        // ① 批次主单置已支付
        batch.setStatus(MallPayBatchStatusEnum.PAID.getCode());
        batch.setPayTime(new Date());
        batch.setPayType(MallPayTypeEnum.WECHAT.getCode());
        mallPayBatchService.updatePayBatch(batch);

        // ② 逐个子单 confirmPaySuccess + 佣金/积分
        List<MallOrder> subOrders = mallOrderService.listByPayBatchId(batch.getId());
        List<String> subOrderIds = new ArrayList<>();
        for (MallOrder sub : subOrders) {
            if (sub == null || StrUtil.isBlank(sub.getId())) {
                continue;
            }
            MallConfirmPayDTO subDto = new MallConfirmPayDTO();
            subDto.setOrderId(sub.getId());
            subDto.setPayOrderId(dto.getPayOrderId());
            subDto.setThirdPartyTradeNo(dto.getThirdPartyTradeNo());
            mallOrderService.confirmPaySuccess(subDto);
            subOrderIds.add(sub.getId());
            triggerPointsOnPaySuccess(sub.getId());
        }
        // ③ 整批核销冻券 + 首单赠券 + 统一单 + 分账
        couponCheckoutApi.confirmAllFrozenForMallBatch(batch.getId(), subOrderIds);
        mallFirstOrderPromoSupport.tryGrantCouponsAfterFirstPaidOrder(batch.getMemberId());
        if (isMallDualWriteEnabled()) {
            trySyncUnifiedPaidForBatch(batch);
        }
        triggerProfitSharingPending(dto, batch.getId());
    }

    private void ensureSubOrdersPaidForBatch(MallPayBatch batch, MallConfirmPayDTO dto) {
        // 补偿：批次已 PAID 但个别子单仍待支付时逐单确认
        List<MallOrder> subOrders = mallOrderService.listByPayBatchId(batch.getId());
        for (MallOrder sub : subOrders) {
            if (sub == null || StrUtil.isBlank(sub.getId())) {
                continue;
            }
            if (MallOrderStatusEnum.PAID.getCode().equals(sub.getStatus())) {
                continue;
            }
            MallConfirmPayDTO subDto = new MallConfirmPayDTO();
            subDto.setOrderId(sub.getId());
            subDto.setPayOrderId(dto.getPayOrderId());
            subDto.setThirdPartyTradeNo(dto.getThirdPartyTradeNo());
            mallOrderService.confirmPaySuccess(subDto);
            triggerPointsOnPaySuccess(sub.getId());
        }
    }

    private void trySyncUnifiedPaidForBatch(MallPayBatch batch) {
        if (batch == null || !MallPayBatchStatusEnum.PAID.getCode().equals(batch.getStatus())) {
            return;
        }
        if (StrUtil.isBlank(batch.getUnifiedOrderId())) {
            return;
        }
        String payRef = StrUtil.blankToDefault(batch.getBatchNo(), "MALL-BATCH-" + batch.getId());
        String channel = StrUtil.blankToDefault(batch.getPayType(), MallPayTypeEnum.WECHAT.getCode());
        TradeOrder existing = tradeOrderFacadeApi.get(batch.getUnifiedOrderId());
        if (existing != null
                && TradeOrderStatusEnum.PENDING_PAY.getValue().equals(existing.getStatus())) {
            tradeOrderFacadeApi.markPaid(batch.getUnifiedOrderId(), payRef, channel);
        }
    }

    private String resolveCancelBatchId(MallOrderCancelDTO dto) {
        if (StrUtil.isNotBlank(dto.getBatchId())) {
            return dto.getBatchId().trim();
        }
        if (StrUtil.isBlank(dto.getOrderId())) {
            return null;
        }
        MallOrder order = mallOrderService.get(dto.getOrderId().trim());
        if (order == null) {
            return null;
        }
        return StrUtil.trimToNull(order.getPayBatchId());
    }

    private void cancelPayBatchByMember(String batchId, String memberId, String cancelReason) {
        MallPayBatch batch = mallPayBatchService.get(batchId);
        if (batch == null) {
            throw new BizErrorException("订单不存在");
        }
        if (!memberId.equals(batch.getMemberId())) {
            throw new BizErrorException("无权操作该订单");
        }
        if (!MallPayBatchStatusEnum.PENDING_PAY.getCode().equals(batch.getStatus())) {
            throw new BizErrorException("当前状态不可取消");
        }
        cancelPayBatchCore(batch, cancelReason, memberId, "会员", MallOrderOperateTypeConstants.CANCEL);
    }

    private void cancelPayBatchBySystem(String batchId, String reason) {
        MallPayBatch batch = mallPayBatchService.get(batchId);
        if (batch == null) {
            return;
        }
        if (!MallPayBatchStatusEnum.PENDING_PAY.getCode().equals(batch.getStatus())) {
            return;
        }
        cancelPayBatchCore(batch, reason, "SYSTEM", "系统", MallOrderOperateTypeConstants.CLOSE);
    }

    private void cancelPayBatchCore(MallPayBatch batch, String reason, String operatorId, String operatorName,
                                    String operateType) {
        List<MallOrder> subOrders = mallOrderService.listByPayBatchId(batch.getId());
        List<String> subOrderIds = new ArrayList<>();
        for (MallOrder sub : subOrders) {
            if (sub == null || StrUtil.isBlank(sub.getId())) {
                continue;
            }
            if (!MallOrderStatusEnum.PENDING_PAY.getCode().equals(sub.getStatus())) {
                continue;
            }
            Integer beforeStatus = sub.getStatus();
            MallOrder canceled = MallOrderOperateTypeConstants.CANCEL.equals(operateType)
                    ? mallOrderService.cancelPendingMobileOrder(sub.getId(), batch.getMemberId())
                    : mallOrderService.cancelPendingOrderBySystem(sub.getId());
            subOrderIds.add(canceled.getId());
            mallOrderLogService.recordOrderStatusChange(
                    canceled.getId(),
                    canceled.getOrderNo(),
                    beforeStatus,
                    MallOrderStatusEnum.CANCELED.getCode(),
                    operateType,
                    operatorId,
                    operatorName,
                    StrUtil.blankToDefault(reason, "订单已取消"));
        }
        couponCheckoutApi.releaseAllFrozenForMallBatch(batch.getId(), subOrderIds,
                StrUtil.blankToDefault(reason, "订单已取消"));
        batch.setStatus(MallPayBatchStatusEnum.CLOSED.getCode());
        mallPayBatchService.updatePayBatch(batch);
        if (isMallDualWriteEnabled() && StrUtil.isNotBlank(batch.getUnifiedOrderId())) {
            tradeOrderFacadeApi.cancelPending(batch.getUnifiedOrderId());
        }
    }

    private static long calcAmountFen(BigDecimal amount) {
        if (amount == null) {
            return 0L;
        }
        return amount.multiply(BigDecimal.valueOf(100)).setScale(0, RoundingMode.HALF_UP).longValue();
    }

    private void releaseMallOrderCoupons(MallOrder order, String reason) {
        if (order == null) {
            return;
        }
        String safeReason = StrUtil.blankToDefault(reason, "订单已取消");
        if (StrUtil.isNotBlank(order.getPayBatchId())) {
            List<MallOrder> batchOrders = mallOrderService.listByPayBatchId(order.getPayBatchId());
            List<String> subOrderIds = batchOrders.stream()
                    .map(MallOrder::getId)
                    .filter(StrUtil::isNotBlank)
                    .collect(Collectors.toList());
            couponCheckoutApi.releaseAllFrozenForMallBatch(order.getPayBatchId(), subOrderIds, safeReason);
            return;
        }
        couponCheckoutApi.releaseFrozen(order.getId(), safeReason);
    }

    private void confirmMallOrderCoupons(MallOrder order) {
        if (order == null) {
            return;
        }
        if (StrUtil.isNotBlank(order.getPayBatchId())) {
            List<MallOrder> batchOrders = mallOrderService.listByPayBatchId(order.getPayBatchId());
            List<String> subOrderIds = batchOrders.stream()
                    .map(MallOrder::getId)
                    .filter(StrUtil::isNotBlank)
                    .collect(Collectors.toList());
            couponCheckoutApi.confirmAllFrozenForMallBatch(order.getPayBatchId(), subOrderIds);
            return;
        }
        couponCheckoutApi.confirmOrderPaid(order.getId());
    }

    private static MallDeliveryTypeEnum resolveDeliveryType(String deliveryTypeRaw) {
        String normalized = StrUtil.blankToDefault(deliveryTypeRaw, MallDeliveryTypeEnum.DELIVERY.getCode());
        MallDeliveryTypeEnum kind = MallDeliveryTypeEnum.fromCode(normalized);
        if (kind == null) {
            throw new BizErrorException("配送方式无效");
        }
        return kind;
    }

    private static void applyReceiverContact(MallOrder order, MallMemberAddress addr, MallPickupPoint point,
                                           MallDeliveryTypeEnum deliveryKind) {
        if (addr != null) {
            order.setReceiverName(addr.getReceiverName());
            order.setReceiverMobile(addr.getReceiverMobile());
            order.setReceiverProvince(addr.getProvince());
            order.setReceiverCity(addr.getCity());
            order.setReceiverDistrict(addr.getDistrict());
            order.setReceiverAddress(addr.getDetailAddress());
            return;
        }
        if (deliveryKind == MallDeliveryTypeEnum.PICKUP && point != null) {
            order.setReceiverName(StrUtil.blankToDefault(point.getPointName(), "到店自提"));
            order.setReceiverMobile(StrUtil.blankToDefault(point.getContactMobile(), ""));
            order.setReceiverProvince(StrUtil.blankToDefault(point.getProvince(), ""));
            order.setReceiverCity(StrUtil.blankToDefault(point.getCity(), ""));
            order.setReceiverDistrict(StrUtil.blankToDefault(point.getDistrict(), ""));
            order.setReceiverAddress(
                    StrUtil.blankToDefault(point.getProvince(), "")
                            + StrUtil.blankToDefault(point.getCity(), "")
                            + StrUtil.blankToDefault(point.getDistrict(), "")
                            + StrUtil.blankToDefault(point.getAddress(), ""));
        }
    }

    private static String composeMobileOrderNo() {
        return "MO" + UUID.randomUUID().toString().replace("-", "");
    }

    private static String composePayBatchNo() {
        return "MB" + UUID.randomUUID().toString().replace("-", "");
    }

    /**
     * 支付成功确认（微信回调 / mock-success 均调用此方法）。
     *
     * <p>合并支付：attach.orderId 为 batchId，本方法确认整批子单 + 核销全部冻结券 + 同步统一单。</p>
     */
    @Transactional(readOnly = false)
    public void confirmPaySuccess(MallConfirmPayDTO dto) {
        if (dto == null) {
            throw new BizErrorException("订单不存在");
        }
        // 合并支付：attach.orderId 为 mall_pay_batch.id，走整批确认
        String batchId = resolveConfirmBatchId(dto);
        if (StrUtil.isNotBlank(batchId)) {
            MallPayBatch batch = mallPayBatchService.get(batchId);
            if (batch != null) {
                confirmPaySuccessForBatch(dto, batch);
                return;
            }
        }
        if (StrUtil.isBlank(dto.getOrderId())) {
            throw new BizErrorException("订单不存在");
        }
        // ① 领域层：子单待支付 → 已支付（幂等）
        mallOrderService.confirmPaySuccess(dto);
        MallOrder after = mallOrderService.get(dto.getOrderId());
        if (after != null) {
            // ② 核销冻券 + 首单赠券
            confirmMallOrderCoupons(after);
            mallFirstOrderPromoSupport.tryGrantCouponsAfterFirstPaidOrder(after.getMemberId());
            // ③ 同步统一单
            if (isMallDualWriteEnabled()) {
                trySyncUnifiedPaidIfNeeded(after);
            }
            // ④ 分账、商户应收、分销佣金、积分等后置
            triggerProfitSharingPending(dto, after.getPayBatchId());
            triggerMerchantReceivable(dto, after.getPayBatchId());
            triggerPointsOnPaySuccess(after.getId());
        }
    }

    /**
     * 支付成功后触发订单消费送积分（幂等；失败不阻断主支付流程）。
     */
    private void triggerPointsOnPaySuccess(String mallOrderId) {
        if (StrUtil.isBlank(mallOrderId)) {
            return;
        }
        try {
            memberPointsOrderApi.onOrderPaid(PayRecordBizSceneEnum.MALL_ORDER.getCode(), mallOrderId.trim());
        } catch (Exception ex) {
            log.warn("商城订单消费送积分失败 orderId={}: {}", mallOrderId, ex.getMessage());
        }
    }

    private void triggerProfitSharingPending(MallConfirmPayDTO dto, String payBatchId) {
        if (dto == null || StrUtil.isBlank(dto.getPayOrderId())) {
            return;
        }
        try {
            profitSharingApi.createPendingForMallPay(
                    dto.getPayOrderId(), payBatchId, dto.getThirdPartyTradeNo());
        } catch (Exception ex) {
            log.warn("创建待分账记录失败 payOrderId={}: {}", dto.getPayOrderId(), ex.getMessage());
        }
    }

    private void triggerMerchantReceivable(MallConfirmPayDTO dto, String payBatchId) {
        if (dto == null || StrUtil.isBlank(dto.getPayOrderId())) {
            return;
        }
        try {
            merchantReceivableApi.recordForMallPay(
                    dto.getPayOrderId(), payBatchId, dto.getThirdPartyTradeNo());
        } catch (Exception ex) {
            log.warn("记商户应收失败 payOrderId={}: {}", dto.getPayOrderId(), ex.getMessage());
        }
    }

    private void triggerProfitSharingOnComplete(String mallOrderId) {
        if (StrUtil.isBlank(mallOrderId)) {
            return;
        }
        try {
            profitSharingApi.scheduleSharingBySubOrder(mallOrderId);
        } catch (Exception ex) {
            log.warn("子单完成触发分账失败 orderId={}: {}", mallOrderId, ex.getMessage());
        }
    }

    @Transactional(readOnly = false)
    public void save(MallOrder order) {
        if (StrUtil.isBlank(order.getId())) {
            mallOrderService.createOrder(order);
            return;
        }
        mallOrderService.updateOrder(order);
        if (isMallDualWriteEnabled()) {
            MallOrder persisted = mallOrderService.get(order.getId());
            trySyncUnifiedPaidIfNeeded(persisted);
        }
    }

    @Transactional(readOnly = false)
    public void delete(String ids) {
        mallOrderService.batchDelete(StrUtil.splitTrim(ids, ','));
    }

    /**
     * 小程序下单成功：按支付批次写入待支付 {@code biz_trade_order}（合并支付一条统一单）。
     */
    private void trySyncUnifiedPendingOnSubmitBatch(MallPayBatch batch, List<MallOrder> orders,
                                                    List<MallOrderItem> allLines) {
        if (batch == null || StrUtil.isBlank(batch.getId()) || StrUtil.isNotBlank(batch.getUnifiedOrderId())) {
            return;
        }
        BigDecimal pay = batch.getPayAmount() == null ? BigDecimal.ZERO : batch.getPayAmount();
        TradeOrderCreateDTO dto = buildMallBatchTradeOrderCreateDto(batch, pay, orders, allLines);
        String createdId = tradeOrderFacadeApi.createPendingPayOrder(dto);
        batch.setUnifiedOrderId(createdId);
        mallPayBatchService.updatePayBatch(batch);
    }

    /**
     * 0 元支付批次：统一单立即标记已支付。
     */
    private void trySyncUnifiedMarkPaidIfZeroAmountBatch(MallPayBatch batch) {
        if (batch == null || StrUtil.isBlank(batch.getUnifiedOrderId())) {
            return;
        }
        BigDecimal pay = batch.getPayAmount() == null ? BigDecimal.ZERO : batch.getPayAmount();
        if (pay.compareTo(BigDecimal.ZERO) > 0) {
            return;
        }
        String payRef = StrUtil.blankToDefault(batch.getBatchNo(), "MALL-BATCH-" + batch.getId());
        TradeOrder existing = tradeOrderFacadeApi.get(batch.getUnifiedOrderId());
        if (existing != null
                && TradeOrderStatusEnum.PENDING_PAY.getValue().equals(existing.getStatus())) {
            tradeOrderFacadeApi.markPaid(batch.getUnifiedOrderId(), payRef, "RESERVE");
        }
    }

    /**
     * @deprecated 历史补偿仍可使用；新下单请走 {@link #trySyncUnifiedPendingOnSubmitBatch}
     */
    private void trySyncUnifiedPendingOnSubmit(MallOrder order, List<MallOrderItem> lines) {
        if (order == null || StrUtil.isBlank(order.getId())) {
            return;
        }
        if (StrUtil.isNotBlank(order.getUnifiedOrderId())) {
            return;
        }
        BigDecimal pay = resolvePayAmount(order);
        if (pay == null) {
            pay = BigDecimal.ZERO;
        }
        List<MallOrderItem> mallLines = lines != null && !lines.isEmpty()
            ? lines
            : mallOrderItemService.findByOrderIds(Collections.singletonList(order.getId()));
        TradeOrderCreateDTO dto = buildMallTradeOrderCreateDto(order, pay, mallLines);
        String createdId = tradeOrderFacadeApi.createPendingPayOrder(dto);
        order.setUnifiedOrderId(createdId);
        mallOrderService.updateOrder(order);
    }

    /**
     * 0 元商城单：写入统一单后立即标记已支付。
     */
    private void trySyncUnifiedMarkPaidIfZeroAmount(MallOrder order) {
        if (order == null || StrUtil.isBlank(order.getUnifiedOrderId())) {
            return;
        }
        BigDecimal pay = resolvePayAmount(order);
        if (pay == null) {
            pay = BigDecimal.ZERO;
        }
        if (pay.compareTo(BigDecimal.ZERO) > 0) {
            return;
        }
        String payRef = StrUtil.blankToDefault(order.getOrderNo(), "MALL-" + order.getId());
        TradeOrder existing = tradeOrderFacadeApi.get(order.getUnifiedOrderId());
        if (existing != null
            && TradeOrderStatusEnum.PENDING_PAY.getValue().equals(existing.getStatus())) {
            tradeOrderFacadeApi.markPaid(order.getUnifiedOrderId(), payRef, "RESERVE");
        }
    }

    /**
     * 商城单已支付：将已有统一单标记为已支付；无统一单时走历史补偿（建单后立即 markPaid）。
     */
    private void trySyncUnifiedPaidIfNeeded(MallOrder after) {
        if (after == null || !MallOrderStatusEnum.PAID.getCode().equals(after.getStatus())) {
            return;
        }
        BigDecimal pay = resolvePayAmount(after);
        if (pay == null) {
            pay = BigDecimal.ZERO;
        }
        String payRef = StrUtil.blankToDefault(after.getOrderNo(), "MALL-" + after.getId());
        String channel = StrUtil.blankToDefault(after.getPayType(), "NONE");

        if (StrUtil.isNotBlank(after.getUnifiedOrderId())) {
            TradeOrder existing = tradeOrderFacadeApi.get(after.getUnifiedOrderId());
            if (existing != null
                && TradeOrderStatusEnum.PENDING_PAY.getValue().equals(existing.getStatus())) {
                tradeOrderFacadeApi.markPaid(after.getUnifiedOrderId(), payRef, channel);
            }
            return;
        }

        List<MallOrderItem> mallLines = mallOrderItemService.findByOrderIds(Collections.singletonList(after.getId()));
        TradeOrderCreateDTO dto = buildMallTradeOrderCreateDto(after, pay, mallLines);
        String createdId = tradeOrderFacadeApi.createPendingPayOrder(dto);
        tradeOrderFacadeApi.markPaid(createdId, payRef, channel);
        after.setUnifiedOrderId(createdId);
        mallOrderService.updateOrder(after);
    }

    private TradeOrderCreateDTO buildMallBatchTradeOrderCreateDto(MallPayBatch batch, BigDecimal pay,
                                                                List<MallOrder> orders,
                                                                List<MallOrderItem> allLines) {
        TradeOrderCreateDTO dto = new TradeOrderCreateDTO();
        dto.setMemberId(StrUtil.blankToDefault(batch.getMemberId(), "-"));
        dto.setBizType(TradeBizTypeEnum.MALL.getValue());
        String title = "商城订单-" + StrUtil.blankToDefault(batch.getBatchNo(), batch.getId());
        dto.setTitle(title);
        dto.setTotalAmount(pay);
        dto.setDiscountAmount(batch.getDiscountAmount() == null ? BigDecimal.ZERO : batch.getDiscountAmount());
        dto.setPayAmount(pay);
        dto.setVerticalOrderId(batch.getId());
        dto.setTenantCode(PlatformTenantConstants.PLATFORM_TENANT_CODE);
        dto.setTenantId(PlatformTenantConstants.PLATFORM_TENANT_ID);
        List<String> mallOrderIds = orders == null ? Collections.emptyList()
                : orders.stream().map(MallOrder::getId).filter(StrUtil::isNotBlank).collect(Collectors.toList());
        dto.setExtJson(JSONUtil.createObj()
                .set("dualWriteSource", "MALL_M5")
                .set("payBatchId", batch.getId())
                .set("mallOrderIds", mallOrderIds)
                .toString());

        if (allLines != null && !allLines.isEmpty()) {
            List<TradeOrderLineCreateDTO> tradeLines = new ArrayList<>();
            int no = 1;
            for (MallOrderItem it : allLines) {
                TradeOrderLineCreateDTO line = new TradeOrderLineCreateDTO();
                line.setLineNo(no++);
                String itemTitle = StrUtil.blankToDefault(it.getSkuName(), "商品");
                if (StrUtil.isNotBlank(it.getSpecInfo())) {
                    itemTitle = itemTitle + " " + it.getSpecInfo().trim();
                }
                line.setItemTitle(itemTitle);
                line.setSkuRef(StrUtil.blankToDefault(it.getSkuId(), StrUtil.blankToDefault(it.getSpuId(), batch.getId())));
                line.setResourceRef(StrUtil.blankToDefault(it.getSpuId(), ""));
                int qty = it.getQuantity() == null || it.getQuantity() < 1 ? 1 : it.getQuantity();
                line.setQuantity(qty);
                BigDecimal unit = it.getPrice() != null ? it.getPrice() : BigDecimal.ZERO;
                line.setUnitPrice(unit);
                BigDecimal lineAmt = it.getTotalAmount();
                if (lineAmt == null && it.getPrice() != null) {
                    lineAmt = unit.multiply(BigDecimal.valueOf(qty)).setScale(2, RoundingMode.HALF_UP);
                }
                if (lineAmt == null) {
                    lineAmt = BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP);
                }
                line.setLineAmount(lineAmt);
                tradeLines.add(line);
            }
            dto.setLines(tradeLines);
        } else {
            TradeOrderLineCreateDTO line = new TradeOrderLineCreateDTO();
            line.setLineNo(1);
            line.setItemTitle(title);
            line.setSkuRef(batch.getId());
            line.setQuantity(1);
            line.setUnitPrice(pay);
            line.setLineAmount(pay);
            dto.setLines(Collections.singletonList(line));
        }
        return dto;
    }

    private TradeOrderCreateDTO buildMallTradeOrderCreateDto(MallOrder mall, BigDecimal pay, List<MallOrderItem> mallLines) {
        TradeOrderCreateDTO dto = new TradeOrderCreateDTO();
        dto.setMemberId(StrUtil.blankToDefault(mall.getMemberId(), "-"));
        dto.setBizType(TradeBizTypeEnum.MALL.getValue());
        String title = "商城订单-" + StrUtil.blankToDefault(mall.getOrderNo(), mall.getId());
        dto.setTitle(title);
        dto.setTotalAmount(pay);
        dto.setDiscountAmount(BigDecimal.ZERO);
        dto.setPayAmount(pay);
        dto.setVerticalOrderId(mall.getId());
        dto.setTenantCode(StrUtil.blankToDefault(mall.getTenantCode(), PlatformTenantConstants.PLATFORM_TENANT_CODE));
        dto.setTenantId(StrUtil.blankToDefault(mall.getTenantId(), PlatformTenantConstants.PLATFORM_TENANT_ID));
        dto.setExtJson("{\"dualWriteSource\":\"MALL_M5\"}");

        if (mallLines != null && !mallLines.isEmpty()) {
            List<TradeOrderLineCreateDTO> tradeLines = new ArrayList<>();
            int no = 1;
            for (MallOrderItem it : mallLines) {
                TradeOrderLineCreateDTO line = new TradeOrderLineCreateDTO();
                line.setLineNo(no++);
                String itemTitle = StrUtil.blankToDefault(it.getSkuName(), "商品");
                if (StrUtil.isNotBlank(it.getSpecInfo())) {
                    itemTitle = itemTitle + " " + it.getSpecInfo().trim();
                }
                line.setItemTitle(itemTitle);
                line.setSkuRef(StrUtil.blankToDefault(it.getSkuId(), StrUtil.blankToDefault(it.getSpuId(), mall.getId())));
                line.setResourceRef(StrUtil.blankToDefault(it.getSpuId(), ""));
                int qty = it.getQuantity() == null || it.getQuantity() < 1 ? 1 : it.getQuantity();
                line.setQuantity(qty);
                BigDecimal unit = it.getPrice() != null ? it.getPrice() : BigDecimal.ZERO;
                line.setUnitPrice(unit);
                BigDecimal lineAmt = it.getTotalAmount();
                if (lineAmt == null && it.getPrice() != null) {
                    lineAmt = unit.multiply(BigDecimal.valueOf(qty)).setScale(2, RoundingMode.HALF_UP);
                }
                if (lineAmt == null) {
                    lineAmt = BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP);
                }
                line.setLineAmount(lineAmt);
                tradeLines.add(line);
            }
            dto.setLines(tradeLines);
        } else {
            TradeOrderLineCreateDTO line = new TradeOrderLineCreateDTO();
            line.setLineNo(1);
            line.setItemTitle(title);
            line.setSkuRef(mall.getId());
            line.setQuantity(1);
            line.setUnitPrice(pay);
            line.setLineAmount(pay);
            dto.setLines(Collections.singletonList(line));
        }
        return dto;
    }

    private static BigDecimal resolvePayAmount(MallOrder mall) {
        if (mall.getPayAmount() != null) {
            return mall.getPayAmount();
        }
        return mall.getTotalAmount();
    }

    /**
     * 商城履约节点（发货/收货/核销）同步统一单状态，供小程序「我的订单」列表展示。
     */
    private void trySyncUnifiedFulfillmentForMallOrder(MallOrder order) {
        if (!isMallDualWriteEnabled() || order == null) {
            return;
        }
        String unifiedOrderId = resolveUnifiedOrderIdForSync(order);
        if (StrUtil.isBlank(unifiedOrderId)) {
            return;
        }
        List<MallOrder> scope = resolveMallOrdersForUnifiedSync(order);
        TradeOrderStatusEnum target = resolveTradeStatusFromMallOrders(scope);
        if (target == null) {
            return;
        }
        applyUnifiedTradeStatus(unifiedOrderId, target);
    }

    private String resolveUnifiedOrderIdForSync(MallOrder order) {
        if (StrUtil.isNotBlank(order.getUnifiedOrderId())) {
            return order.getUnifiedOrderId().trim();
        }
        if (StrUtil.isNotBlank(order.getPayBatchId())) {
            MallPayBatch batch = mallPayBatchService.get(order.getPayBatchId().trim());
            if (batch != null && StrUtil.isNotBlank(batch.getUnifiedOrderId())) {
                return batch.getUnifiedOrderId().trim();
            }
        }
        return null;
    }

    private List<MallOrder> resolveMallOrdersForUnifiedSync(MallOrder order) {
        if (StrUtil.isNotBlank(order.getPayBatchId())) {
            List<MallOrder> subs = mallOrderService.listByPayBatchId(order.getPayBatchId().trim());
            if (subs != null && !subs.isEmpty()) {
                return subs;
            }
        }
        return List.of(order);
    }

    /**
     * 合并支付批次按子单聚合目标统一单状态；单张子单则按自身状态映射。
     */
    private TradeOrderStatusEnum resolveTradeStatusFromMallOrders(List<MallOrder> orders) {
        if (orders == null || orders.isEmpty()) {
            return null;
        }
        boolean anyActive = false;
        boolean anyRefunding = false;
        boolean allTerminalDone = true;
        boolean anyFulfilling = false;
        boolean anyFulfilled = false;
        for (MallOrder o : orders) {
            if (o == null || o.getStatus() == null) {
                continue;
            }
            Integer s = o.getStatus();
            if (MallOrderStatusEnum.CANCELED.getCode().equals(s)
                    || MallOrderStatusEnum.CLOSED.getCode().equals(s)) {
                continue;
            }
            anyActive = true;
            if (MallOrderStatusEnum.REFUNDING.getCode().equals(s)) {
                anyRefunding = true;
            }
            if (MallOrderStatusEnum.FULFILLING.getCode().equals(s)) {
                anyFulfilling = true;
            }
            if (MallOrderStatusEnum.FULFILLED.getCode().equals(s)
                    || MallOrderStatusEnum.REFUNDED.getCode().equals(s)) {
                anyFulfilled = true;
            }
            if (!MallOrderStatusEnum.FULFILLED.getCode().equals(s)
                    && !MallOrderStatusEnum.REFUNDED.getCode().equals(s)) {
                allTerminalDone = false;
            }
        }
        if (!anyActive) {
            return null;
        }
        if (anyRefunding) {
            return TradeOrderStatusEnum.REFUNDING;
        }
        if (allTerminalDone) {
            return TradeOrderStatusEnum.FULFILLED;
        }
        if (anyFulfilling || anyFulfilled) {
            return TradeOrderStatusEnum.FULFILLING;
        }
        return TradeOrderStatusEnum.PAID;
    }

    private void applyUnifiedTradeStatus(String unifiedOrderId, TradeOrderStatusEnum target) {
        TradeOrder trade = tradeOrderFacadeApi.get(unifiedOrderId);
        if (trade == null || target == null) {
            return;
        }
        Integer current = trade.getStatus();
        if (target.getValue().equals(current)) {
            return;
        }
        switch (target) {
            case FULFILLING -> tradeOrderFacadeApi.markFulfilling(unifiedOrderId);
            case FULFILLED -> tradeOrderFacadeApi.markFulfilled(unifiedOrderId);
            case PAID -> {
                // 履约状态一般不回退，忽略
            }
            case REFUNDING -> {
                // 退款双写由售后链路处理，此处不推进
            }
            default -> log.debug("skip unified trade sync, unifiedOrderId={}, target={}", unifiedOrderId, target);
        }
    }

    private boolean isMallDualWriteEnabled() {
        return Boolean.parseBoolean(environment.getProperty(PROP_DUAL_WRITE, "false"));
    }

    /**
     * 补偿：对尚未关联统一单的商城订单补写 {@code biz_trade_order}（含 0 元历史单）。
     *
     * @return 成功补写条数
     */
    @Transactional(readOnly = false)
    public int backfillUnifiedTradeMissing() {
        if (!isMallDualWriteEnabled()) {
            throw new BizErrorException("商城统一单双写未启用（jeesharp.trade.dual-write.mall-enabled）");
        }
        List<MallOrder> list = mallOrderService.findList(new MallOrderQueryVO());
        int count = 0;
        if (list == null) {
            return 0;
        }
        for (MallOrder order : list) {
            if (order == null || StrUtil.isBlank(order.getId()) || StrUtil.isNotBlank(order.getUnifiedOrderId())) {
                continue;
            }
            List<MallOrderItem> lines = mallOrderItemService.findByOrderIds(Collections.singletonList(order.getId()));
            if (MallOrderStatusEnum.PAID.getCode().equals(order.getStatus())) {
                trySyncUnifiedPaidIfNeeded(order);
            } else {
                trySyncUnifiedPendingOnSubmit(order, lines);
                trySyncUnifiedMarkPaidIfZeroAmount(order);
            }
            count++;
        }
        return count;
    }

    /**
     * 补偿：对已发货/已完成且关联统一单的商城订单，回写统一单履约状态（历史数据一次性修复）。
     *
     * @return 尝试同步的商城子单条数
     */
    @Transactional(readOnly = false)
    public int backfillUnifiedFulfillmentSync() {
        if (!isMallDualWriteEnabled()) {
            throw new BizErrorException("商城统一单双写未启用（jeesharp.trade.dual-write.mall-enabled）");
        }
        MallOrderQueryVO q = new MallOrderQueryVO();
        q.setStatus(MallOrderStatusEnum.FULFILLING.getCode());
        List<MallOrder> fulfilling = mallOrderService.findList(q);
        MallOrderQueryVO q2 = new MallOrderQueryVO();
        q2.setStatus(MallOrderStatusEnum.FULFILLED.getCode());
        List<MallOrder> fulfilled = mallOrderService.findList(q2);
        Set<String> seenUnified = new HashSet<>();
        int count = 0;
        for (MallOrder order : fulfilling) {
            if (order == null) {
                continue;
            }
            String unifiedId = resolveUnifiedOrderIdForSync(order);
            if (StrUtil.isBlank(unifiedId) || !seenUnified.add(unifiedId)) {
                continue;
            }
            trySyncUnifiedFulfillmentForMallOrder(order);
            count++;
        }
        for (MallOrder order : fulfilled) {
            if (order == null) {
                continue;
            }
            String unifiedId = resolveUnifiedOrderIdForSync(order);
            if (StrUtil.isBlank(unifiedId) || !seenUnified.add(unifiedId)) {
                continue;
            }
            trySyncUnifiedFulfillmentForMallOrder(order);
            count++;
        }
        return count;
    }
}
