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

import com.jeesharp.business.modules.common.enums.VerticalPayStatusEnum;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
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.coupon.enums.CouponSceneCodeEnum;
import com.jeesharp.business.modules.food.convert.FoodBookingOrderConvert;
import com.jeesharp.business.modules.food.dto.FoodBookingCreateDTO;
import com.jeesharp.business.modules.food.dto.FoodBookingOrderSaveDTO;
import com.jeesharp.business.modules.finance.api.MerchantReceivableApi;
import com.jeesharp.business.modules.finance.api.PayRefundApi;
import com.jeesharp.business.modules.finance.api.ProfitSharingApi;
import com.jeesharp.business.modules.finance.payrefund.dto.PayRefundCreateDTO;
import com.jeesharp.business.modules.finance.paymentrecord.enums.PayRecordBizSceneEnum;
import com.jeesharp.business.modules.food.dto.FoodBookingRefundApplyDTO;
import com.jeesharp.business.modules.food.dto.FoodBookingRefundTriggerDTO;
import com.jeesharp.business.modules.food.dto.FoodBookingVerifyDTO;
import com.jeesharp.business.modules.food.dto.FoodCancelBookingDTO;
import com.jeesharp.business.modules.food.dto.FoodConfirmPayDTO;
import com.jeesharp.business.modules.food.dto.FoodPreparePayDTO;
import com.jeesharp.business.modules.food.entity.FoodBookingOrder;
import com.jeesharp.business.modules.food.entity.FoodPackage;
import com.jeesharp.business.modules.food.enums.FoodBookingStatusEnum;

import com.jeesharp.business.modules.food.application.support.FoodBookingCreateAssembler;
import com.jeesharp.business.modules.food.application.support.FoodBookingPayAssembler;
import com.jeesharp.business.modules.common.support.QueryInCsvSupport;
import com.jeesharp.business.modules.food.service.FoodBookingOrderService;
import com.jeesharp.business.modules.food.service.FoodPackageService;
import com.jeesharp.business.modules.food.service.FoodRestaurantService;
import com.jeesharp.business.modules.member.service.MemberService;
import com.jeesharp.business.modules.food.vo.FoodBookingOrderInfoVO;
import com.jeesharp.business.modules.food.vo.FoodBookingOrderListVO;
import com.jeesharp.business.modules.food.vo.FoodBookingOrderQueryVO;
import com.jeesharp.business.modules.food.vo.FoodBookingResultVO;
import com.jeesharp.business.modules.food.vo.FoodPayPrepareVO;
import com.jeesharp.business.modules.food.vo.FoodPayStatusVO;
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.enums.TradeBizTypeEnum;
import com.jeesharp.business.modules.trade.enums.TradeOrderStatusEnum;
import com.jeesharp.business.modules.trade.vo.TradeOrderInfoVO;
import com.jeesharp.business.modules.trade.vo.TradeOrderQueryVO;
import com.jeesharp.framework.pojo.Page;
import com.jeesharp.framework.pojo.query.PageRespVO;
import cn.hutool.json.JSONUtil;
import com.jeesharp.framework.exception.BizErrorException;
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.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;

/**
 * 美食团购预约应用服务（C 端下单 / 支付 / 取消编排）。
 *
 * <p><b>主流程</b></p>
 * <pre>
 * createBooking → app_food_booking_order + 扣套餐库存 → 冻券(FOOD 场景) → 双写统一单（预约即确认）
 * preparePay    → 按冻券调减应付金额
 * confirmPaySuccess → 核销券 → markPaid 统一单
 * cancelBooking → 释放冻券 + markCancelled
 * verifyConsume → 到店核销 → markClosed 统一单 → scheduleSharingByBizOrder（场景 C）
 * refund        → PayRefundApi.createAndExecute → 微信 SUCCESS 后 FoodPayRefundSuccessHandler 关单退券
 * </pre>
 *
 * <p>另见 {@link FoodPersonalMealApplicationService}：个人点餐（独立支付回调 FoodMealWechatPayCallbackAdapter）。</p>
 *
 * @author jeesharp
 * @since 2026-05-08
 */
@Slf4j
@Service
@AllArgsConstructor
@Transactional(readOnly = true)
public class FoodBookingOrderApplicationService {

    private static final String PROP_DUAL_WRITE = "jeesharp.trade.dual-write.food-enabled";
    private static final String PROP_READ_FOOD_UNIFIED = "jeesharp.trade.read.food-from-unified";
    private static final String DEFAULT_MEMBER_ID = "-";
    private static final String BOOKING_ORDER_CREATE_FAILED_MESSAGE = "创建预约单失败，请稍后重试";
    private static final String DEFAULT_PAY_CHANNEL = "wechat_mini";
    private static final int PAY_EXPIRE_MINUTES = 30;
    private static final String BOOKING_ORDER_NOT_FOUND_MESSAGE = "预订单不存在";
    private static final DateTimeFormatter DT_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");

    private final FoodBookingOrderService foodBookingOrderService;
    private final FoodBookingCreateAssembler foodBookingCreateAssembler;
    private final FoodBookingPayAssembler foodBookingPayAssembler;
    private final FoodPackageService foodPackageService;
    private final FoodRestaurantService foodRestaurantService;
    private final MemberService memberService;
    private final CouponCheckoutApi couponCheckoutApi;
    private final Environment environment;
    private final TradeOrderFacadeApi tradeOrderFacadeApi;
    private final ProfitSharingApi profitSharingApi;
    private final MerchantReceivableApi merchantReceivableApi;
    private final PayRefundApi payRefundApi;

    public FoodBookingOrderInfoVO get(String id) {
        FoodBookingOrder entity = foodBookingOrderService.get(id);
        if (entity == null) {
            return null;
        }
        FoodBookingOrderInfoVO vo = FoodBookingOrderConvert.INSTANCE.toInfoVO(entity);
        enrichInfoDisplay(vo);
        return vo;
    }

    public PageRespVO<FoodBookingOrderListVO> findPage(Page<FoodBookingOrder> page, FoodBookingOrderQueryVO query) {
        FoodBookingOrderQueryVO prepared = prepareQuery(query);
        if (prepared == null) {
            return emptyBookingPage(page);
        }
        Page<FoodBookingOrder> result = foodBookingOrderService.findPage(page, prepared);
        PageRespVO<FoodBookingOrderListVO> pageVo = FoodBookingOrderConvert.INSTANCE.toPageRespVO(result);
        enrichListDisplay(pageVo.getRecords());
        return pageVo;
    }

    @Transactional(readOnly = false)
    public void save(FoodBookingOrderSaveDTO dto) {
        FoodBookingOrder entity = FoodBookingOrderConvert.INSTANCE.toEntity(dto);
        if (StrUtil.isBlank(dto.getId())) {
            foodBookingOrderService.createOrder(entity);
            return;
        }
        foodBookingOrderService.updateOrder(entity);
    }

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

    /**
     * 小程序创建美食预订（应用层编排）。
     * <p>顺序：领域层落垂直单 {@code app_food_booking_order} → 组装结果 VO →
     * 可选双写全站统一单 {@code biz_trade_order} → 刷新结果返回。</p>
     * <p>双写失败仅打日志，不影响垂直单已提交。</p>
     */
    @Transactional(readOnly = false)
    public FoodBookingResultVO createBooking(FoodBookingCreateDTO dto) {
        // ① 校验门店/套餐、扣库存并组装垂直单，再由领域层 INSERT
        FoodBookingOrder order = foodBookingOrderService.insertBooking(
                foodBookingCreateAssembler.buildBookingOrder(dto));
        if (order == null || StrUtil.isBlank(order.getId())) {
            throw new BizErrorException(BOOKING_ORDER_CREATE_FAILED_MESSAGE);
        }
        String bookingOrderId = order.getId().trim();
        // ② 从垂直单快照 JSON 或主数据重建成功页 VO
        FoodBookingResultVO vo = foodBookingOrderService.getBookingResult(bookingOrderId);
        if (vo == null) {
            log.error("Food booking result missing after create, bookingOrderId={}", bookingOrderId);
            throw new BizErrorException(BOOKING_ORDER_CREATE_FAILED_MESSAGE);
        }
        applyCouponOnCreate(order, dto, vo);
        // ③ M5 统一单双写（jeesharp.trade.dual-write.food-enabled=true 时）
        if (isFoodDualWriteEnabled()) {
            if (DEFAULT_MEMBER_ID.equals(order.getMemberId())) {
                log.warn("Food trade dual-write skipped: memberId is placeholder, bookingOrderId={}", bookingOrderId);
            } else {
                try {
                    syncUnifiedTradeOnReservationConfirmed(order, dto, vo);
                    if (StrUtil.isNotBlank(order.getUnifiedOrderId())) {
                        log.info("Food trade dual-write ok, bookingOrderId={}, unifiedOrderId={}",
                                bookingOrderId, order.getUnifiedOrderId());
                    } else {
                        log.warn("Food trade dual-write finished without unified_order_id, bookingOrderId={}",
                                bookingOrderId);
                    }
                } catch (Exception ex) {
                    log.warn("Food trade dual-write failed, bookingOrderId={}", bookingOrderId, ex);
                }
            }
        }
        // ④ 双写可能更新了 unified_order_id 与 result_payload_json，再读一次返回最新结果
        FoodBookingResultVO refreshed = foodBookingOrderService.getBookingResult(bookingOrderId);
        FoodBookingResultVO result = refreshed != null ? refreshed : vo;
        result.setOrderId(bookingOrderId);
        if (result.getAmount() != null && result.getAmount().compareTo(BigDecimal.ZERO) <= 0) {
            FoodConfirmPayDTO confirm = new FoodConfirmPayDTO();
            confirm.setOrderId(bookingOrderId);
            return confirmPaySuccess(confirm);
        }
        return result;
    }

    @Transactional(readOnly = false)
    public FoodPayPrepareVO preparePay(FoodPreparePayDTO dto) {
        resolveVerticalOrderId(dto);
        FoodBookingOrder order = foodBookingOrderService.findByOrderRef(dto.getOrderId());
        if (order == null) {
            throw new BizErrorException(BOOKING_ORDER_NOT_FOUND_MESSAGE);
        }
        foodBookingOrderService.assertPayPreparable(order);
        String payChannel = StrUtil.blankToDefault(dto.getPayChannel(), DEFAULT_PAY_CHANNEL);
        foodBookingOrderService.markPayPreparing(
                order,
                payChannel,
                FoodBookingPayAssembler.newPayOrderId(),
                java.time.LocalDateTime.now().plusMinutes(PAY_EXPIRE_MINUTES));
        FoodPayPrepareVO vo = foodBookingPayAssembler.buildPrepareVo(order, payChannel);
        adjustPayAmountByFrozenCoupon(vo);
        return vo;
    }

    public FoodPayStatusVO getPayStatus(String orderId) {
        FoodBookingOrder order = resolveBookingOrder(orderId);
        if (order == null) {
            return null;
        }
        return foodBookingPayAssembler.buildPayStatusVo(order);
    }

    /**
     * 支付成功确认（微信回调 / mock-success 入口）：领域层 markPaySuccess → 核销冻券 → 同步统一单 markPaid → 触发结算。
     */
    @Transactional(readOnly = false)
    public FoodBookingResultVO confirmPaySuccess(FoodConfirmPayDTO dto) {
        resolveVerticalOrderId(dto);
        FoodBookingOrder order = foodBookingOrderService.findByOrderRef(dto.getOrderId());
        if (order == null) {
            throw new BizErrorException(BOOKING_ORDER_NOT_FOUND_MESSAGE);
        }
        // ① 领域层：团购预约单 markPaySuccess（幂等）
        FoodBookingResultVO vo = foodBookingOrderService.markPaySuccess(
                order, dto.getPayOrderId(), dto.getThirdPartyTradeNo());
        if (vo != null && dto != null && StrUtil.isNotBlank(dto.getOrderId())) {
            // ② 核销冻券
            couponCheckoutApi.confirmOrderPaid(dto.getOrderId().trim());
            // ③ 同步统一单
            if (isFoodDualWriteEnabled()) {
                FoodBookingOrder paidOrder = foodBookingOrderService.get(dto.getOrderId().trim());
                trySyncUnifiedMarkPaidIfPending(paidOrder, vo, "FOOD-");
            }
            // ④ 触发结算
            triggerFinanceSettleOnPaySuccess(dto);
        }
        return vo;
    }

    @Transactional(readOnly = false)
    public void cancelBooking(FoodCancelBookingDTO dto) {
        resolveVerticalOrderId(dto);
        FoodBookingOrder order = dto != null && StrUtil.isNotBlank(dto.getOrderId())
                ? resolveBookingOrder(dto.getOrderId()) : null;
        if (order == null) {
            throw new BizErrorException(BOOKING_ORDER_NOT_FOUND_MESSAGE);
        }
        foodBookingOrderService.markCancelled(order, dto != null ? dto.getCancelReason() : null);
        if (order != null && StrUtil.isNotBlank(order.getUnifiedOrderId())) {
            tradeOrderFacadeApi.markCancelled(order.getUnifiedOrderId());
        }
        if (dto != null && StrUtil.isNotBlank(dto.getOrderId())) {
            couponCheckoutApi.releaseFrozen(dto.getOrderId().trim(), "美食团购已取消");
        }
    }

    /**
     * 补偿：对尚未关联统一单的美食预约单补写 {@code biz_trade_order}（含 0 元历史单）。
     *
     * @return 成功补写条数
     */
    @Transactional(readOnly = false)
    public int backfillUnifiedTradeMissing() {
        if (!isFoodDualWriteEnabled()) {
            throw new BizErrorException("美食统一单双写未启用（jeesharp.trade.dual-write.food-enabled）");
        }
        List<FoodBookingOrder> list = foodBookingOrderService.findList(new FoodBookingOrderQueryVO());
        int count = 0;
        if (list == null) {
            return 0;
        }
        for (FoodBookingOrder order : list) {
            if (order == null || StrUtil.isBlank(order.getId()) || StrUtil.isNotBlank(order.getUnifiedOrderId())) {
                continue;
            }
            if (DEFAULT_MEMBER_ID.equals(order.getMemberId())) {
                continue;
            }
            FoodBookingResultVO vo = foodBookingOrderService.getBookingResult(order.getId());
            syncUnifiedTradeOnReservationConfirmed(order, null, vo);
            count++;
        }
        log.info("Food M5 backfill done: count={}", count);
        return count;
    }

    /**
     * 小程序「我的美食预订」分页。
     */
    public PageRespVO<FoodBookingResultVO> findMyBookingsForMobile(String memberId, FoodBookingOrderQueryVO query) {
        if (StrUtil.isBlank(memberId)) {
            return emptyPageResp(query);
        }
        if (query == null) {
            query = new FoodBookingOrderQueryVO();
        }
        if (readFoodFromUnified()) {
            return findMyBookingsFromTrade(memberId, query);
        }
        query.setMemberId(memberId);
        Page<FoodBookingOrder> page = new Page<>(query);
        Page<FoodBookingOrder> result = foodBookingOrderService.findPage(page, query);
        PageRespVO<FoodBookingResultVO> resp = new PageRespVO<>();
        resp.setTotal(result.getTotal());
        resp.setPageNo(result.getPageNo());
        resp.setPageSize(result.getPageSize());
        List<FoodBookingResultVO> rows = new ArrayList<>();
        for (FoodBookingOrder e : result.getRecords()) {
            FoodBookingResultVO vo = foodBookingOrderService.getBookingResult(e.getId());
            if (vo != null) {
                if (StrUtil.isNotBlank(e.getUnifiedOrderId())) {
                    vo.setUnifiedOrderId(e.getUnifiedOrderId());
                }
                rows.add(vo);
            }
        }
        resp.setRecords(rows);
        return resp;
    }

    /**
     * 小程序「我的美食预订」详情（按垂直单 id）。
     * <p>校验会员归属；若开启 {@link #readFoodFromUnified()} 且已双写，会用统一单金额/状态覆盖展示字段。</p>
     *
     * @param orderId  垂直单 {@code app_food_booking_order.id}
     * @param memberId 当前登录会员 id
     * @return 结果 VO；无权限或不存在时 {@code null}
     */
    public FoodBookingResultVO getBookingResultForMobile(String orderId, String memberId) {
        FoodBookingOrder fo = foodBookingOrderService.get(orderId);
        if (fo == null) {
            return null;
        }
        if (StrUtil.isNotBlank(memberId)
            && StrUtil.isNotBlank(fo.getMemberId())
            && !DEFAULT_MEMBER_ID.equals(fo.getMemberId())
            && !memberId.equals(fo.getMemberId())) {
            return null;
        }
        FoodBookingResultVO vo = foodBookingOrderService.getBookingResult(orderId);
        if (vo == null) {
            return null;
        }
        if (StrUtil.isNotBlank(fo.getUnifiedOrderId())) {
            vo.setUnifiedOrderId(fo.getUnifiedOrderId());
        }
        if (readFoodFromUnified() && StrUtil.isNotBlank(fo.getUnifiedOrderId())) {
            TradeOrder trade = tradeOrderFacadeApi.get(fo.getUnifiedOrderId());
            if (trade != null && TradeBizTypeEnum.FOOD.getValue().equals(trade.getBizType())) {
                if (trade.getPayAmount() != null) {
                    vo.setAmount(trade.getPayAmount());
                    vo.setAmountLabel(formatAmountLabel(trade.getPayAmount()));
                }
                vo.setStatusLabel(TradeOrderStatusEnum.getDescByValue(trade.getStatus()));
                if (trade.getCreateDate() != null) {
                    vo.setCreatedAt(trade.getCreateDate().format(DT_FMT));
                }
            }
        }
        return vo;
    }

    /**
     * 按垂直美食单主键或统一交易单主键查询预订结果（须为本人订单）。
     *
     * @param idOrUnifiedId 垂直单 {@code app_food_booking_order.id} 或 {@code biz_trade_order.id}
     * @param memberId      当前会员主键
     * @return 结果 VO，无权限或不存在时返回 {@code null}
     */
    public FoodBookingResultVO getBookingResultForMobileByAnyId(String idOrUnifiedId, String memberId) {
        if (StrUtil.isBlank(idOrUnifiedId)) {
            return null;
        }
        FoodBookingResultVO direct = getBookingResultForMobile(idOrUnifiedId, memberId);
        if (direct != null) {
            return direct;
        }
        FoodBookingOrderQueryVO q = new FoodBookingOrderQueryVO();
        q.setUnifiedOrderId(idOrUnifiedId);
        List<FoodBookingOrder> list = foodBookingOrderService.findList(q);
        if (list == null || list.isEmpty()) {
            return null;
        }
        FoodBookingOrder matched = list.get(0);
        if (!canMemberAccessFoodBooking(matched, memberId)) {
            return null;
        }
        return getBookingResultForMobile(matched.getId(), memberId);
    }

    /**
     * 小程序结果页/分享链接：占位会员「-」或未登录时不校验归属；否则须为本人订单。
     */
    private boolean canMemberAccessFoodBooking(FoodBookingOrder order, String memberId) {
        if (order == null) {
            return false;
        }
        if (StrUtil.isBlank(memberId)) {
            return true;
        }
        if (StrUtil.isBlank(order.getMemberId()) || DEFAULT_MEMBER_ID.equals(order.getMemberId())) {
            return true;
        }
        return memberId.equals(order.getMemberId());
    }

    /**
     * 管理端到店核销：凭核销码将「待使用」美食预订单置为已使用，并同步关闭统一单。
     * <p>垂直单 → {@code used}；若存在 {@code unified_order_id}，调用 {@code markClosed}。</p>
     */
    @Transactional(readOnly = false)
    public void verifyConsumeByVerifyCode(FoodBookingVerifyDTO dto) {
        FoodBookingOrder order = foodBookingOrderService.getByVerifyCode(dto.getVerifyCode());
        foodBookingOrderService.verifyConsumeByVerifyCode(dto.getVerifyCode());
        if (order != null && StrUtil.isNotBlank(order.getUnifiedOrderId())) {
            tradeOrderFacadeApi.markClosed(order.getUnifiedOrderId());
        }
        if (order != null && StrUtil.isNotBlank(order.getId())) {
            triggerProfitSharingOnVerify(order.getId());
        }
    }

    /**
     * C 端申请微信退款（未核销前；美食预订，非团餐）。
     *
     * @return 支付退款单主键
     */
    @Transactional(readOnly = false)
    public String applyBookingRefund(FoodBookingRefundApplyDTO dto, String memberId) {
        if (dto == null || StrUtil.isBlank(dto.getOrderId())) {
            throw new BizErrorException("预订单ID不能为空");
        }
        resolveVerticalOrderId(dto);
        FoodBookingOrder order = foodBookingOrderService.get(dto.getOrderId().trim());
        if (order == null) {
            throw new BizErrorException(BOOKING_ORDER_NOT_FOUND_MESSAGE);
        }
        assertMemberOwns(order.getMemberId(), memberId);
        assertRefundable(order);
        return executeBookingRefund(
                order,
                null,
                StrUtil.blankToDefault(dto.getRefundReason(), "用户申请退款"),
                null);
    }

    /**
     * 管理端发起微信退款（财务统一 Facade）。
     *
     * @return 支付退款单主键
     */
    @Transactional(readOnly = false)
    public String triggerBookingRefund(FoodBookingRefundTriggerDTO dto) {
        if (dto == null || StrUtil.isBlank(dto.getOrderId())) {
            throw new BizErrorException("预订单ID不能为空");
        }
        resolveVerticalOrderId(dto);
        FoodBookingOrder order = foodBookingOrderService.get(dto.getOrderId().trim());
        if (order == null) {
            throw new BizErrorException(BOOKING_ORDER_NOT_FOUND_MESSAGE);
        }
        assertRefundable(order);
        BigDecimal refundAmount = dto.getRefundAmount();
        if (refundAmount == null || refundAmount.compareTo(BigDecimal.ZERO) <= 0) {
            refundAmount = resolveRefundableAmount(order);
        }
        return executeBookingRefund(
                order,
                refundAmount,
                StrUtil.blankToDefault(dto.getRefundReason(), "美食团购退款"),
                dto.getOperatorId());
    }

    private String executeBookingRefund(
            FoodBookingOrder order, BigDecimal refundAmount, String reason, String operatorId) {
        if (refundAmount == null || refundAmount.compareTo(BigDecimal.ZERO) <= 0) {
            refundAmount = resolveRefundableAmount(order);
        }
        PayRefundCreateDTO refundDto = new PayRefundCreateDTO();
        refundDto.setBizScene(PayRecordBizSceneEnum.FOOD_ORDER.getCode());
        refundDto.setBizOrderId(order.getId());
        refundDto.setRefundAmount(refundAmount);
        refundDto.setReason(reason);
        refundDto.setOperatorId(operatorId);
        return payRefundApi.createAndExecute(refundDto);
    }

    public FoodBookingResultVO getBookingResult(String orderId) {
        return foodBookingOrderService.getBookingResult(orderId);
    }

    /**
     * 从统一单分页组装「我的美食预订」列表（{@code jeesharp.trade.read.food-from-unified=true} 时使用）。
     * <p>数据源：{@code biz_trade_order}（{@code biz_type=FOOD}），每行再尽量反查垂直单快照。</p>
     */
    private PageRespVO<FoodBookingResultVO> findMyBookingsFromTrade(String memberId, FoodBookingOrderQueryVO query) {
        TradeOrderQueryVO tq = new TradeOrderQueryVO();
        tq.setMemberId(memberId);
        tq.setBizType(TradeBizTypeEnum.FOOD.getValue());
        tq.setPageNo(query.getPageNo());
        tq.setPageSize(query.getPageSize());
        Page<TradeOrder> page = new Page<>(tq);
        Page<TradeOrder> result = tradeOrderFacadeApi.findPage(page, tq);
        PageRespVO<FoodBookingResultVO> resp = new PageRespVO<>();
        resp.setTotal(result.getTotal());
        resp.setPageNo(result.getPageNo());
        resp.setPageSize(result.getPageSize());
        List<FoodBookingResultVO> rows = new ArrayList<>();
        for (TradeOrder tr : result.getRecords()) {
            rows.add(buildMobileRowFromTrade(tr));
        }
        resp.setRecords(rows);
        return resp;
    }

    /**
     * 将一条统一单记录转为小程序列表/详情行 VO。
     * <p>优先用 {@code vertical_order_id} 反查垂直单 {@code result_payload_json}；查不到则用统一单字段拼最小展示。</p>
     */
    private FoodBookingResultVO buildMobileRowFromTrade(TradeOrder tr) {
        String verticalId = tr.getVerticalOrderId();
        FoodBookingResultVO vo = null;
        if (StrUtil.isNotBlank(verticalId)) {
            vo = foodBookingOrderService.getBookingResult(verticalId);
        }
        if (vo == null) {
            vo = new FoodBookingResultVO();
            vo.setOrderId(StrUtil.blankToDefault(verticalId, ""));
            vo.setTitle(StrUtil.blankToDefault(tr.getTitle(), "美食预订"));
            vo.setVerifyCode("");
            if (TradeOrderStatusEnum.PAID.getValue().equals(tr.getStatus())) {
                vo.setStatus(FoodBookingStatusEnum.PENDING_USE.getValue());
            } else {
                vo.setStatus(FoodBookingStatusEnum.CREATED.getValue());
            }
            if (tr.getPayAmount() != null) {
                vo.setAmount(tr.getPayAmount());
                vo.setAmountLabel(formatAmountLabel(tr.getPayAmount()));
            }
            vo.setStatusLabel(TradeOrderStatusEnum.getDescByValue(tr.getStatus()));
            if (tr.getCreateDate() != null) {
                vo.setCreatedAt(tr.getCreateDate().format(DT_FMT));
            }
            vo.setResultPayloadJson("{}");
        }
        vo.setUnifiedOrderId(tr.getId());
        if (tr.getPayAmount() != null) {
            vo.setAmount(tr.getPayAmount());
            vo.setAmountLabel(formatAmountLabel(tr.getPayAmount()));
        }
        vo.setStatusLabel(TradeOrderStatusEnum.getDescByValue(tr.getStatus()));
        if (tr.getCreateDate() != null) {
            vo.setCreatedAt(tr.getCreateDate().format(DT_FMT));
        }
        return vo;
    }

    /** 会员 id 为空时返回的空分页结构（避免 NPE）。 */
    private static PageRespVO<FoodBookingResultVO> emptyPageResp(FoodBookingOrderQueryVO query) {
        PageRespVO<FoodBookingResultVO> resp = new PageRespVO<>();
        resp.setTotal(0L);
        if (query != null && query.getPageNo() != null) {
            resp.setPageNo(query.getPageNo());
        } else {
            resp.setPageNo(1L);
        }
        if (query != null && query.getPageSize() != null) {
            resp.setPageSize(query.getPageSize());
        } else {
            resp.setPageSize(10L);
        }
        resp.setRecords(List.of());
        return resp;
    }

    /**
     * 是否从统一单 {@code biz_trade_order} 读取美食「我的预订」列表。
     * <p>配置项：{@code jeesharp.trade.read.food-from-unified}，默认 {@code false}（读垂直单）。</p>
     */
    private boolean readFoodFromUnified() {
        return Boolean.parseBoolean(environment.getProperty(PROP_READ_FOOD_UNIFIED, "false"));
    }

    /**
     * 是否在创建美食预订时双写全站统一交易单。
     * <p>配置项：{@code jeesharp.trade.dual-write.food-enabled}，默认 {@code false}。</p>
     */
    private boolean isFoodDualWriteEnabled() {
        return Boolean.parseBoolean(environment.getProperty(PROP_DUAL_WRITE, "false"));
    }

    // ==================== M5 统一单双写（预约确认即入账） ====================

    /**
     * 美食预约确认后的统一单双写入口（由 {@link #createBooking} 在垂直单落库后调用）。
     * <p><b>流程（同一次请求、同一事务内顺序执行，整段只 INSERT 一次统一单）：</b></p>
     * <ol>
     *   <li>{@link #trySyncUnifiedPendingOnSubmit} — 幂等查重或 INSERT 待支付统一单，回写 {@code unified_order_id}</li>
     *   <li>{@link #trySyncUnifiedMarkPaidIfPending} — 仅 markPaid，<b>禁止</b>再次 INSERT（避免 {@code uk_order_no_tenant} 冲突）</li>
     * </ol>
     * <p>美食为预约型业务，无独立在线支付，故建单后直接标记已支付（{@code payChannel=RESERVE}）。</p>
     *
     * @param order 已落库的垂直单实体 {@code app_food_booking_order}
     * @param dto   创建请求（第一步解析金额时可能用到套餐信息）
     * @param outVo 成功页结果 VO，双写过程中会更新 {@code unifiedOrderId} 等字段
     */
    private void syncUnifiedTradeOnReservationConfirmed(FoodBookingOrder order, FoodBookingCreateDTO dto,
                                                        FoodBookingResultVO outVo) {
        trySyncUnifiedPendingOnSubmit(order, dto, outVo);
    }

    /**
     * 双写第一步：确保存在待支付统一单，并回写垂直单 {@code unified_order_id}。
     * <p><b>前置条件：</b>垂直单已有主键、尚未关联统一单（含 0 元）。</p>
     * <p><b>幂等：</b>先 {@link #findExistingFoodTradeOrder} 按 {@code vertical_order_id} 查是否已双写，
     * 已存在则只 {@link #applyUnifiedOrderLink}，不再 INSERT。</p>
     * <p><b>新建时：</b>{@code order_no} 形态为 {@code UTOFOOD-{垂直单id}}（见 TradeOrderServiceImpl）。</p>
     *
     * @param order 垂直单（会被写入 {@code unifiedOrderId}）
     * @param dto   创建 DTO，{@link #resolveFoodPayAmount} 在 VO 无金额时可兜底取套餐价
     * @param outVo 结果 VO，同步更新 {@code unifiedOrderId} 与 {@code result_payload_json}
     */
    private void trySyncUnifiedPendingOnSubmit(FoodBookingOrder order, FoodBookingCreateDTO dto,
                                               FoodBookingResultVO outVo) {
        // 无垂直单主键或已关联统一单 → 无需建单
        if (order == null || StrUtil.isBlank(order.getId()) || StrUtil.isNotBlank(order.getUnifiedOrderId())) {
            return;
        }
        BigDecimal total = resolveFoodPayAmount(order, dto, outVo);
        // 幂等：同事务内上一步可能已 INSERT 但内存尚未写 unifiedOrderId
        TradeOrder existing = findExistingFoodTradeOrder(order);
        if (existing != null) {
            applyUnifiedOrderLink(order, outVo, existing.getId());
            return;
        }
        // 真正 INSERT biz_trade_order + 明细行，状态=待支付
        TradeOrderCreateDTO createDto = buildFoodTradeOrderCreateDto(order, dto, outVo, total);
        String createdId = tradeOrderFacadeApi.createPendingPayOrder(createDto);
        if (StrUtil.isBlank(createdId)) {
            log.warn("Food trade pending create returned empty id, bookingOrderId={}", order.getId());
            return;
        }
        applyUnifiedOrderLink(order, outVo, createdId);
    }

    /**
     * 双写第二步：将待支付统一单标记为已支付（预约型直接入账）。
     * <p><b>重要：</b>本方法<b>不再</b>调用 {@code createPendingPayOrder}，避免与第一步重复 INSERT 相同 {@code order_no}。</p>
     * <p>若内存中尚无 {@code unifiedOrderId}，先 {@link #ensureUnifiedOrderLinked} 从库中按垂直单主键找回关联。</p>
     *
     * @param order         垂直单
     * @param outVo         结果 VO
     * @param payRefPrefix  支付参考号前缀，美食固定传 {@code "FOOD-"}，完整为 {@code FOOD-{垂直单id}}
     */
    private void trySyncUnifiedMarkPaidIfPending(FoodBookingOrder order, FoodBookingResultVO outVo, String payRefPrefix) {
        if (order == null) {
            return;
        }
        BigDecimal total = resolveFoodPayAmount(order, null, outVo);
        String payRef = payRefPrefix + order.getId();
        // 兜底：第一步 INSERT 成功但 unifiedOrderId 未写回内存时，从库关联
        ensureUnifiedOrderLinked(order, outVo);
        if (StrUtil.isBlank(order.getUnifiedOrderId())) {
            log.warn("Food trade markPaid skipped: unified order missing after pending sync, bookingOrderId={}",
                    order.getId());
            return;
        }
        TradeOrder existing = tradeOrderFacadeApi.get(order.getUnifiedOrderId());
        String channel = StrUtil.blankToDefault(order.getPayChannel(), "RESERVE");
        if (existing != null
            && TradeOrderStatusEnum.PENDING_PAY.getValue().equals(existing.getStatus())) {
            tradeOrderFacadeApi.markPaid(order.getUnifiedOrderId(), payRef, channel);
        }
        if (outVo != null) {
            outVo.setUnifiedOrderId(order.getUnifiedOrderId());
        }
    }

    /**
     * 在 markPaid 前兜底：垂直单内存/库中尚无 {@code unified_order_id} 时，按垂直单主键查已有统一单并回写。
     * <p>典型场景：{@link #trySyncUnifiedPendingOnSubmit} 已 INSERT 成功，但 {@code order.unifiedOrderId} 尚未赋值。</p>
     *
     * @param order 垂直单（查到时写入 {@code unifiedOrderId} 并 UPDATE 垂直单）
     * @param outVo 结果 VO，可选同步
     */
    private void ensureUnifiedOrderLinked(FoodBookingOrder order, FoodBookingResultVO outVo) {
        if (order == null || StrUtil.isNotBlank(order.getUnifiedOrderId())) {
            return;
        }
        TradeOrder existing = findExistingFoodTradeOrder(order);
        if (existing != null) {
            applyUnifiedOrderLink(order, outVo, existing.getId());
        }
    }

    /**
     * 将统一单主键关联到垂直单，并刷新成功页快照 JSON。
     * <p>写入：{@code app_food_booking_order.unified_order_id}、{@code result_payload_json}。</p>
     *
     * @param order          垂直单实体（内存字段会被更新）
     * @param outVo          成功页 VO；非 null 时同步 {@code unifiedOrderId} 并刷新 payload
     * @param unifiedOrderId {@code biz_trade_order.id}
     */
    private void applyUnifiedOrderLink(FoodBookingOrder order, FoodBookingResultVO outVo, String unifiedOrderId) {
        order.setUnifiedOrderId(unifiedOrderId);
        foodBookingOrderService.updateOrder(order);
        if (outVo != null) {
            outVo.setUnifiedOrderId(unifiedOrderId);
            refreshFoodResultPayloadJson(order.getId(), outVo);
        }
    }

    /**
     * 按垂直单查已双写的统一单（幂等查重用）。
     * <p>条件：{@code vertical_order_id = 垂直单id} 且 {@code biz_type = FOOD} 且 {@code member_id} 一致。</p>
     *
     * @param order 美食垂直单
     * @return 已存在的统一单；无则 {@code null}
     */
    private TradeOrder findExistingFoodTradeOrder(FoodBookingOrder order) {
        if (order == null || StrUtil.isBlank(order.getId())) {
            return null;
        }
        TradeOrderQueryVO query = new TradeOrderQueryVO();
        query.setVerticalOrderId(order.getId());
        query.setBizType(TradeBizTypeEnum.FOOD.getValue());
        query.setMemberId(StrUtil.blankToDefault(order.getMemberId(), DEFAULT_MEMBER_ID));
        query.setPageNo(1L);
        query.setPageSize(1L);
        Page<TradeOrder> page = tradeOrderFacadeApi.findPage(new Page<>(1L, 1L), query);
        if (page.getRecords() == null || page.getRecords().isEmpty()) {
            return null;
        }
        return page.getRecords().get(0);
    }

    /**
     * 解析美食预订应付金额（用于判断是否需双写、以及统一单金额字段）。
     * <p>优先级：结果 VO {@code amount} → 垂直单套餐单价×份数 → 创建 DTO 套餐单价×份数 → {@code 0}。</p>
     *
     * @param order 垂直单（含 {@code packageId}、{@code quantity}）
     * @param dto   创建 DTO；第二步 markPaid 时传 {@code null}
     * @param outVo 成功页 VO
     * @return 应付总额（含 0 元）
     */
    private BigDecimal resolveFoodPayAmount(FoodBookingOrder order, FoodBookingCreateDTO dto,
                                                   FoodBookingResultVO outVo) {
        if (outVo != null && outVo.getAmount() != null) {
            return outVo.getAmount();
        }
        if (order != null && StrUtil.isNotBlank(order.getPackageId()) && order.getQuantity() != null) {
            FoodPackage pkg = foodPackageService.get(order.getPackageId());
            if (pkg != null && pkg.getPrice() != null) {
                return pkg.getPrice().multiply(BigDecimal.valueOf(order.getQuantity()));
            }
        }
        if (dto != null && StrUtil.isNotBlank(dto.getPackageId()) && dto.getQuantity() != null) {
            FoodPackage pkg = foodPackageService.get(dto.getPackageId());
            if (pkg != null && pkg.getPrice() != null) {
                return pkg.getPrice().multiply(BigDecimal.valueOf(dto.getQuantity()));
            }
        }
        return BigDecimal.ZERO;
    }

    /**
     * 组装写入 {@code biz_trade_order} 的创建 DTO。
     * <p>关键字段：{@code bizType=FOOD}、{@code verticalOrderId=垂直单主键}、金额三字段、一行明细（门店/套餐）。</p>
     *
     * @param order 垂直单
     * @param dto   创建 DTO（可为 null，仅影响金额兜底路径）
     * @param outVo 结果 VO（标题等展示字段）
     * @param total 应付总额（已解析）
     * @return 传给 {@link TradeOrderFacadeApi#createPendingPayOrder} 的 DTO
     */
    private TradeOrderCreateDTO buildFoodTradeOrderCreateDto(FoodBookingOrder order, FoodBookingCreateDTO dto,
                                                             FoodBookingResultVO outVo, BigDecimal total) {
        TradeOrderCreateDTO createDto = new TradeOrderCreateDTO();
        createDto.setMemberId(StrUtil.blankToDefault(order.getMemberId(), DEFAULT_MEMBER_ID));
        createDto.setBizType(TradeBizTypeEnum.FOOD.getValue());
        createDto.setTitle(outVo != null && StrUtil.isNotBlank(outVo.getTitle()) ? outVo.getTitle() : "美食预订");
        createDto.setTotalAmount(total);
        createDto.setDiscountAmount(BigDecimal.ZERO);
        createDto.setPayAmount(total);
        createDto.setVerticalOrderId(order.getId());
        createDto.setExtJson("{\"dualWriteSource\":\"FOOD_M5\"}");
        createDto.setTenantCode(order.getTenantCode());
        createDto.setTenantId(order.getTenantId());

        TradeOrderLineCreateDTO line = new TradeOrderLineCreateDTO();
        line.setLineNo(1);
        line.setItemTitle(createDto.getTitle());
        line.setSkuRef(order.getRestaurantId());
        line.setResourceRef(order.getPackageId());
        int qty = order.getQuantity() != null && order.getQuantity() > 0 ? order.getQuantity() : 1;
        line.setQuantity(qty);
        line.setUnitPrice(total.divide(BigDecimal.valueOf(qty), 2, RoundingMode.HALF_UP));
        line.setLineAmount(total);
        createDto.setLines(Collections.singletonList(line));
        return createDto;
    }

    /**
     * 双写成功后，把最新成功页结果（含 {@code unifiedOrderId}）序列化写回垂直单 {@code result_payload_json}。
     * <p>供小程序结果页/分享链接反序列化展示，与 {@link FoodBookingOrderService#getBookingResult} 配合。</p>
     *
     * @param orderId 垂直单主键
     * @param payload 待写入的结果 VO
     */
    private void refreshFoodResultPayloadJson(String orderId, FoodBookingResultVO payload) {
        FoodBookingOrder refreshed = foodBookingOrderService.get(orderId);
        if (payload != null && refreshed != null) {
            refreshed.setResultPayloadJson(JSONUtil.toJsonStr(payload));
            foodBookingOrderService.updateOrder(refreshed);
        }
    }

    /** 金额格式化为小程序展示文案，如 {@code ¥128}、{@code ¥0}。 */
    private static String formatAmountLabel(BigDecimal total) {
        if (total == null || total.compareTo(BigDecimal.ZERO) == 0) {
            return "¥0";
        }
        return "¥" + total.stripTrailingZeros().toPlainString();
    }

    private FoodBookingOrderQueryVO prepareQuery(FoodBookingOrderQueryVO query) {
        FoodBookingOrderQueryVO q = query != null ? query : new FoodBookingOrderQueryVO();
        if (StrUtil.isNotBlank(q.getMemberKeyword())) {
            List<String> memberIds = memberService.findIdsByKeyword(q.getMemberKeyword().trim());
            q.setMemberKeyword(null);
            if (CollUtil.isEmpty(memberIds)) {
                return null;
            }
            q.setMemberIdInCsv(QueryInCsvSupport.toIdInCsv(memberIds));
        }
        if (StrUtil.isNotBlank(q.getRestaurantName())) {
            List<String> restaurantIds = foodRestaurantService.findIdsByNameLike(q.getRestaurantName().trim());
            q.setRestaurantName(null);
            if (CollUtil.isEmpty(restaurantIds)) {
                return null;
            }
            q.setRestaurantIdInCsv(QueryInCsvSupport.toIdInCsv(restaurantIds));
        }
        if (StrUtil.isNotBlank(q.getPackageName())) {
            List<String> packageIds = foodPackageService.findIdsByNameLike(q.getPackageName().trim());
            q.setPackageName(null);
            if (CollUtil.isEmpty(packageIds)) {
                return null;
            }
            q.setPackageIdInCsv(QueryInCsvSupport.toIdInCsv(packageIds));
        }
        return q;
    }

    private void applyCouponOnCreate(FoodBookingOrder order, FoodBookingCreateDTO dto, FoodBookingResultVO vo) {
        if (order == null || dto == null || vo == null || StrUtil.isBlank(dto.getMemberCouponId())) {
            return;
        }
        BigDecimal orderAmount = resolveFoodPayAmount(order, dto, vo);
        if (orderAmount.compareTo(BigDecimal.ZERO) <= 0) {
            return;
        }
        if (vo.getAmount() == null) {
            vo.setAmount(orderAmount);
        }
        String memberId = StrUtil.blankToDefault(dto.getMemberId(), order.getMemberId());
        if (StrUtil.isBlank(memberId) || DEFAULT_MEMBER_ID.equals(memberId)) {
            return;
        }
        CouponCheckoutContextDTO context = buildFoodCheckoutContext(
                memberId, orderAmount, dto.getPackageId(), dto.getRestaurantId());
        BigDecimal discount = couponCheckoutApi.resolveDiscount(
                context, dto.getMemberCouponId().trim());
        if (discount.compareTo(BigDecimal.ZERO) <= 0) {
            return;
        }
        CouponFreezeCommandDTO freezeCommand = new CouponFreezeCommandDTO();
        freezeCommand.setMemberId(memberId);
        freezeCommand.setMemberCouponId(dto.getMemberCouponId().trim());
        freezeCommand.setOrderId(order.getId());
        freezeCommand.setOrderNo(StrUtil.blankToDefault(order.getTradeOrderId(), order.getId()));
        freezeCommand.setCheckoutContext(context);
        freezeCommand.setExpectedDiscount(discount);
        freezeCommand.setFreezeReason("美食团购待支付");
        couponCheckoutApi.freezeForOrder(freezeCommand);
        BigDecimal payAmount = orderAmount.subtract(discount).max(BigDecimal.ZERO)
                .setScale(2, RoundingMode.HALF_UP);
        persistFoodBookingResultAmount(order, vo, payAmount);
    }

    private void adjustPayAmountByFrozenCoupon(FoodPayPrepareVO vo) {
        if (vo == null || StrUtil.isBlank(vo.getOrderId()) || vo.getAmountFen() == null) {
            return;
        }
        BigDecimal discount = couponCheckoutApi.getFrozenDiscountAmount(vo.getOrderId().trim());
        if (discount.compareTo(BigDecimal.ZERO) <= 0) {
            return;
        }
        long discountFen = discount.multiply(BigDecimal.valueOf(100))
                .setScale(0, RoundingMode.HALF_UP)
                .longValue();
        vo.setAmountFen(Math.max(0L, vo.getAmountFen() - discountFen));
    }

    private FoodBookingOrder resolveBookingOrder(String orderId) {
        if (StrUtil.isBlank(orderId)) {
            return null;
        }
        FoodBookingOrder entity = foodBookingOrderService.get(orderId.trim());
        if (entity != null) {
            return entity;
        }
        FoodBookingOrderQueryVO byUnified = new FoodBookingOrderQueryVO();
        byUnified.setUnifiedOrderId(orderId.trim());
        List<FoodBookingOrder> list = foodBookingOrderService.findList(byUnified);
        return list == null || list.isEmpty() ? null : list.get(0);
    }

    private void resolveVerticalOrderId(FoodPreparePayDTO dto) {
        if (dto == null || StrUtil.isBlank(dto.getOrderId())) {
            return;
        }
        FoodBookingOrder order = resolveBookingOrder(dto.getOrderId());
        if (order != null) {
            dto.setOrderId(order.getId());
        }
    }

    private void resolveVerticalOrderId(FoodConfirmPayDTO dto) {
        if (dto == null || StrUtil.isBlank(dto.getOrderId())) {
            return;
        }
        FoodBookingOrder order = resolveBookingOrder(dto.getOrderId());
        if (order != null) {
            dto.setOrderId(order.getId());
        }
    }

    private void resolveVerticalOrderId(FoodCancelBookingDTO dto) {
        if (dto == null || StrUtil.isBlank(dto.getOrderId())) {
            return;
        }
        FoodBookingOrder order = resolveBookingOrder(dto.getOrderId());
        if (order != null) {
            dto.setOrderId(order.getId());
        }
    }

    private void resolveVerticalOrderId(FoodBookingRefundTriggerDTO dto) {
        if (dto == null || StrUtil.isBlank(dto.getOrderId())) {
            return;
        }
        FoodBookingOrder order = resolveBookingOrder(dto.getOrderId());
        if (order != null) {
            dto.setOrderId(order.getId());
        }
    }

    private void resolveVerticalOrderId(FoodBookingRefundApplyDTO dto) {
        if (dto == null || StrUtil.isBlank(dto.getOrderId())) {
            return;
        }
        FoodBookingOrder order = resolveBookingOrder(dto.getOrderId());
        if (order != null) {
            dto.setOrderId(order.getId());
        }
    }

    private void triggerFinanceSettleOnPaySuccess(FoodConfirmPayDTO dto) {
        if (dto == null || StrUtil.isBlank(dto.getOrderId())) {
            return;
        }
        String bizOrderId = dto.getOrderId().trim();
        String financePayOrderId = StrUtil.blankToDefault(dto.getPayOrderId(), null);
        if (StrUtil.isBlank(financePayOrderId)) {
            FoodBookingOrder order = foodBookingOrderService.get(bizOrderId);
            if (order != null) {
                financePayOrderId = order.getPayOrderId();
            }
        }
        if (StrUtil.isBlank(financePayOrderId)) {
            log.warn("美食支付成功缺少 financePayOrderId，跳过结算挂接 orderId={}", bizOrderId);
            return;
        }
        String bizScene = PayRecordBizSceneEnum.FOOD_ORDER.getCode();
        String txnId = dto.getThirdPartyTradeNo();
        try {
            profitSharingApi.createPendingForPay(financePayOrderId, bizScene, null, bizOrderId, txnId);
        } catch (Exception ex) {
            log.warn("美食创建待分账失败 orderId={}: {}", bizOrderId, ex.getMessage());
        }
        try {
            merchantReceivableApi.recordForMallPay(financePayOrderId, bizOrderId, txnId);
        } catch (Exception ex) {
            log.warn("美食记商户应收失败 orderId={}: {}", bizOrderId, ex.getMessage());
        }
    }

    private void triggerProfitSharingOnVerify(String bookingOrderId) {
        if (StrUtil.isBlank(bookingOrderId)) {
            return;
        }
        try {
            profitSharingApi.scheduleSharingByBizOrder(
                    PayRecordBizSceneEnum.FOOD_ORDER.getCode(), bookingOrderId.trim());
        } catch (Exception ex) {
            log.warn("美食核销触发分账失败 orderId={}: {}", bookingOrderId, ex.getMessage());
        }
    }

    private static void assertMemberOwns(String orderMemberId, String memberId) {
        if (StrUtil.isBlank(memberId)) {
            throw new BizErrorException("请先登录");
        }
        if (StrUtil.isNotBlank(orderMemberId) && !StrUtil.equals(memberId, orderMemberId)) {
            throw new BizErrorException("无权申请退款");
        }
    }

    private static void assertRefundable(FoodBookingOrder order) {
        if (!VerticalPayStatusEnum.PAID.getValue().equals(order.getPayStatus())) {
            throw new BizErrorException("仅已支付预订单可发起退款");
        }
        if (FoodBookingStatusEnum.USED.getValue().equals(order.getBookStatus())) {
            throw new BizErrorException("已核销预订单不可退款");
        }
        if (FoodBookingStatusEnum.REFUNDED.getValue().equals(order.getBookStatus())) {
            throw new BizErrorException("预订单已退款");
        }
    }

    private BigDecimal resolveRefundableAmount(FoodBookingOrder order) {
        FoodBookingResultVO vo = foodBookingOrderService.getBookingResult(order.getId());
        if (vo != null && vo.getAmount() != null && vo.getAmount().compareTo(BigDecimal.ZERO) > 0) {
            return vo.getAmount();
        }
        throw new BizErrorException("无法解析预订单可退金额，请手动填写退款金额");
    }

    private CouponCheckoutContextDTO buildFoodCheckoutContext(String memberId, BigDecimal orderAmount,
                                                              String packageId, String restaurantId) {
        CouponCheckoutContextDTO context = new CouponCheckoutContextDTO();
        context.setMemberId(memberId);
        context.setSceneCode(CouponSceneCodeEnum.FOOD.getCode());
        context.setPlatform("MINI");
        context.setOrderAmount(orderAmount);
        List<String> scopeItemIds = new ArrayList<>();
        if (StrUtil.isNotBlank(packageId)) {
            scopeItemIds.add(packageId.trim());
        }
        context.setScopeItemIds(scopeItemIds);
        if (StrUtil.isNotBlank(restaurantId)) {
            context.setCategoryIds(List.of(restaurantId.trim()));
        }
        return context;
    }

    private void persistFoodBookingResultAmount(FoodBookingOrder order, FoodBookingResultVO vo, BigDecimal payAmount) {
        vo.setAmount(payAmount);
        vo.setAmountLabel(formatFoodYuanLabel(payAmount));
        order.setResultPayloadJson(JSONUtil.toJsonStr(vo));
        foodBookingOrderService.updateOrder(order);
    }

    private static String formatFoodYuanLabel(BigDecimal total) {
        if (total == null || total.compareTo(BigDecimal.ZERO) == 0) {
            return "¥0";
        }
        return "¥" + total.stripTrailingZeros().toPlainString();
    }

    private void enrichListDisplay(List<FoodBookingOrderListVO> rows) {
        if (CollUtil.isEmpty(rows)) {
            return;
        }
        memberService.enrichMemberDisplay(rows);
        List<String> restaurantIds = rows.stream()
            .map(FoodBookingOrderListVO::getRestaurantId)
            .filter(StrUtil::isNotBlank)
            .distinct()
            .toList();
        List<String> packageIds = rows.stream()
            .map(FoodBookingOrderListVO::getPackageId)
            .filter(StrUtil::isNotBlank)
            .distinct()
            .toList();
        Map<String, String> restaurantNameMap = foodRestaurantService.mapRestaurantNameByIds(restaurantIds);
        Map<String, String> packageNameMap = foodPackageService.mapPackageNameByIds(packageIds);
        for (FoodBookingOrderListVO row : rows) {
            if (StrUtil.isNotBlank(row.getRestaurantId())) {
                row.setRestaurantName(restaurantNameMap.get(row.getRestaurantId()));
            }
            if (StrUtil.isNotBlank(row.getPackageId())) {
                row.setPackageName(packageNameMap.get(row.getPackageId()));
            }
        }
    }

    private void enrichInfoDisplay(FoodBookingOrderInfoVO vo) {
        if (vo == null) {
            return;
        }
        memberService.enrichMemberDisplay(vo);
        if (StrUtil.isNotBlank(vo.getRestaurantId())) {
            Map<String, String> restaurantNameMap = foodRestaurantService.mapRestaurantNameByIds(List.of(vo.getRestaurantId()));
            vo.setRestaurantName(restaurantNameMap.get(vo.getRestaurantId()));
        }
        if (StrUtil.isNotBlank(vo.getPackageId())) {
            Map<String, String> packageNameMap = foodPackageService.mapPackageNameByIds(List.of(vo.getPackageId()));
            vo.setPackageName(packageNameMap.get(vo.getPackageId()));
        }
    }

    private static PageRespVO<FoodBookingOrderListVO> emptyBookingPage(Page<FoodBookingOrder> page) {
        PageRespVO<FoodBookingOrderListVO> empty = new PageRespVO<>();
        empty.setRecords(Collections.emptyList());
        empty.setTotal(0L);
        empty.setPageNo(page != null ? page.getPageNo() : 1L);
        empty.setPageSize(page != null ? page.getPageSize() : 10L);
        return empty;
    }
}
