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

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.RandomUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import com.jeesharp.business.modules.finance.api.MerchantReceivableApi;
import com.jeesharp.business.modules.finance.api.PayOrderApi;
import com.jeesharp.business.modules.finance.api.PayRefundApi;
import com.jeesharp.business.modules.finance.api.ProfitSharingApi;
import com.jeesharp.business.modules.finance.payorder.vo.PayOrderIntentVO;
import com.jeesharp.business.modules.finance.payrefund.dto.PayRefundCreateDTO;
import com.jeesharp.business.modules.finance.paymentrecord.enums.PayRecordBizSceneEnum;
import com.jeesharp.business.modules.food.application.support.FoodCompanyMealRefundCoordinator;
import com.jeesharp.business.modules.food.application.support.FoodMealPayAssembler;
import com.jeesharp.business.modules.food.dto.FoodPersonalMealRefundDTO;
import com.jeesharp.business.modules.food.dto.FoodPersonalMealRefundTriggerDTO;
import com.jeesharp.business.modules.food.dto.FoodCompanyMealOrderCreateDTO;
import com.jeesharp.business.modules.food.dto.FoodCompanyMealOrderPaidDTO;
import com.jeesharp.business.modules.food.dto.FoodCompanyMealTrialDTO;
import com.jeesharp.business.modules.food.dto.FoodMealOrderRefundApplyDTO;
import com.jeesharp.business.modules.food.dto.FoodMealOrderRefundSuccessDTO;
import com.jeesharp.business.modules.food.dto.FoodMealOrderVerifyDTO;
import com.jeesharp.business.modules.food.dto.FoodPersonalMealOrderCancelDTO;
import com.jeesharp.business.modules.food.entity.FoodCompany;
import com.jeesharp.business.modules.food.entity.FoodCompanyAccount;
import com.jeesharp.business.modules.food.entity.FoodCompanyAccountLog;
import com.jeesharp.business.modules.food.entity.FoodCompanyEmployee;
import com.jeesharp.business.modules.food.entity.FoodCompanySubsidyRule;
import com.jeesharp.business.modules.food.entity.FoodCompanySubsidyUse;
import com.jeesharp.business.modules.food.entity.FoodDish;
import com.jeesharp.business.modules.food.entity.FoodMealMenu;
import com.jeesharp.business.modules.food.entity.FoodMealMenuItem;
import com.jeesharp.business.modules.food.entity.FoodMealMenuItemSpec;
import com.jeesharp.business.modules.food.entity.FoodMealOrder;
import com.jeesharp.business.modules.food.entity.FoodMealOrderItem;
import com.jeesharp.business.modules.food.entity.FoodMealRule;
import com.jeesharp.business.modules.food.entity.FoodMealServiceArea;
import com.jeesharp.business.modules.food.service.FoodCompanyAccountLedgerService;
import com.jeesharp.business.modules.food.service.FoodCompanyAccountLogService;
import com.jeesharp.business.modules.food.service.FoodCompanyAccountService;
import com.jeesharp.business.modules.food.service.FoodCompanyEmployeeService;
import com.jeesharp.business.modules.food.service.FoodCompanyService;
import com.jeesharp.business.modules.food.service.FoodCompanySubsidyRuleService;
import com.jeesharp.business.modules.food.service.FoodCompanySubsidyUseService;
import com.jeesharp.business.modules.food.service.FoodDishService;
import com.jeesharp.business.modules.food.service.FoodMealMenuItemService;
import com.jeesharp.business.modules.food.service.FoodMealMenuItemSpecService;
import com.jeesharp.business.modules.food.service.FoodMealMenuService;
import com.jeesharp.business.modules.food.service.FoodMealOrderItemService;
import com.jeesharp.business.modules.food.service.FoodMealOrderService;
import com.jeesharp.business.modules.food.service.FoodMealRuleService;
import com.jeesharp.business.modules.food.service.FoodMealServiceAreaService;
import com.jeesharp.business.modules.food.vo.FoodCompanyEmployeeCompanyVO;
import com.jeesharp.business.modules.food.vo.FoodCompanyMealBenefitVO;
import com.jeesharp.business.modules.food.vo.FoodCompanyMealOrderResultVO;
import com.jeesharp.business.modules.food.vo.FoodCompanyMealTrialVO;
import com.jeesharp.business.modules.food.vo.FoodMealDateStatusVO;
import com.jeesharp.business.modules.food.vo.FoodMealMenuItemListVO;
import com.jeesharp.business.modules.food.vo.FoodMealOrderQueryVO;
import com.jeesharp.business.modules.food.vo.FoodMealRestaurantVO;
import com.jeesharp.business.modules.food.vo.FoodPersonalMealMenuVO;
import com.jeesharp.business.modules.food.vo.FoodPersonalMealPayPrepareVO;
import com.jeesharp.business.modules.mall.application.MallMemberAddressApplicationService;
import com.jeesharp.business.modules.mall.entity.MallMemberAddress;
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.TradeOrderQueryVO;
import com.jeesharp.framework.exception.BizErrorException;
import com.jeesharp.framework.pojo.Page;
import com.jeesharp.framework.pojo.query.PageRespVO;
import com.jeesharp.framework.utils.WebFrameworkUtils;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.DayOfWeek;
import java.time.Duration;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

/**
 * 企业团餐应用服务：员工补贴试算、企业账户扣款下单、核销与退款编排。
 *
 * <p>补贴冻结 → 支付成功扣款 → 核销/退款回滚补贴；与个人订餐
 * {@link FoodPersonalMealApplicationService} 独立。</p>
 *
 * @author jeesharp
 * @since 2026-06-07
 */
@Slf4j
@Service
@AllArgsConstructor
@Transactional(readOnly = true)
public class FoodCompanyMealApplicationService {

    private static final String DEL_NORMAL = "0";
    private static final int STATUS_ENABLED = 1;
    private static final int BIND_STATUS_BOUND = 1;
    private static final int SUBSIDY_STATUS_FROZEN = 0;
    private static final int SUBSIDY_STATUS_USED = 1;
    private static final int SUBSIDY_STATUS_CANCELED = 2;
    private static final int SUBSIDY_STATUS_REFUNDED = 3;
    private static final String MENU_STATUS_PUBLISHED = "published";
    private static final String ITEM_STATUS_PUBLISHED = "published";
    private static final String MENU_SCOPE_COMPANY = "company";
    private static final String MENU_SCOPE_ALL = "all";
    private static final String RULE_SCOPE_ALL = "all";
    private static final String RULE_SCOPE_COMPANY = "company";
    private static final String RULE_TYPE_FULL = "FULL";
    private static final String RULE_TYPE_DISCOUNT = "DISCOUNT";
    private static final String ORDER_TYPE_COMPANY = "company";
    private static final String DELIVERY_TYPE_DELIVERY = "delivery";
    private static final String DELIVERY_TYPE_PICKUP = "pickup";
    private static final String ORDER_STATUS_PENDING_PAY = "pending_pay";
    private static final String ORDER_STATUS_PAID = "paid";
    private static final String ORDER_STATUS_CANCELLED = "cancelled";
    private static final String ORDER_STATUS_REFUNDING = "refunding";
    private static final String ORDER_STATUS_REFUNDED = "refunded";
    private static final String ORDER_STATUS_PICKED = "picked";
    private static final String PAY_STATUS_UNPAID = "unpaid";
    private static final String PAY_STATUS_PAID = "paid";
    private static final String PAY_STATUS_REFUNDED = "refunded";
    private static final String SOURCE_MINI = "MINI";
    private static final String SOURCE_ADMIN = "ADMIN";
    private static final String SOURCE_PAY_NOTIFY = "PAY_NOTIFY";    private static final String DATE_STATUS_ORDERABLE = "orderable";
    private static final String DATE_STATUS_EXPIRED = "expired";
    private static final String DATE_STATUS_NOT_STARTED = "not_started";
    private static final String DATE_STATUS_AFTER_DEADLINE = "after_deadline";
    private static final String DATE_STATUS_OUT_OF_RANGE = "out_of_range";
    private static final String DATE_STATUS_NO_MENU = "no_menu";
    private static final String DATE_STATUS_INVALID_DATE = "invalid_date";
    private static final String DATE_STATUS_UNAVAILABLE = "unavailable";
    private static final String CANCEL_RULE_NONE = "none";
    private static final String CANCEL_RULE_BEFORE_DEADLINE = "before_deadline";
    private static final String CANCEL_RULE_ANYTIME = "anytime";
    private static final String SOURCE_REFUND_NOTIFY = "REFUND_NOTIFY";
    private static final String ORDER_NO_PREFIX = "FCM";
    private static final DateTimeFormatter NO_TIME_FMT = DateTimeFormatter.ofPattern("yyyyMMddHHmmss");
    private static final BigDecimal ZERO = BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP);
    private static final BigDecimal ONE = BigDecimal.ONE;

    private final FoodCompanyService foodCompanyService;
    private final FoodCompanyEmployeeService foodCompanyEmployeeService;
    private final FoodCompanyAccountService foodCompanyAccountService;
    private final FoodCompanyAccountLogService foodCompanyAccountLogService;
    private final FoodCompanyAccountLedgerService foodCompanyAccountLedgerService;
    private final FoodCompanySubsidyRuleService foodCompanySubsidyRuleService;
    private final FoodCompanySubsidyUseService foodCompanySubsidyUseService;
    private final FoodMealMenuService foodMealMenuService;
    private final FoodMealMenuItemService foodMealMenuItemService;
    private final FoodMealMenuItemSpecService foodMealMenuItemSpecService;
    private final FoodDishService foodDishService;
    private final FoodMealRuleService foodMealRuleService;
    private final FoodMealServiceAreaService foodMealServiceAreaService;
    private final FoodMealOrderService foodMealOrderService;
    private final FoodMealOrderItemService foodMealOrderItemService;
    private final PayOrderApi payOrderApi;
    private final ProfitSharingApi profitSharingApi;
    private final MerchantReceivableApi merchantReceivableApi;
    private final PayRefundApi payRefundApi;
    private final TradeOrderFacadeApi tradeOrderFacadeApi;
    private final MallMemberAddressApplicationService mallMemberAddressApplicationService;
    private final FoodCompanyMealRefundCoordinator foodCompanyMealRefundCoordinator;
    private final FoodMealPayAssembler foodMealPayAssembler;

    public FoodCompanyMealBenefitVO getBenefit(String memberId, String companyId) {
        EmployeeContext context = getValidEmployeeContext(memberId, companyId);
        List<FoodCompanySubsidyRule> rules = listEffectiveRules(companyId, null, LocalDateTime.now());

        FoodCompanyMealBenefitVO vo = new FoodCompanyMealBenefitVO();
        vo.setValidEmployee(Boolean.TRUE);
        vo.setCompanyId(context.company().getId());
        vo.setCompanyCode(context.company().getCompanyCode());
        vo.setCompanyName(context.company().getCompanyName());
        vo.setEmployeeId(context.employee().getId());
        vo.setEmployeeNo(context.employee().getEmployeeNo());
        vo.setEmployeeName(context.employee().getEmployeeName());
        vo.setMobile(context.employee().getMobile());
        vo.setMemberId(context.employee().getMemberId());
        if (context.account() != null) {
            vo.setAccountId(context.account().getId());
            vo.setAvailableBalance(money(context.account().getAvailableBalance()));
            vo.setFrozenBalance(money(context.account().getFrozenBalance()));
        }
        vo.setSubsidyRules(rules.stream().map(this::toRuleSummary).collect(Collectors.toList()));
        return vo;
    }

    public List<FoodCompanyMealTrialVO.FoodCompanySubsidyRuleSummaryVO> listSubsidyRules(String memberId, String companyId, String mealType) {
        getValidEmployeeContext(memberId, companyId);
        return listEffectiveRules(companyId, mealType, LocalDateTime.now()).stream()
            .map(this::toRuleSummary)
            .collect(Collectors.toList());
    }

    public List<FoodCompanyEmployeeCompanyVO> listMyCompanies(String memberId) {
        if (StrUtil.isBlank(memberId)) {
            throw new BizErrorException("会员ID不能为空");
        }
        List<FoodCompanyEmployee> employees = foodCompanyEmployeeService.listBoundByMemberId(memberId);
        if (CollUtil.isEmpty(employees)) {
            return new ArrayList<>();
        }
        return employees.stream()
            .map(this::toEmployeeCompanyVO)
            .filter(vo -> vo != null)
            .collect(Collectors.toList());
    }

    public List<LocalDate> listBookableDates(String memberId, String companyId, String serviceAreaId) {
        getValidEmployeeContext(memberId, companyId);
        FoodMealRule rule = chooseMealRule(serviceAreaId, null);
        int futureDays = rule != null && rule.getBookableFutureDays() != null && rule.getBookableFutureDays() >= 0
            ? rule.getBookableFutureDays()
            : 7;
        LocalDate today = LocalDate.now();
        List<LocalDate> dates = new ArrayList<>();
        for (int i = 0; i <= futureDays; i++) {
            LocalDate date = today.plusDays(i);
            List<FoodMealMenu> menus = listPublishedCompanyMenus(serviceAreaId, date, null, companyId);
            boolean hasOrderableMeal = menus.stream()
                .map(FoodMealMenu::getMealType)
                .filter(StrUtil::isNotBlank)
                .map(this::normalizeMealType)
                .distinct()
                .anyMatch(mealType -> checkMealOrderable(serviceAreaId, mealType, date).orderable());
            if (hasOrderableMeal) {
                dates.add(date);
            }
        }
        return dates;
    }

    public List<FoodMealDateStatusVO> listDateStatuses(String memberId, String companyId, String serviceAreaId, LocalDate weekStart) {
        getValidEmployeeContext(memberId, companyId);
        LocalDate start = normalizeWeekStart(weekStart);
        LocalDateTime serverTime = LocalDateTime.now();
        List<FoodMealDateStatusVO> statuses = new ArrayList<>();
        for (int i = 0; i < 7; i++) {
            LocalDate date = start.plusDays(i);
            List<FoodMealMenu> menus = listPublishedCompanyMenus(serviceAreaId, date, null, companyId);
            if (CollUtil.isEmpty(menus)) {
                statuses.add(buildDateStatus(date, MealOrderableResult.fail(DATE_STATUS_NO_MENU, "当前日期暂无可订菜单", null), serverTime));
                continue;
            }
            List<MealOrderableResult> results = menus.stream()
                .map(FoodMealMenu::getMealType)
                .filter(StrUtil::isNotBlank)
                .map(this::normalizeMealType)
                .distinct()
                .map(mealType -> checkMealOrderable(serviceAreaId, mealType, date))
                .collect(Collectors.toList());
            statuses.add(buildDateStatus(date, chooseDateStatusResult(results), serverTime));
        }
        return statuses;
    }

    public List<Map<String, String>> listMealTypes(String memberId, String companyId, String serviceAreaId, LocalDate mealDate) {
        getValidEmployeeContext(memberId, companyId);
        return listPublishedCompanyMenus(serviceAreaId, mealDate, null, companyId).stream()
            .map(FoodMealMenu::getMealType)
            .filter(StrUtil::isNotBlank)
            .map(this::normalizeMealType)
            .distinct()
            .filter(type -> checkMealOrderable(serviceAreaId, type, mealDate).orderable())
            .map(type -> Map.of("value", type, "label", mealTypeLabel(type)))
            .collect(Collectors.toList());
    }

    public FoodPersonalMealMenuVO getMenu(String memberId, String companyId, String serviceAreaId, LocalDate mealDate, String mealType) {
        getValidEmployeeContext(memberId, companyId);
        List<FoodMealMenu> menus = listPublishedCompanyMenus(serviceAreaId, mealDate, mealType, companyId);
        if (CollUtil.isEmpty(menus)) {
            return null;
        }
        FoodMealMenu menu = chooseCompanyMenu(menus, companyId);
        FoodPersonalMealMenuVO vo = toMenuVO(menu);
        fillMenuRuntimeInfo(vo, menu);
        List<FoodMealMenuItem> items = foodMealMenuItemService.listByMenuId(menu.getId()).stream()
            .filter(this::isPublishedMenuItem)
            .collect(Collectors.toList());
        List<FoodMealMenuItemListVO> itemVOList = items.stream().map(this::toMenuItemVO).collect(Collectors.toList());
        fillMenuItemSpecs(itemVOList, items);
        applyDishDisplay(itemVOList, items);
        vo.setItems(itemVOList);
        return vo;
    }

    public FoodMealMenuItemListVO getMenuItemDetail(String memberId, String companyId, String menuItemId) {
        if (StrUtil.isBlank(menuItemId)) {
            return null;
        }
        getValidEmployeeContext(memberId, companyId);
        FoodMealMenuItem item = foodMealMenuItemService.get(menuItemId);
        if (item == null || !DEL_NORMAL.equals(item.getDelFlag()) || !isPublishedMenuItem(item)) {
            return null;
        }
        FoodMealMenu menu = foodMealMenuService.get(item.getMenuId());
        if (menu == null || !DEL_NORMAL.equals(menu.getDelFlag()) || !MENU_STATUS_PUBLISHED.equals(menu.getPublishStatus())) {
            return null;
        }
        if (!companyMenuAvailable(menu, companyId)) {
            return null;
        }
        FoodMealMenuItemListVO vo = toMenuItemVO(item);
        fillMenuItemSpecs(List.of(vo), List.of(item));
        applyDishDisplay(List.of(vo), List.of(item));
        MealOrderableResult orderableResult = checkMealOrderable(menu.getServiceAreaId(), menu.getMealType(), menu.getMenuDate());
        vo.setOrderable(orderableResult.orderable());
        vo.setNotOrderableReason(orderableResult.reason());
        vo.setDeadlineTime(orderableResult.deadlineTime());
        vo.setDeadlineRemainSeconds(deadlineRemainSeconds(menu.getMenuDate(), orderableResult.deadlineTime()));
        vo.setRestaurant(mealServiceArea(menu.getServiceAreaId()));
        vo.setMenuDate(menu.getMenuDate());
        vo.setMealType(normalizeMealType(menu.getMealType()));
        vo.setPickupType(DELIVERY_TYPE_PICKUP);
        vo.setPickupDate(menu.getMenuDate());
        vo.setPickupLocation(vo.getRestaurant() == null ? null : vo.getRestaurant().getPickupLocation());
        return vo;
    }

    public FoodCompanyMealTrialVO trial(FoodCompanyMealTrialDTO dto) {
        EmployeeContext context = getValidEmployeeContext(dto.getMemberId(), dto.getCompanyId());
        FoodMealMenu menu = getValidMenu(dto, context.company());
        validateMealRule(menu.getServiceAreaId(), dto.getMealType(), dto.getMealDate());
        List<FoodMealMenuItem> menuItems = getValidMenuItems(dto, false);
        List<MealOrderRequestItem> requestItems = buildRequestItems(dto, menu, menuItems, false);
        return buildTrial(dto, context, menu, requestItems);
    }

    @Transactional(readOnly = false)
    public FoodCompanyMealOrderResultVO createCompanyMealOrder(FoodCompanyMealOrderCreateDTO dto) {
        EmployeeContext context = getValidEmployeeContext(dto.getMemberId(), dto.getCompanyId());
        FoodMealMenu menu = getValidMenu(dto, context.company());
        validateMealRule(menu.getServiceAreaId(), dto.getMealType(), dto.getMealDate());
        List<FoodMealMenuItem> menuItems = getValidMenuItems(dto, true);
        List<MealOrderRequestItem> requestItems = buildRequestItems(dto, menu, menuItems, true);
        FoodCompanyMealTrialVO trial = buildTrial(dto, context, menu, requestItems);
        FoodCompanyAccount account = lockAccountIfNecessary(context.account(), trial.getCompanyPayAmount());
        if (account != null) {
            trial.setAccountAvailableBalance(money(account.getAvailableBalance()));
        }
        if (trial.getCompanyPayAmount().compareTo(ZERO) > 0 && money(account.getAvailableBalance()).compareTo(trial.getCompanyPayAmount()) < 0) {
            throw new BizErrorException("企业账户余额不足");
        }

        boolean pickup = DELIVERY_TYPE_PICKUP.equals(StrUtil.blankToDefault(dto.getDeliveryType(), DELIVERY_TYPE_PICKUP));
        MallMemberAddress address = pickup ? null : mallMemberAddressApplicationService.getMemberAddressForOrder(dto.getMemberId(), dto.getAddressId());
        FoodMealOrder order = buildOrder(dto, context, menu, trial);
        fillDeliverySnapshot(order, address, pickup ? DELIVERY_TYPE_PICKUP : DELIVERY_TYPE_DELIVERY, dto.getRemark());
        insertOrder(order);
        List<FoodMealOrderItem> orderItems = buildOrderItems(order, requestItems, trial.getItems());
        for (FoodMealOrderItem orderItem : orderItems) {
            insertOrderItem(orderItem);
        }
        increaseSoldQuantity(orderItems);

        FoodCompanySubsidyUse subsidyUse = null;
        if (trial.getCompanyPayAmount().compareTo(ZERO) > 0) {
            foodCompanyAccountLedgerService.freezeAmount(account, order, trial.getCompanyPayAmount(), SOURCE_MINI, context.employee().getMemberId());
            subsidyUse = createFrozenSubsidyUse(order, context, trial);
        }
        syncTradeOrderOnCreate(order, orderItems);

        PayOrderIntentVO intent = null;
        if (trial.getEmployeePayAmount().compareTo(ZERO) > 0) {
            intent = createMealPayOrderIntent(order, dto);
            order.setTradeOrderId(intent.getPayOrderId());
            order.setUnifiedOrderId(intent.getOutTradeNo());
            updateOrder(order);
            log.info("企业团餐差额支付单已创建 companyId={}, orderId={}, orderNo={}, employeePayAmount={}, payOrderId={}, payOrderNo={}",
                    order.getCompanyId(), order.getId(), order.getOrderNo(), order.getUserPayAmount(),
                    order.getTradeOrderId(), order.getUnifiedOrderId());
        } else {
            log.info("企业团餐全额企业支付，直接结算开始 companyId={}, orderId={}, orderNo={}, companyPayAmount={}",
                    order.getCompanyId(), order.getId(), order.getOrderNo(), order.getCompanyPayAmount());
            completePaidOrder(order, account, subsidyUse, LocalDateTime.now(), SOURCE_MINI, context.employee().getMemberId());
        }
        return toOrderResult(order, intent);
    }

    /**
     * 企业团餐支付成功确认（微信回调入口）：校验金额 → 扣减企业账户/补贴 → 订单置已支付 → 触发结算。
     * <p>幂等：已支付单校验金额后直接返回。</p>
     */
    @Transactional(readOnly = false)
    public FoodCompanyMealOrderResultVO handleCompanyMealOrderPaid(FoodCompanyMealOrderPaidDTO dto) {
        FoodMealOrder order = lockOrderForPaid(dto);
        if (order == null) {
            throw new BizErrorException("企业团餐订单不存在");
        }
        // 幂等：已支付则校验回调金额后返回
        if (PAY_STATUS_PAID.equals(order.getPayStatus()) || ORDER_STATUS_PAID.equals(order.getOrderStatus())) {
            assertPaidAmountMatched(order, dto.getPaidAmount());
            return toOrderResult(order, null);
        }
        if (!ORDER_TYPE_COMPANY.equals(order.getOrderType())
                || !ORDER_STATUS_PENDING_PAY.equals(order.getOrderStatus())
                || !PAY_STATUS_UNPAID.equals(order.getPayStatus())) {
            log.warn("企业团餐支付回调状态非法 orderId={}, orderNo={}, orderType={}, orderStatus={}, payStatus={}",
                    order.getId(), order.getOrderNo(), order.getOrderType(), order.getOrderStatus(), order.getPayStatus());
            throw new BizErrorException("当前订单不可支付确认");
        }
        assertPaidAmountMatched(order, dto.getPaidAmount());
        // 企业承担部分 > 0 时锁定企业账户（扣可用/解冻补贴）
        FoodCompanyAccount account = null;
        if (money(order.getCompanyPayAmount()).compareTo(ZERO) > 0) {
            account = foodCompanyAccountService.lockByCompanyId(order.getCompanyId(), order.getTenantCode());
            if (account == null || !Integer.valueOf(STATUS_ENABLED).equals(account.getStatus())) {
                log.warn("企业团餐支付回调账户异常 orderId={}, orderNo={}, companyId={}, accountExists={}, accountStatus={}",
                        order.getId(), order.getOrderNo(), order.getCompanyId(), account != null, account == null ? null : account.getStatus());
                throw new BizErrorException("企业账户不存在或未启用");
            }
            log.info("企业团餐支付回调账户已锁定 companyId={}, accountId={}, orderId={}, availableBalance={}, frozenBalance={}, companyPayAmount={}",
                    account.getCompanyId(), account.getId(), order.getId(), account.getAvailableBalance(), account.getFrozenBalance(), order.getCompanyPayAmount());
        }
        FoodCompanySubsidyUse subsidyUse = selectFrozenSubsidyUse(order.getId());
        // 完成扣款、补贴核销、订单置已支付
        completePaidOrder(order, account, subsidyUse, dto.getPaidTime() != null ? dto.getPaidTime() : LocalDateTime.now(), SOURCE_PAY_NOTIFY, null);
        triggerFinanceSettleOnPaySuccess(dto, order);
        return toOrderResult(order, null);
    }

    public FoodPersonalMealPayPrepareVO preparePay(String orderId, String memberId) {
        FoodMealOrder order = foodMealOrderService.get(orderId);
        if (order == null || !DEL_NORMAL.equals(order.getDelFlag()) || !ORDER_TYPE_COMPANY.equals(order.getOrderType())) {
            return null;
        }
        if (StrUtil.isNotBlank(memberId) && !StrUtil.equals(memberId, order.getMemberId())) {
            return null;
        }
        if (!ORDER_STATUS_PENDING_PAY.equals(order.getOrderStatus()) || !PAY_STATUS_UNPAID.equals(order.getPayStatus())) {
            throw new BizErrorException("当前订单不可支付");
        }
        if (money(order.getUserPayAmount()).compareTo(ZERO) <= 0) {
            throw new BizErrorException("当前订单无需员工补差支付");
        }
        return foodMealPayAssembler.buildPrepareVo(order);
    }

    @Transactional(readOnly = false)
    public FoodCompanyMealOrderResultVO bindCompanyPayOrder(String orderId, String memberId, String payOrderId, String outTradeNo) {
        FoodMealOrder order = foodMealOrderService.lockById(orderId, WebFrameworkUtils.getLoginUserTenantCode());
        if (order == null || !ORDER_TYPE_COMPANY.equals(order.getOrderType())) {
            throw new BizErrorException("企业团餐订单不存在");
        }
        if (StrUtil.isNotBlank(memberId) && !StrUtil.equals(memberId, order.getMemberId())) {
            throw new BizErrorException("无权操作该订单");
        }
        if (!ORDER_STATUS_PENDING_PAY.equals(order.getOrderStatus()) || !PAY_STATUS_UNPAID.equals(order.getPayStatus())) {
            throw new BizErrorException("当前订单不可支付");
        }
        if (money(order.getUserPayAmount()).compareTo(ZERO) <= 0) {
            throw new BizErrorException("当前订单无需员工补差支付");
        }
        order.setTradeOrderId(payOrderId);
        order.setUnifiedOrderId(outTradeNo);
        updateOrder(order);
        return getCompanyOrderDetail(order.getId(), memberId);
    }

    @Transactional(readOnly = false)
    public FoodCompanyMealOrderResultVO cancel(FoodPersonalMealOrderCancelDTO dto, String memberId) {
        log.info("企业团餐取消开始 orderId={}, memberId={}, cancelReason={}", dto.getOrderId(), memberId, dto.getCancelReason());
        FoodMealOrder order = foodMealOrderService.lockById(dto.getOrderId(), WebFrameworkUtils.getLoginUserTenantCode());
        if (order == null || !ORDER_TYPE_COMPANY.equals(order.getOrderType())) {
            log.warn("企业团餐取消失败，订单不存在或类型不匹配 orderId={}, memberId={}", dto.getOrderId(), memberId);
            throw new BizErrorException("企业团餐订单不存在");
        }
        log.info("企业团餐取消订单已锁定 orderId={}, orderNo={}, companyId={}, orderStatus={}, payStatus={}, companyPayAmount={}",
                order.getId(), order.getOrderNo(), order.getCompanyId(), order.getOrderStatus(), order.getPayStatus(), order.getCompanyPayAmount());
        if (StrUtil.isNotBlank(memberId) && !StrUtil.equals(memberId, order.getMemberId())) {
            throw new BizErrorException("无权取消该订单");
        }
        if (!ORDER_STATUS_PENDING_PAY.equals(order.getOrderStatus()) || !PAY_STATUS_UNPAID.equals(order.getPayStatus())) {
            throw new BizErrorException("仅待支付订单可取消");
        }
        validateCancelRule(order);
        List<FoodMealOrderItem> items = listOrderItems(order.getId());
        releaseSoldQuantity(items);
        if (money(order.getCompanyPayAmount()).compareTo(ZERO) > 0) {
            FoodCompanyAccount account = foodCompanyAccountService.lockByCompanyId(order.getCompanyId(), order.getTenantCode());
            foodCompanyAccountLedgerService.unfreezeAmount(account, order, money(order.getCompanyPayAmount()), SOURCE_MINI, memberId);
            FoodCompanySubsidyUse use = selectSubsidyUse(order.getId());
            if (use != null && SUBSIDY_STATUS_FROZEN == use.getStatus()) {
                use.setStatus(SUBSIDY_STATUS_CANCELED);
                use.setCancelTime(LocalDateTime.now());
                updateSubsidyUse(use);
            }
        }
        order.setOrderStatus(ORDER_STATUS_CANCELLED);
        order.setCancelTime(LocalDateTime.now());
        order.setCancelReason(StrUtil.blankToDefault(dto.getCancelReason(), "用户取消"));
        updateOrder(order);
        syncTradeOrderCancel(order);
        return toOrderResult(order, null);
    }

    @Transactional(readOnly = false)
    public FoodCompanyMealOrderResultVO applyRefund(FoodMealOrderRefundApplyDTO dto, String memberId) {
        FoodMealOrder order = foodMealOrderService.lockById(dto.getOrderId(), WebFrameworkUtils.getLoginUserTenantCode());
        if (order == null || !ORDER_TYPE_COMPANY.equals(order.getOrderType())) {
            throw new BizErrorException("企业团餐订单不存在");
        }
        if (StrUtil.isNotBlank(memberId) && !StrUtil.equals(memberId, order.getMemberId())) {
            throw new BizErrorException("无权申请退款");
        }
        if (ORDER_STATUS_REFUNDING.equals(order.getOrderStatus())) {
            log.info("企业团餐退款申请幂等返回 orderId={}, orderNo={}, orderStatus={}", order.getId(), order.getOrderNo(), order.getOrderStatus());
            return toOrderResult(order, null);
        }
        if (!ORDER_STATUS_PAID.equals(order.getOrderStatus()) || !PAY_STATUS_PAID.equals(order.getPayStatus())) {
            log.warn("企业团餐退款申请状态非法 orderId={}, orderNo={}, orderStatus={}, payStatus={}",
                    order.getId(), order.getOrderNo(), order.getOrderStatus(), order.getPayStatus());
            throw new BizErrorException("仅已支付订单可申请退款");
        }
        validateCancelRule(order);
        order.setOrderStatus(ORDER_STATUS_REFUNDING);
        order.setRefundApplyTime(LocalDateTime.now());
        order.setRefundReason(StrUtil.blankToDefault(dto.getRefundReason(), "用户申请退款"));
        updateOrder(order);
        executeRefundAfterMarkRefunding(order);
        log.info("企业团餐退款申请已受理 orderId={}, orderNo={}, orderStatus={}, refundReason={}",
                order.getId(), order.getOrderNo(), order.getOrderStatus(), order.getRefundReason());
        return toOrderResult(order, null);
    }

    /**
     * 管理端发起企业团餐微信退款（仅退员工自付差额；企业补贴在 SUCCESS 回调内回滚）。
     *
     * @return 支付退款单主键
     */
    @Transactional(readOnly = false)
    public String triggerCompanyMealRefund(FoodPersonalMealRefundTriggerDTO dto) {
        if (dto == null || StrUtil.isBlank(dto.getOrderId())) {
            throw new BizErrorException("订单ID不能为空");
        }
        FoodMealOrder order = foodMealOrderService.lockById(dto.getOrderId().trim(), WebFrameworkUtils.getLoginUserTenantCode());
        if (order == null || !ORDER_TYPE_COMPANY.equals(order.getOrderType())) {
            throw new BizErrorException("企业团餐订单不存在");
        }
        assertCompanyRefundable(order);
        if (!ORDER_STATUS_REFUNDING.equals(order.getOrderStatus())) {
            order.setOrderStatus(ORDER_STATUS_REFUNDING);
            order.setRefundApplyTime(LocalDateTime.now());
            order.setRefundReason(StrUtil.blankToDefault(dto.getRefundReason(), "企业团餐退款"));
            updateOrder(order);
        }
        BigDecimal userPay = money(order.getUserPayAmount());
        if (userPay.compareTo(ZERO) <= 0) {
            foodCompanyMealRefundCoordinator.applyRefundSuccess(order.getId(), ZERO, LocalDateTime.now());
            return null;
        }
        BigDecimal refundAmount = dto.getRefundAmount();
        if (refundAmount == null || refundAmount.compareTo(BigDecimal.ZERO) <= 0) {
            refundAmount = userPay;
        }
        PayRefundCreateDTO refundDto = new PayRefundCreateDTO();
        refundDto.setBizScene(PayRecordBizSceneEnum.FOOD_COMPANY_MEAL_ORDER.getCode());
        refundDto.setBizOrderId(order.getId());
        refundDto.setRefundAmount(refundAmount);
        refundDto.setReason(StrUtil.blankToDefault(dto.getRefundReason(), "企业团餐退款"));
        refundDto.setOperatorId(dto.getOperatorId());
        return payRefundApi.createAndExecute(refundDto);
    }

    /**
     * 管理端退款完成补偿：渠道已退款成功但业务未关单时手工同步。
     */
    @Transactional(readOnly = false)
    public void confirmCompanyMealRefund(FoodPersonalMealRefundDTO dto) {
        if (dto == null || StrUtil.isBlank(dto.getOrderId())) {
            throw new BizErrorException("订单ID不能为空");
        }
        FoodMealOrder order = foodMealOrderService.get(dto.getOrderId().trim());
        if (order == null || !ORDER_TYPE_COMPANY.equals(order.getOrderType())) {
            throw new BizErrorException("企业团餐订单不存在");
        }
        foodCompanyMealRefundCoordinator.applyRefundSuccess(
                order.getId(),
                null,
                LocalDateTime.now());
    }

    @Transactional(readOnly = false)
    public FoodCompanyMealOrderResultVO handleRefundSuccess(FoodMealOrderRefundSuccessDTO dto) {
        FoodMealOrder order = lockOrderForRefund(dto);
        if (order == null) {
            throw new BizErrorException("企业团餐订单不存在");
        }
        if (!ORDER_TYPE_COMPANY.equals(order.getOrderType())) {
            throw new BizErrorException("订单类型不匹配");
        }
        if (ORDER_STATUS_REFUNDED.equals(order.getOrderStatus()) || PAY_STATUS_REFUNDED.equals(order.getPayStatus())) {
            return toOrderResult(order, null);
        }
        if (!ORDER_STATUS_REFUNDING.equals(order.getOrderStatus()) && !ORDER_STATUS_PAID.equals(order.getOrderStatus())) {
            throw new BizErrorException("当前订单不可退款确认");
        }
        foodCompanyMealRefundCoordinator.applyRefundSuccess(dto);
        return toOrderResult(foodMealOrderService.get(order.getId()), null);
    }

    @Transactional(readOnly = false)
    public FoodCompanyMealOrderResultVO verify(FoodMealOrderVerifyDTO dto) {
        FoodMealOrder order = foodMealOrderService.lockById(dto.getOrderId(), WebFrameworkUtils.getLoginUserTenantCode());
        if (order == null || !ORDER_TYPE_COMPANY.equals(order.getOrderType())) {
            throw new BizErrorException("企业团餐订单不存在");
        }
        if (ORDER_STATUS_PICKED.equals(order.getOrderStatus())) {
            return toOrderResult(order, null);
        }
        if (!ORDER_STATUS_PAID.equals(order.getOrderStatus()) || !PAY_STATUS_PAID.equals(order.getPayStatus())) {
            throw new BizErrorException("仅已支付订单可核销取餐");
        }
        order.setOrderStatus(ORDER_STATUS_PICKED);
        order.setVerifyTime(LocalDateTime.now());
        order.setVerifyUserId(StrUtil.blankToDefault(dto.getVerifyUserId(), WebFrameworkUtils.getLoginUserId()));
        order.setVerifyUserName(StrUtil.blankToDefault(dto.getVerifyUserName(), WebFrameworkUtils.getLoginUserName()));
        updateOrder(order);
        syncTradeOrderMarkClosed(order);
        triggerProfitSharingOnVerify(order.getId());
        return toOrderResult(order, null);
    }

    public Page<FoodMealOrder> findOrderPage(Page<FoodMealOrder> page, FoodMealOrderQueryVO query) {
        return foodMealOrderService.findOrderPage(page, query != null ? query : new FoodMealOrderQueryVO());
    }

    public PageRespVO<FoodCompanyMealOrderResultVO> findMyCompanyOrders(String memberId, FoodMealOrderQueryVO query) {
        FoodMealOrderQueryVO q = query != null ? query : new FoodMealOrderQueryVO();
        q.setMemberId(memberId);
        q.setOrderType(ORDER_TYPE_COMPANY);
        Page<FoodMealOrder> page = new Page<>(q);
        Page<FoodMealOrder> result = foodMealOrderService.findOrderPage(page, q);
        PageRespVO<FoodCompanyMealOrderResultVO> resp = new PageRespVO<>();
        resp.setTotal(result.getTotal());
        resp.setPageNo(result.getPageNo());
        resp.setPageSize(result.getPageSize());
        resp.setRecords(result.getRecords().stream()
            .map(order -> toOrderResult(order, null, listOrderItems(order.getId())))
            .collect(Collectors.toList()));
        return resp;
    }

    public FoodCompanyMealOrderResultVO getCompanyOrderDetail(String orderId, String memberId) {
        FoodMealOrder order = foodMealOrderService.get(orderId);
        if (order == null || !DEL_NORMAL.equals(order.getDelFlag()) || !ORDER_TYPE_COMPANY.equals(order.getOrderType())) {
            return null;
        }
        if (StrUtil.isNotBlank(memberId) && !StrUtil.equals(memberId, order.getMemberId())) {
            return null;
        }
        return toOrderResult(order, null, listOrderItems(orderId));
    }

    private FoodCompanyMealTrialVO buildTrial(FoodCompanyMealTrialDTO dto, EmployeeContext context, FoodMealMenu menu, List<MealOrderRequestItem> requestItems) {
        FoodCompanySubsidyRule rule = chooseEffectiveRule(dto.getCompanyId(), dto.getMealType(), LocalDateTime.now());

        BigDecimal originalAmount = ZERO;
        List<FoodCompanyMealTrialVO.Item> itemVOList = new ArrayList<>();
        for (MealOrderRequestItem requestItem : requestItems) {
            FoodMealMenuItem menuItem = requestItem.menuItem();
            FoodMealMenuItemSpec spec = requestItem.spec();
            int quantity = requestItem.quantity();
            BigDecimal itemOriginalAmount = money(requestItem.salePrice()).multiply(BigDecimal.valueOf(quantity));
            itemOriginalAmount = money(itemOriginalAmount);
            originalAmount = originalAmount.add(itemOriginalAmount);

            FoodCompanyMealTrialVO.Item itemVO = new FoodCompanyMealTrialVO.Item();
            itemVO.setMenuItemId(menuItem.getId());
            fillTrialSpecSnapshot(itemVO, spec);
            itemVO.setItemType(menuItem.getItemType());
            itemVO.setItemName(menuItem.getItemName());
            itemVO.setItemImage(menuItem.getItemImage());
            itemVO.setSalePrice(money(requestItem.salePrice()));
            itemVO.setQuantity(quantity);
            itemVO.setOriginalAmount(itemOriginalAmount);
            itemVOList.add(itemVO);
        }
        originalAmount = money(originalAmount);
        BigDecimal discountRate = rule == null || rule.getDiscountRate() == null ? ONE : rule.getDiscountRate();
        BigDecimal payableAmount = money(originalAmount.multiply(discountRate));
        BigDecimal discountAmount = money(originalAmount.subtract(payableAmount));
        BigDecimal dailyUsedAmount = sumUsedAmount(context.employee().getId(), dto.getCompanyId(), dto.getMealDate(), null);
        BigDecimal mealUsedAmount = sumUsedAmount(context.employee().getId(), dto.getCompanyId(), dto.getMealDate(), dto.getMealType());
        BigDecimal companyPayAmount = calculateCompanyPayAmount(rule, payableAmount, dailyUsedAmount, mealUsedAmount);
        BigDecimal employeePayAmount = money(payableAmount.subtract(companyPayAmount));

        fillItemTrialAmounts(itemVOList, payableAmount, companyPayAmount, discountRate);

        FoodCompanyMealTrialVO vo = new FoodCompanyMealTrialVO();
        vo.setCompanyId(context.company().getId());
        vo.setCompanyName(context.company().getCompanyName());
        vo.setEmployeeId(context.employee().getId());
        vo.setEmployeeName(context.employee().getEmployeeName());
        if (context.account() != null) {
            vo.setAccountId(context.account().getId());
            vo.setAccountAvailableBalance(money(context.account().getAvailableBalance()));
        }
        vo.setMenuId(menu.getId());
        vo.setMealDate(dto.getMealDate());
        vo.setMealType(dto.getMealType());
        vo.setOriginalAmount(originalAmount);
        vo.setDiscountAmount(discountAmount);
        vo.setPayableAmount(payableAmount);
        vo.setCompanyPayAmount(companyPayAmount);
        vo.setEmployeePayAmount(employeePayAmount);
        vo.setSubsidyRule(toRuleSummary(rule));
        vo.setDailyUsedAmount(dailyUsedAmount);
        vo.setMealUsedAmount(mealUsedAmount);
        vo.setDailyRemainAmount(remain(rule == null ? null : rule.getDailyLimitAmount(), dailyUsedAmount));
        vo.setMealRemainAmount(remain(rule == null ? null : rule.getMealLimitAmount(), mealUsedAmount));
        vo.setItems(itemVOList);
        return vo;
    }

    private void fillTrialSpecSnapshot(FoodCompanyMealTrialVO.Item item, FoodMealMenuItemSpec spec) {
        if (spec == null) {
            return;
        }
        item.setSpecId(spec.getId());
        item.setSpecName(spec.getSpecName());
        item.setSpecDescription(spec.getSpecDescription());
    }

    private void fillOrderItemSpecSnapshot(FoodMealOrderItem orderItem, FoodMealMenuItemSpec spec) {
        if (spec == null) {
            return;
        }
        orderItem.setSpecId(spec.getId());
        orderItem.setSpecName(spec.getSpecName());
        orderItem.setSpecDescription(spec.getSpecDescription());
    }

    private EmployeeContext getValidEmployeeContext(String memberId, String companyId) {
        if (StrUtil.isBlank(memberId)) {
            throw new BizErrorException("会员ID不能为空");
        }
        if (StrUtil.isBlank(companyId)) {
            throw new BizErrorException("企业ID不能为空");
        }
        FoodCompany company = foodCompanyService.getById(companyId);
        if (company == null || !DEL_NORMAL.equals(company.getDelFlag()) || !Integer.valueOf(STATUS_ENABLED).equals(company.getStatus())) {
            throw new BizErrorException("企业不存在或已停用");
        }
        FoodCompanyEmployee employee = foodCompanyEmployeeService.getBoundEmployee(companyId, memberId);
        if (employee == null) {
            throw new BizErrorException("当前会员不是有效企业员工");
        }
        FoodCompanyAccount account = foodCompanyAccountService.getEnabledByCompanyId(companyId, WebFrameworkUtils.getLoginUserTenantCode());
        return new EmployeeContext(company, employee, account);
    }

    private List<FoodMealMenu> listPublishedCompanyMenus(String serviceAreaId, LocalDate mealDate, String mealType, String companyId) {
        return foodMealMenuService.listPublishedMenus(serviceAreaId, mealDate, mealType,
                Set.of(MENU_SCOPE_COMPANY, MENU_SCOPE_ALL), mealTypeCandidates(mealType)).stream()
            .filter(menu -> companyMenuAvailable(menu, companyId))
            .collect(Collectors.toList());
    }

    private boolean companyMenuAvailable(FoodMealMenu menu, String companyId) {
        return MENU_SCOPE_ALL.equals(menu.getScopeType())
            || (MENU_SCOPE_COMPANY.equals(menu.getScopeType()) && StrUtil.equals(menu.getCompanyId(), companyId));
    }

    private FoodMealMenu chooseCompanyMenu(List<FoodMealMenu> menus, String companyId) {
        return menus.stream()
            .filter(menu -> MENU_SCOPE_COMPANY.equals(menu.getScopeType()) && StrUtil.equals(menu.getCompanyId(), companyId))
            .findFirst()
            .orElse(menus.get(0));
    }

    private FoodMealMenu getValidMenu(FoodCompanyMealTrialDTO dto, FoodCompany company) {
        FoodMealMenu menu = foodMealMenuService.get(dto.getMenuId());
        if (menu == null || !DEL_NORMAL.equals(menu.getDelFlag()) || !MENU_STATUS_PUBLISHED.equals(menu.getPublishStatus())) {
            throw new BizErrorException("菜单不存在或未发布");
        }
        if (!dto.getMealDate().equals(menu.getMenuDate()) || !equalsMealType(menu.getMealType(), dto.getMealType())) {
            throw new BizErrorException("菜单日期或餐别不匹配");
        }
        if (MENU_SCOPE_COMPANY.equals(menu.getScopeType()) && !StrUtil.equals(company.getId(), menu.getCompanyId())) {
            throw new BizErrorException("无权使用该企业专属菜单");
        }
        if (!MENU_SCOPE_COMPANY.equals(menu.getScopeType()) && !MENU_SCOPE_ALL.equals(menu.getScopeType())) {
            throw new BizErrorException("该菜单不适用于企业团餐");
        }
        return menu;
    }

    private void validateMealRule(String serviceAreaId, String mealType, LocalDate mealDate) {
        MealOrderableResult result = checkMealOrderable(serviceAreaId, mealType, mealDate);
        if (!result.orderable()) {
            throw new BizErrorException(result.reason());
        }
    }

    private MealOrderableResult checkMealOrderable(String serviceAreaId, String mealType, LocalDate mealDate) {
        FoodMealRule rule = chooseMealRule(serviceAreaId, mealType);
        if (rule == null) {
            return MealOrderableResult.ok(null);
        }
        String deadlineTime = rule.getFixedDeadlineTime() == null ? null : rule.getFixedDeadlineTime().toString();
        LocalDate today = LocalDate.now();
        if (mealDate == null) {
            return MealOrderableResult.fail(DATE_STATUS_INVALID_DATE, "订餐日期不能为空", deadlineTime);
        }
        if (mealDate.isBefore(today)) {
            return MealOrderableResult.fail(DATE_STATUS_EXPIRED, "订餐日期不能早于当前日期", deadlineTime);
        }
        Integer bookableFutureDays = rule.getBookableFutureDays();
        if (bookableFutureDays != null && bookableFutureDays >= 0 && mealDate.isAfter(today.plusDays(bookableFutureDays))) {
            return MealOrderableResult.fail(DATE_STATUS_OUT_OF_RANGE, "订餐日期超出可预订范围", deadlineTime);
        }
        LocalDateTime now = LocalDateTime.now();
        if (rule.getAdvanceDays() != null && rule.getAdvanceDays() > 0 && mealDate.isBefore(now.toLocalDate().plusDays(rule.getAdvanceDays()))) {
            return MealOrderableResult.fail(DATE_STATUS_NOT_STARTED, "未到可预订时间", deadlineTime);
        }
        if (rule.getAdvanceHours() != null && rule.getAdvanceHours() > 0 && today.equals(mealDate) && now.plusHours(rule.getAdvanceHours()).isAfter(mealDate.plusDays(1).atStartOfDay())) {
            return MealOrderableResult.fail(DATE_STATUS_NOT_STARTED, "未到可预订时间", deadlineTime);
        }
        if (rule.getFixedDeadlineTime() != null && today.equals(mealDate) && now.toLocalTime().isAfter(rule.getFixedDeadlineTime())) {
            return MealOrderableResult.fail(DATE_STATUS_AFTER_DEADLINE, "已超过" + mealTypeLabel(normalizeMealType(mealType)) + "预约截止时间", deadlineTime);
        }
        return MealOrderableResult.ok(deadlineTime);
    }

    private FoodMealRule chooseMealRule(String serviceAreaId, String mealType) {
        List<FoodMealRule> rules = foodMealRuleService.listEnabledByScopeTypes(Set.of(RULE_SCOPE_ALL, RULE_SCOPE_COMPANY));
        if (CollUtil.isEmpty(rules)) {
            return null;
        }
        return rules.stream()
            .filter(rule -> StrUtil.isBlank(rule.getServiceAreaId()) || StrUtil.equals(rule.getServiceAreaId(), serviceAreaId))
            .filter(rule -> StrUtil.isBlank(rule.getMealType()) || equalsMealType(rule.getMealType(), mealType))
            .max(Comparator.comparingInt(rule -> ruleScore(rule, serviceAreaId, mealType)))
            .orElse(null);
    }

    private int ruleScore(FoodMealRule rule, String serviceAreaId, String mealType) {
        int score = 0;
        if (StrUtil.equals(rule.getServiceAreaId(), serviceAreaId)) {
            score += 10;
        }
        if (equalsMealType(rule.getMealType(), mealType)) {
            score += 5;
        }
        if (RULE_SCOPE_COMPANY.equals(rule.getScopeType())) {
            score += 1;
        }
        return score;
    }

    private List<FoodMealMenuItem> getValidMenuItems(FoodCompanyMealTrialDTO dto, boolean forUpdate) {
        Set<String> requestIds = dto.getItems().stream()
            .map(FoodCompanyMealTrialDTO.Item::getMenuItemId)
            .collect(Collectors.toCollection(HashSet::new));
        List<FoodMealMenuItem> items = foodMealMenuItemService.listByMenuIdAndIds(dto.getMenuId(), requestIds, forUpdate);
        if (items.size() != requestIds.size()) {
            throw new BizErrorException("菜单明细不存在或不属于当前菜单");
        }
        if (items.stream().anyMatch(item -> !isPublishedMenuItem(item))) {
            throw new BizErrorException("菜单明细未发布或已下线");
        }
        return items;
    }

    private List<MealOrderRequestItem> buildRequestItems(FoodCompanyMealTrialDTO dto, FoodMealMenu menu,
                                                         List<FoodMealMenuItem> menuItems, boolean forUpdate) {
        Map<String, FoodMealMenuItem> menuItemMap = menuItems.stream().collect(Collectors.toMap(FoodMealMenuItem::getId, item -> item));
        Map<String, List<FoodMealMenuItemSpec>> specsByMenuItemId = listSpecsByMenuItemIds(menuItemMap.keySet());
        Set<String> requestSpecIds = dto.getItems().stream()
            .map(FoodCompanyMealTrialDTO.Item::getSpecId)
            .filter(StrUtil::isNotBlank)
            .collect(Collectors.toCollection(HashSet::new));
        Map<String, FoodMealMenuItemSpec> specMap = getSpecMap(requestSpecIds, forUpdate);
        Map<String, FoodCompanyMealTrialDTO.Item> itemMap = new HashMap<>();
        Map<String, Integer> quantityMap = new HashMap<>();
        for (FoodCompanyMealTrialDTO.Item item : dto.getItems()) {
            int quantity = item.getQuantity() == null ? 0 : item.getQuantity();
            if (quantity <= 0) {
                throw new BizErrorException("订餐数量必须大于0");
            }
            String key = requestItemKey(item.getMenuItemId(), item.getSpecId());
            itemMap.putIfAbsent(key, item);
            quantityMap.merge(key, quantity, Integer::sum);
        }
        List<MealOrderRequestItem> requestItems = new ArrayList<>();
        for (Map.Entry<String, FoodCompanyMealTrialDTO.Item> entry : itemMap.entrySet()) {
            FoodCompanyMealTrialDTO.Item item = entry.getValue();
            FoodMealMenuItem menuItem = menuItemMap.get(item.getMenuItemId());
            if (menuItem == null) {
                throw new BizErrorException("菜单明细不存在");
            }
            List<FoodMealMenuItemSpec> itemSpecs = specsByMenuItemId.getOrDefault(menuItem.getId(), List.of());
            FoodMealMenuItemSpec spec = null;
            if (CollUtil.isNotEmpty(itemSpecs)) {
                if (StrUtil.isBlank(item.getSpecId())) {
                    throw new BizErrorException("请选择菜品规格");
                }
                spec = specMap.get(item.getSpecId());
                validateSpecBelongsToMenuItem(spec, menuItem, menu);
            } else if (StrUtil.isNotBlank(item.getSpecId())) {
                throw new BizErrorException("菜单明细不存在该规格");
            }
            int quantity = quantityMap.getOrDefault(entry.getKey(), 0);
            validateQuantity(menuItem, spec, quantity);
            validateUserLimit(menuItem, spec, dto.getMemberId(), quantity);
            requestItems.add(new MealOrderRequestItem(menuItem, spec, quantity, spec == null ? money(menuItem.getSalePrice()) : money(spec.getSalePrice())));
        }
        return requestItems;
    }

    private String requestItemKey(String menuItemId, String specId) {
        return menuItemId + "#" + StrUtil.blankToDefault(specId, "");
    }

    private String requestItemKey(MealOrderRequestItem item) {
        return requestItemKey(item.menuItem().getId(), item.spec() == null ? null : item.spec().getId());
    }

    private String trialItemKey(FoodCompanyMealTrialVO.Item item) {
        return requestItemKey(item.getMenuItemId(), item.getSpecId());
    }

    private Map<String, FoodMealMenuItemSpec> getSpecMap(Set<String> specIds, boolean forUpdate) {
        if (CollUtil.isEmpty(specIds)) {
            return Map.of();
        }
        List<FoodMealMenuItemSpec> specs = foodMealMenuItemSpecService.listByIds(specIds, forUpdate);
        if (specs.size() != specIds.size()) {
            throw new BizErrorException("菜单明细规格不存在");
        }
        return specs.stream().collect(Collectors.toMap(FoodMealMenuItemSpec::getId, spec -> spec));
    }

    private Map<String, List<FoodMealMenuItemSpec>> listSpecsByMenuItemIds(Set<String> menuItemIds) {
        if (CollUtil.isEmpty(menuItemIds)) {
            return Map.of();
        }
        return foodMealMenuItemSpecService.listByMenuItemIds(menuItemIds).stream()
            .collect(Collectors.groupingBy(FoodMealMenuItemSpec::getMenuItemId));
    }

    private void validateSpecBelongsToMenuItem(FoodMealMenuItemSpec spec, FoodMealMenuItem menuItem, FoodMealMenu menu) {
        if (spec == null
                || !StrUtil.equals(spec.getMenuItemId(), menuItem.getId())
                || !StrUtil.equals(spec.getMenuId(), menu.getId())
                || (StrUtil.isNotBlank(menu.getServiceAreaId())
                && StrUtil.isNotBlank(spec.getServiceAreaId())
                && !StrUtil.equals(spec.getServiceAreaId(), menu.getServiceAreaId()))) {
            throw new BizErrorException("菜单明细规格不属于当前菜单");
        }
    }

    private void validateQuantity(FoodMealMenuItem menuItem, FoodMealMenuItemSpec spec, Integer quantity) {
        if (quantity == null || quantity <= 0) {
            throw new BizErrorException("订餐数量必须大于0");
        }
        if (menuItem.getLimitQuantityPerUser() != null && quantity > menuItem.getLimitQuantityPerUser()) {
            throw new BizErrorException("订餐数量超过单人限购数量");
        }
        if (spec != null && spec.getLimitQuantityPerUser() != null && quantity > spec.getLimitQuantityPerUser()) {
            throw new BizErrorException("订餐数量超过规格单人限购数量");
        }
        if (menuItem.getStockQuantity() != null) {
            int soldQuantity = menuItem.getSoldQuantity() == null ? 0 : menuItem.getSoldQuantity();
            if (quantity > menuItem.getStockQuantity() - soldQuantity) {
                throw new BizErrorException("菜单库存不足");
            }
        }
        if (spec != null && spec.getStockQuantity() != null) {
            int soldQuantity = spec.getSoldQuantity() == null ? 0 : spec.getSoldQuantity();
            if (quantity > spec.getStockQuantity() - soldQuantity) {
                throw new BizErrorException("规格库存不足");
            }
        }
    }

    private void validateUserLimit(FoodMealMenuItem menuItem, FoodMealMenuItemSpec spec, String memberId, int quantity) {
        if (menuItem.getLimitQuantityPerUser() != null) {
            int orderedQuantity = sumOrderedQuantity(memberId, menuItem.getMenuId(), menuItem.getId(), null);
            if (orderedQuantity + quantity > menuItem.getLimitQuantityPerUser()) {
                throw new BizErrorException("订餐数量超过单人限购数量");
            }
        }
        if (spec != null && spec.getLimitQuantityPerUser() != null) {
            int orderedQuantity = sumOrderedQuantity(memberId, menuItem.getMenuId(), menuItem.getId(), spec.getId());
            if (orderedQuantity + quantity > spec.getLimitQuantityPerUser()) {
                throw new BizErrorException("订餐数量超过规格单人限购数量");
            }
        }
    }

    private int sumOrderedQuantity(String memberId, String menuId, String menuItemId, String specId) {
        List<FoodMealOrder> orders = foodMealOrderService.listActiveByMemberIdAndMenuId(memberId, menuId,
            List.of(ORDER_STATUS_PENDING_PAY, ORDER_STATUS_PAID));
        if (CollUtil.isEmpty(orders)) {
            return 0;
        }
        Set<String> orderIds = orders.stream().map(FoodMealOrder::getId).collect(Collectors.toSet());
        List<FoodMealOrderItem> items = foodMealOrderItemService.listByOrderIdsAndMenuItemId(orderIds, menuItemId);
        return items.stream()
            .filter(item -> StrUtil.isBlank(specId) || StrUtil.equals(specId, item.getSpecId()))
            .map(FoodMealOrderItem::getQuantity)
            .filter(q -> q != null)
            .reduce(0, Integer::sum);
    }

    private FoodCompanySubsidyRule chooseEffectiveRule(String companyId, String mealType, LocalDateTime now) {
        List<FoodCompanySubsidyRule> rules = listEffectiveRules(companyId, mealType, now);
        if (CollUtil.isEmpty(rules)) {
            return null;
        }
        return rules.get(0);
    }

    private List<FoodCompanySubsidyRule> listEffectiveRules(String companyId, String mealType, LocalDateTime now) {
        List<FoodCompanySubsidyRule> rules = foodCompanySubsidyRuleService.listEffectiveRules(companyId, now);
        return rules.stream()
            .filter(rule -> StrUtil.isBlank(mealType) || matchMealType(rule.getMealTypes(), mealType))
            .collect(Collectors.toList());
    }

    private BigDecimal sumUsedAmount(String employeeId, String companyId, LocalDate mealDate, String mealType) {
        return foodCompanySubsidyUseService.sumUsedAmount(employeeId, companyId, mealDate,
            StrUtil.isNotBlank(mealType) ? normalizeMealType(mealType) : mealType);
    }

    private BigDecimal calculateCompanyPayAmount(FoodCompanySubsidyRule rule, BigDecimal payableAmount, BigDecimal dailyUsedAmount, BigDecimal mealUsedAmount) {
        if (rule == null || RULE_TYPE_DISCOUNT.equals(rule.getRuleType())) {
            return ZERO;
        }
        BigDecimal baseAmount = RULE_TYPE_FULL.equals(rule.getRuleType()) ? payableAmount : min(payableAmount, money(rule.getSubsidyAmount()));
        baseAmount = applyLimit(baseAmount, rule.getDailyLimitAmount(), dailyUsedAmount);
        baseAmount = applyLimit(baseAmount, rule.getMealLimitAmount(), mealUsedAmount);
        return money(min(payableAmount, baseAmount));
    }

    private BigDecimal applyLimit(BigDecimal amount, BigDecimal limitAmount, BigDecimal usedAmount) {
        if (limitAmount == null) {
            return amount;
        }
        BigDecimal remainAmount = money(limitAmount).subtract(money(usedAmount));
        if (remainAmount.compareTo(BigDecimal.ZERO) <= 0) {
            return ZERO;
        }
        return min(amount, remainAmount);
    }

    private void fillItemTrialAmounts(List<FoodCompanyMealTrialVO.Item> items, BigDecimal payableAmount, BigDecimal companyPayAmount, BigDecimal discountRate) {
        BigDecimal allocatedCompanyPay = ZERO;
        BigDecimal allocatedPayable = ZERO;
        for (int i = 0; i < items.size(); i++) {
            FoodCompanyMealTrialVO.Item item = items.get(i);
            BigDecimal itemPayableAmount;
            if (i == items.size() - 1) {
                itemPayableAmount = money(payableAmount.subtract(allocatedPayable));
            } else {
                itemPayableAmount = money(item.getOriginalAmount().multiply(discountRate));
                allocatedPayable = allocatedPayable.add(itemPayableAmount);
            }
            BigDecimal itemDiscountAmount = money(item.getOriginalAmount().subtract(itemPayableAmount));
            BigDecimal itemCompanyPayAmount;
            if (i == items.size() - 1) {
                itemCompanyPayAmount = money(companyPayAmount.subtract(allocatedCompanyPay));
            } else if (payableAmount.compareTo(BigDecimal.ZERO) == 0) {
                itemCompanyPayAmount = ZERO;
            } else {
                itemCompanyPayAmount = money(companyPayAmount.multiply(itemPayableAmount).divide(payableAmount, 2, RoundingMode.HALF_UP));
                allocatedCompanyPay = allocatedCompanyPay.add(itemCompanyPayAmount);
            }
            item.setDiscountAmount(itemDiscountAmount);
            item.setPayableAmount(itemPayableAmount);
            item.setCompanyPayAmount(itemCompanyPayAmount);
            item.setEmployeePayAmount(money(itemPayableAmount.subtract(itemCompanyPayAmount)));
        }
    }

    private FoodCompanyAccount lockAccountIfNecessary(FoodCompanyAccount account, BigDecimal companyPayAmount) {
        if (companyPayAmount.compareTo(ZERO) <= 0) {
            return account;
        }
        if (account == null) {
            throw new BizErrorException("企业账户不存在");
        }
        FoodCompanyAccount locked = foodCompanyAccountService.lockById(account.getId(), account.getTenantCode());
        if (locked == null || !Integer.valueOf(STATUS_ENABLED).equals(locked.getStatus())) {
            throw new BizErrorException("企业账户不存在或未启用");
        }
        return locked;
    }

    private FoodMealOrder buildOrder(FoodCompanyMealOrderCreateDTO dto, EmployeeContext context, FoodMealMenu menu, FoodCompanyMealTrialVO trial) {
        FoodMealOrder order = new FoodMealOrder();
        order.setId(IdUtil.getSnowflakeNextIdStr());
        order.setOrderNo(generateOrderNo());
        order.setOrderType(ORDER_TYPE_COMPANY);
        order.setServiceAreaId(menu.getServiceAreaId());
        order.setServiceAreaName(serviceAreaName(menu));
        order.setRestaurantId(menu.getRestaurantId());
        order.setRestaurantName(order.getServiceAreaName());
        order.setMenuId(menu.getId());
        order.setMenuDate(menu.getMenuDate());
        order.setMealType(normalizeMealType(menu.getMealType()));
        order.setMemberId(context.employee().getMemberId());
        order.setMemberName(context.employee().getEmployeeName());
        order.setMemberPhone(context.employee().getMobile());
        order.setCompanyId(context.company().getId());
        order.setCompanyName(context.company().getCompanyName());
        order.setEmployeeId(context.employee().getId());
        order.setTotalAmount(trial.getOriginalAmount());
        order.setDiscountAmount(trial.getDiscountAmount());
        order.setCompanyPayAmount(trial.getCompanyPayAmount());
        order.setUserPayAmount(trial.getEmployeePayAmount());
        order.setRefundAmount(ZERO);
        order.setOrderStatus(trial.getEmployeePayAmount().compareTo(ZERO) > 0 ? ORDER_STATUS_PENDING_PAY : ORDER_STATUS_PAID);
        order.setPayStatus(trial.getEmployeePayAmount().compareTo(ZERO) > 0 ? PAY_STATUS_UNPAID : PAY_STATUS_PAID);
        order.setPickupCode(generatePickupCode());
        if (trial.getEmployeePayAmount().compareTo(ZERO) == 0) {
            order.setPayTime(LocalDateTime.now());
        }
        return order;
    }

    private void fillDeliverySnapshot(FoodMealOrder order, MallMemberAddress address, String deliveryType, String remark) {
        order.setDeliveryType(StrUtil.blankToDefault(deliveryType, DELIVERY_TYPE_PICKUP));
        if (address != null) {
            order.setAddressId(address.getId());
            order.setReceiverName(address.getReceiverName());
            order.setReceiverMobile(address.getReceiverMobile());
            order.setProvince(address.getProvince());
            order.setProvinceCode(address.getProvinceCode());
            order.setCity(address.getCity());
            order.setCityCode(address.getCityCode());
            order.setDistrict(address.getDistrict());
            order.setDistrictCode(address.getDistrictCode());
            order.setDetailAddress(address.getDetailAddress());
            order.setFullAddress(mallMemberAddressApplicationService.getFullAddress(address));
            order.setLongitude(address.getLongitude());
            order.setLatitude(address.getLatitude());
        } else {
            order.setFullAddress(serviceAreaPickupLocation(order.getServiceAreaId()));
        }
        order.setRemark(remark);
    }

    private List<FoodMealOrderItem> buildOrderItems(FoodMealOrder order, List<MealOrderRequestItem> requestItems, List<FoodCompanyMealTrialVO.Item> trialItems) {
        Map<String, MealOrderRequestItem> requestItemMap = requestItems.stream()
            .collect(Collectors.toMap(this::requestItemKey, item -> item, (left, right) -> left));
        List<FoodMealOrderItem> items = new ArrayList<>();
        for (FoodCompanyMealTrialVO.Item trialItem : trialItems) {
            MealOrderRequestItem requestItem = requestItemMap.get(trialItemKey(trialItem));
            if (requestItem == null) {
                throw new BizErrorException("菜单明细不存在");
            }
            FoodMealMenuItem menuItem = requestItem.menuItem();
            FoodMealMenuItemSpec spec = requestItem.spec();
            FoodMealOrderItem orderItem = new FoodMealOrderItem();
            orderItem.setId(IdUtil.getSnowflakeNextIdStr());
            orderItem.setOrderId(order.getId());
            orderItem.setOrderNo(order.getOrderNo());
            orderItem.setMenuItemId(menuItem.getId());
            orderItem.setMenuId(menuItem.getMenuId());
            orderItem.setItemType(menuItem.getItemType());
            orderItem.setDishId(menuItem.getDishId());
            orderItem.setPackageId(menuItem.getPackageId());
            orderItem.setItemName(menuItem.getItemName());
            orderItem.setItemImage(menuItem.getItemImage());
            fillOrderItemSpecSnapshot(orderItem, spec);
            orderItem.setSalePrice(trialItem.getSalePrice());
            orderItem.setQuantity(trialItem.getQuantity());
            orderItem.setTotalAmount(trialItem.getOriginalAmount());
            orderItem.setDiscountAmount(trialItem.getDiscountAmount());
            orderItem.setCompanyPayAmount(trialItem.getCompanyPayAmount());
            orderItem.setUserPayAmount(trialItem.getEmployeePayAmount());
            orderItem.setRefundQuantity(0);
            orderItem.setRefundAmount(ZERO);
            items.add(orderItem);
        }
        return items;
    }

    private void increaseSoldQuantity(List<FoodMealOrderItem> items) {
        foodMealMenuItemService.increaseSoldQuantity(menuItemQuantityMap(items), WebFrameworkUtils.getLoginUserTenantCode());
        foodMealMenuItemSpecService.increaseSoldQuantity(specQuantityMap(items), WebFrameworkUtils.getLoginUserTenantCode());
    }

    private void releaseSoldQuantity(List<FoodMealOrderItem> items) {
        foodMealMenuItemService.releaseSoldQuantity(items, WebFrameworkUtils.getLoginUserTenantCode());
        foodMealMenuItemSpecService.releaseSoldQuantity(items, WebFrameworkUtils.getLoginUserTenantCode());
    }

    private Map<String, Integer> menuItemQuantityMap(List<FoodMealOrderItem> items) {
        Map<String, Integer> quantityMap = new HashMap<>();
        for (FoodMealOrderItem item : items) {
            quantityMap.merge(item.getMenuItemId(), item.getQuantity(), Integer::sum);
        }
        return quantityMap;
    }

    private Map<String, Integer> specQuantityMap(List<FoodMealOrderItem> items) {
        Map<String, Integer> quantityMap = new HashMap<>();
        for (FoodMealOrderItem item : items) {
            if (StrUtil.isNotBlank(item.getSpecId())) {
                quantityMap.merge(item.getSpecId(), item.getQuantity(), Integer::sum);
            }
        }
        return quantityMap;
    }

    private void syncTradeOrderOnCreate(FoodMealOrder order, List<FoodMealOrderItem> items) {
        if (order == null || StrUtil.isNotBlank(order.getTradeCenterOrderId())) {
            return;
        }
        TradeOrder existing = findExistingTradeOrder(order);
        if (existing != null) {
            order.setTradeCenterOrderId(existing.getId());
            updateOrder(order);
            return;
        }
        TradeOrderCreateDTO dto = new TradeOrderCreateDTO();
        dto.setMemberId(order.getMemberId());
        dto.setBizType(TradeBizTypeEnum.FOOD_COMPANY_MEAL.getValue());
        dto.setTitle("企业团餐-" + mealTypeLabel(order.getMealType()) + "-" + StrUtil.blankToDefault(order.getCompanyName(), "企业"));
        dto.setTotalAmount(money(order.getTotalAmount()));
        dto.setDiscountAmount(money(order.getDiscountAmount()));
        dto.setPayAmount(money(order.getUserPayAmount()));
        dto.setVerticalOrderId(order.getId());
        dto.setTenantCode(order.getTenantCode());
        dto.setTenantId(order.getTenantId());
        Map<String, Object> ext = new HashMap<>();
        ext.put("orderType", "company");
        ext.put("companyId", StrUtil.blankToDefault(order.getCompanyId(), ""));
        ext.put("mealDate", order.getMenuDate() == null ? "" : order.getMenuDate().toString());
        ext.put("mealType", StrUtil.blankToDefault(order.getMealType(), ""));
        ext.put("serviceAreaId", StrUtil.blankToDefault(order.getServiceAreaId(), ""));
        ext.put("serviceAreaName", StrUtil.blankToDefault(order.getServiceAreaName(), ""));
        ext.put("restaurantId", StrUtil.blankToDefault(order.getRestaurantId(), ""));
        ext.put("deliveryType", StrUtil.blankToDefault(order.getDeliveryType(), ""));
        ext.put("addressId", StrUtil.blankToDefault(order.getAddressId(), ""));
        ext.put("fullAddress", StrUtil.blankToDefault(order.getFullAddress(), ""));
        dto.setExtJson(JSONUtil.toJsonStr(ext));
        dto.setLines(buildTradeOrderLines(items));
        String tradeCenterOrderId = tradeOrderFacadeApi.createPendingPayOrder(dto);
        if (StrUtil.isNotBlank(tradeCenterOrderId)) {
            order.setTradeCenterOrderId(tradeCenterOrderId);
            updateOrder(order);
        }
    }

    private List<TradeOrderLineCreateDTO> buildTradeOrderLines(List<FoodMealOrderItem> items) {
        List<TradeOrderLineCreateDTO> lines = new ArrayList<>();
        int lineNo = 1;
        for (FoodMealOrderItem item : items) {
            TradeOrderLineCreateDTO line = new TradeOrderLineCreateDTO();
            line.setLineNo(lineNo++);
            line.setItemTitle(StrUtil.blankToDefault(item.getItemName(), "团餐菜品"));
            line.setSkuRef(item.getMenuItemId());
            line.setResourceRef(StrUtil.blankToDefault(item.getDishId(), item.getPackageId()));
            line.setQuantity(item.getQuantity() == null || item.getQuantity() < 1 ? 1 : item.getQuantity());
            line.setUnitPrice(money(item.getSalePrice()));
            line.setLineAmount(money(item.getTotalAmount()));
            Map<String, Object> snapshot = new HashMap<>();
            snapshot.put("itemImage", StrUtil.blankToDefault(item.getItemImage(), ""));
            line.setSnapshotJson(JSONUtil.toJsonStr(snapshot));
            lines.add(line);
        }
        return lines;
    }

    private TradeOrder findExistingTradeOrder(FoodMealOrder order) {
        TradeOrderQueryVO query = new TradeOrderQueryVO();
        query.setVerticalOrderId(order.getId());
        query.setBizType(TradeBizTypeEnum.FOOD_COMPANY_MEAL.getValue());
        query.setMemberId(order.getMemberId());
        query.setPageNo(1L);
        query.setPageSize(1L);
        Page<TradeOrder> page = tradeOrderFacadeApi.findPage(new Page<>(1L, 1L), query);
        return page.getRecords() == null || page.getRecords().isEmpty() ? null : page.getRecords().get(0);
    }

    private void syncTradeOrderMarkPaid(FoodMealOrder order, String source) {
        ensureTradeOrderLinked(order);
        if (StrUtil.isBlank(order.getTradeCenterOrderId())) {
            return;
        }
        TradeOrder tradeOrder = tradeOrderFacadeApi.get(order.getTradeCenterOrderId());
        if (tradeOrder != null && TradeOrderStatusEnum.PENDING_PAY.getValue().equals(tradeOrder.getStatus())) {
            String payOrderId = StrUtil.isNotBlank(order.getTradeOrderId()) ? order.getTradeOrderId() : "COMPANY_SUBSIDY:" + order.getId();
            String channel = SOURCE_PAY_NOTIFY.equals(source) ? "WECHAT_MINI" : "COMPANY_SUBSIDY";
            tradeOrderFacadeApi.markPaid(order.getTradeCenterOrderId(), payOrderId, channel);
        }
    }

    private void syncTradeOrderCancel(FoodMealOrder order) {
        ensureTradeOrderLinked(order);
        if (StrUtil.isBlank(order.getTradeCenterOrderId())) {
            return;
        }
        tradeOrderFacadeApi.cancelPending(order.getTradeCenterOrderId());
    }

    private void syncTradeOrderMarkClosed(FoodMealOrder order) {
        ensureTradeOrderLinked(order);
        if (StrUtil.isBlank(order.getTradeCenterOrderId())) {
            return;
        }
        tradeOrderFacadeApi.markClosed(order.getTradeCenterOrderId());
    }

    private void ensureTradeOrderLinked(FoodMealOrder order) {
        if (order == null || StrUtil.isNotBlank(order.getTradeCenterOrderId())) {
            return;
        }
        TradeOrder existing = findExistingTradeOrder(order);
        if (existing != null) {
            order.setTradeCenterOrderId(existing.getId());
            updateOrder(order);
        }
    }

    private void markOrderItemsRefunded(List<FoodMealOrderItem> items) {
        foodMealOrderItemService.markRefunded(items, true);
    }

    private List<FoodMealOrderItem> listOrderItems(String orderId) {
        return foodMealOrderItemService.listByOrderId(orderId);
    }

    private FoodCompanySubsidyUse createFrozenSubsidyUse(FoodMealOrder order, EmployeeContext context, FoodCompanyMealTrialVO trial) {
        FoodCompanySubsidyUse use = new FoodCompanySubsidyUse();
        use.setId(IdUtil.getSnowflakeNextIdStr());
        use.setCompanyId(context.company().getId());
        use.setEmployeeId(context.employee().getId());
        use.setMemberId(context.employee().getMemberId());
        use.setRuleId(trial.getSubsidyRule() == null ? null : trial.getSubsidyRule().getRuleId());
        use.setOrderId(order.getId());
        use.setOrderNo(order.getOrderNo());
        use.setMealDate(order.getMenuDate());
        use.setMealType(order.getMealType());
        use.setOriginalAmount(order.getTotalAmount());
        use.setDiscountAmount(order.getDiscountAmount());
        use.setPayableAmount(money(order.getTotalAmount()).subtract(money(order.getDiscountAmount())));
        use.setSubsidyAmount(order.getCompanyPayAmount());
        use.setEmployeePayAmount(order.getUserPayAmount());
        use.setStatus(SUBSIDY_STATUS_FROZEN);
        use.setFrozenTime(LocalDateTime.now());
        insertSubsidyUse(use);
        return use;
    }

    private PayOrderIntentVO createMealPayOrderIntent(FoodMealOrder order, FoodCompanyMealOrderCreateDTO dto) {
        return payOrderApi.createIntentForJsapi(
            PayRecordBizSceneEnum.FOOD_COMPANY_MEAL_ORDER.getCode(),
            order.getId(),
            order.getUserPayAmount(),
            order.getMemberId(),
            StrUtil.blankToDefault(dto.getSubject(), "企业团餐差额支付"),
            StrUtil.blankToDefault(dto.getDescription(), "企业团餐订单差额支付"),
            dto.getNotifyUrl(),
            dto.getChannelUserId(),
            dto.getUserIp(),
            order.getTenantId());
    }

    private void completePaidOrder(FoodMealOrder order, FoodCompanyAccount account, FoodCompanySubsidyUse subsidyUse,
                                   LocalDateTime paidTime, String source, String operatorId) {
        BigDecimal companyPayAmount = money(order.getCompanyPayAmount());
        log.info("企业团餐订单结算开始 orderId={}, orderNo={}, companyId={}, companyPayAmount={}, employeePayAmount={}, source={}, operatorId={}, paidTime={}",
                order.getId(), order.getOrderNo(), order.getCompanyId(), companyPayAmount, order.getUserPayAmount(), source, operatorId, paidTime);
        if (companyPayAmount.compareTo(ZERO) > 0) {
            foodCompanyAccountLedgerService.consumeFrozenAmount(account, order, companyPayAmount, source, operatorId);
            FoodCompanySubsidyUse use = subsidyUse != null ? subsidyUse : selectFrozenSubsidyUse(order.getId());
            if (use != null) {
                use.setStatus(SUBSIDY_STATUS_USED);
                use.setUsedTime(paidTime);
                updateSubsidyUse(use);
                log.info("企业团餐补贴使用记录已核销 orderId={}, orderNo={}, subsidyUseId={}, subsidyAmount={}, status={}, usedTime={}",
                        order.getId(), order.getOrderNo(), use.getId(), use.getSubsidyAmount(), use.getStatus(), use.getUsedTime());
            }
        }
        order.setOrderStatus(ORDER_STATUS_PAID);
        order.setPayStatus(PAY_STATUS_PAID);
        order.setPayTime(paidTime);
        updateOrder(order);
        log.info("企业团餐订单结算完成 orderId={}, orderNo={}, companyId={}, orderStatus={}, payStatus={}, payTime={}",
                order.getId(), order.getOrderNo(), order.getCompanyId(), order.getOrderStatus(), order.getPayStatus(), order.getPayTime());
    }

    private FoodCompanySubsidyUse selectFrozenSubsidyUse(String orderId) {
        return foodCompanySubsidyUseService.getFrozenByOrderId(orderId);
    }

    private FoodCompanySubsidyUse selectSubsidyUse(String orderId) {
        return foodCompanySubsidyUseService.getByOrderId(orderId);
    }

    private FoodMealOrder lockOrderForPaid(FoodCompanyMealOrderPaidDTO dto) {
        if (StrUtil.isNotBlank(dto.getOrderId())) {
            return foodMealOrderService.lockById(dto.getOrderId(), WebFrameworkUtils.getLoginUserTenantCode());
        }
        if (StrUtil.isNotBlank(dto.getPayOrderId())) {
            return foodMealOrderService.lockByTradeOrderId(dto.getPayOrderId());
        }
        if (StrUtil.isNotBlank(dto.getPayOrderNo())) {
            return foodMealOrderService.lockByUnifiedOrderId(dto.getPayOrderNo());
        }
        throw new BizErrorException("订单或支付单标识不能为空");
    }

    private FoodMealOrder lockOrderForRefund(FoodMealOrderRefundSuccessDTO dto) {
        if (StrUtil.isNotBlank(dto.getOrderId())) {
            return foodMealOrderService.lockById(dto.getOrderId(), WebFrameworkUtils.getLoginUserTenantCode());
        }
        if (StrUtil.isNotBlank(dto.getPayOrderId())) {
            return foodMealOrderService.lockByTradeOrderId(dto.getPayOrderId());
        }
        if (StrUtil.isNotBlank(dto.getPayOrderNo())) {
            return foodMealOrderService.lockByUnifiedOrderId(dto.getPayOrderNo());
        }
        throw new BizErrorException("订单或支付单标识不能为空");
    }

    private void assertPaidAmountMatched(FoodMealOrder order, BigDecimal paidAmount) {
        if (paidAmount == null || money(order.getUserPayAmount()).compareTo(money(paidAmount)) != 0) {
            log.warn("企业团餐支付金额不一致 orderId={}, orderNo={}, expectedUserPayAmount={}, paidAmount={}",
                    order.getId(), order.getOrderNo(), order.getUserPayAmount(), paidAmount);
            throw new BizErrorException("支付金额与订单自付金额不一致");
        }
    }

    private void executeRefundAfterMarkRefunding(FoodMealOrder order) {
        BigDecimal userPay = money(order.getUserPayAmount());
        if (userPay.compareTo(ZERO) <= 0) {
            foodCompanyMealRefundCoordinator.applyRefundSuccess(order.getId(), ZERO, LocalDateTime.now());
            return;
        }
        PayRefundCreateDTO refundDto = new PayRefundCreateDTO();
        refundDto.setBizScene(PayRecordBizSceneEnum.FOOD_COMPANY_MEAL_ORDER.getCode());
        refundDto.setBizOrderId(order.getId());
        refundDto.setRefundAmount(userPay);
        refundDto.setReason(order.getRefundReason());
        payRefundApi.createAndExecute(refundDto);
    }

    private void triggerFinanceSettleOnPaySuccess(FoodCompanyMealOrderPaidDTO dto, FoodMealOrder order) {
        if (dto == null || order == null || money(order.getUserPayAmount()).compareTo(ZERO) <= 0) {
            return;
        }
        String bizOrderId = StrUtil.blankToDefault(dto.getOrderId(), order.getId());
        String financePayOrderId = StrUtil.blankToDefault(dto.getPayOrderId(), order.getTradeOrderId());
        if (StrUtil.isBlank(financePayOrderId)) {
            log.warn("企业团餐支付成功缺少 financePayOrderId，跳过结算挂接 orderId={}", bizOrderId);
            return;
        }
        String bizScene = PayRecordBizSceneEnum.FOOD_COMPANY_MEAL_ORDER.getCode();
        try {
            profitSharingApi.createPendingForPay(financePayOrderId, bizScene, null, bizOrderId, null);
        } catch (Exception ex) {
            log.warn("企业团餐创建待分账失败 orderId={}: {}", bizOrderId, ex.getMessage());
        }
        try {
            merchantReceivableApi.recordForMallPay(financePayOrderId, bizOrderId, null);
        } catch (Exception ex) {
            log.warn("企业团餐记商户应收失败 orderId={}: {}", bizOrderId, ex.getMessage());
        }
    }

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

    private static void assertCompanyRefundable(FoodMealOrder order) {
        if (!ORDER_STATUS_PAID.equals(order.getOrderStatus()) || !PAY_STATUS_PAID.equals(order.getPayStatus())) {
            if (!ORDER_STATUS_REFUNDING.equals(order.getOrderStatus())) {
                throw new BizErrorException("仅已支付订单可发起退款");
            }
        }
        if (ORDER_STATUS_REFUNDED.equals(order.getOrderStatus()) || PAY_STATUS_REFUNDED.equals(order.getPayStatus())) {
            throw new BizErrorException("订单已退款");
        }
        if (ORDER_STATUS_PICKED.equals(order.getOrderStatus())) {
            throw new BizErrorException("已取餐订单不可退款");
        }
    }

    private FoodCompanyMealTrialVO.FoodCompanySubsidyRuleSummaryVO toRuleSummary(FoodCompanySubsidyRule rule) {
        if (rule == null) {
            return null;
        }
        FoodCompanyMealTrialVO.FoodCompanySubsidyRuleSummaryVO vo = new FoodCompanyMealTrialVO.FoodCompanySubsidyRuleSummaryVO();
        vo.setRuleId(rule.getId());
        vo.setRuleName(rule.getRuleName());
        vo.setRuleType(rule.getRuleType());
        vo.setSubsidyAmount(money(rule.getSubsidyAmount()));
        vo.setDiscountRate(rule.getDiscountRate());
        vo.setDailyLimitAmount(rule.getDailyLimitAmount());
        vo.setMealLimitAmount(rule.getMealLimitAmount());
        vo.setMealTypes(rule.getMealTypes());
        vo.setEffectiveTime(rule.getEffectiveTime());
        vo.setExpireTime(rule.getExpireTime());
        return vo;
    }

    private void fillMenuRuntimeInfo(FoodPersonalMealMenuVO vo, FoodMealMenu menu) {
        MealOrderableResult orderableResult = checkMealOrderable(menu.getServiceAreaId(), menu.getMealType(), menu.getMenuDate());
        vo.setOrderable(orderableResult.orderable());
        vo.setNotOrderableReason(orderableResult.reason());
        vo.setDeadlineTime(orderableResult.deadlineTime());
        vo.setServerTime(LocalDateTime.now());
        vo.setDeadlineRemainSeconds(deadlineRemainSeconds(menu.getMenuDate(), orderableResult.deadlineTime()));
        vo.setPickupType(DELIVERY_TYPE_PICKUP);
        vo.setPickupDate(menu.getMenuDate());
        FoodMealRestaurantVO serviceArea = mealServiceArea(menu.getServiceAreaId());
        vo.setRestaurant(serviceArea);
        vo.setPickupLocation(serviceArea == null ? null : serviceArea.getPickupLocation());
    }

    private FoodPersonalMealMenuVO toMenuVO(FoodMealMenu menu) {
        FoodPersonalMealMenuVO vo = new FoodPersonalMealMenuVO();
        vo.setId(menu.getId());
        vo.setServiceAreaId(menu.getServiceAreaId());
        vo.setServiceAreaName(serviceAreaName(menu));
        vo.setRestaurantId(menu.getRestaurantId());
        vo.setRestaurantName(vo.getServiceAreaName());
        vo.setMenuDate(menu.getMenuDate());
        vo.setMealType(normalizeMealType(menu.getMealType()));
        vo.setScopeType(menu.getScopeType());
        vo.setPublishStatus(menu.getPublishStatus());
        return vo;
    }

    private boolean isPublishedMenuItem(FoodMealMenuItem item) {
        return item != null && (StrUtil.isBlank(item.getPublishStatus()) || ITEM_STATUS_PUBLISHED.equals(item.getPublishStatus()));
    }

    private FoodMealMenuItemListVO toMenuItemVO(FoodMealMenuItem item) {
        FoodMealMenuItemListVO vo = new FoodMealMenuItemListVO();
        vo.setId(item.getId());
        vo.setMenuId(item.getMenuId());
        vo.setRestaurantId(item.getRestaurantId());
        vo.setServiceAreaId(item.getServiceAreaId());
        vo.setTemplateId(item.getTemplateId());
        vo.setTemplateItemId(item.getTemplateItemId());
        vo.setItemType(item.getItemType());
        vo.setDishId(item.getDishId());
        vo.setPackageId(item.getPackageId());
        vo.setItemName(item.getItemName());
        vo.setItemImage(item.getItemImage());
        vo.setSalePrice(money(item.getSalePrice()));
        vo.setStockQuantity(item.getStockQuantity());
        vo.setSoldQuantity(item.getSoldQuantity());
        vo.setRemainingQuantity(remainingQuantity(item));
        vo.setSoldOut(vo.getRemainingQuantity() != null && vo.getRemainingQuantity() <= 0);
        vo.setTags(parseStringList(item.getTagsJson()));
        List<String> images = parseStringList(item.getImagesJson());
        vo.setImages(images.isEmpty() && StrUtil.isNotBlank(item.getItemImage()) ? List.of(item.getItemImage()) : images);
        vo.setDescriptionHtml(item.getDescriptionHtml());
        vo.setPublishStatus(StrUtil.blankToDefault(item.getPublishStatus(), ITEM_STATUS_PUBLISHED));
        vo.setSpecs(List.of());
        vo.setLimitQuantityPerUser(item.getLimitQuantityPerUser());
        vo.setSortNo(item.getSortNo());
        vo.setCreateDate(item.getCreateDate());
        vo.setUpdateDate(item.getUpdateDate());
        return vo;
    }

    private void fillMenuItemSpecs(List<FoodMealMenuItemListVO> itemVOList, List<FoodMealMenuItem> items) {
        if (CollUtil.isEmpty(itemVOList) || CollUtil.isEmpty(items)) {
            return;
        }
        Map<String, FoodMealMenuItemListVO> voMap = itemVOList.stream().collect(Collectors.toMap(FoodMealMenuItemListVO::getId, vo -> vo));
        Map<String, List<FoodMealMenuItemSpec>> specsByMenuItemId = listSpecsByMenuItemIds(items.stream()
            .map(FoodMealMenuItem::getId)
            .collect(Collectors.toSet()));
        for (FoodMealMenuItem item : items) {
            FoodMealMenuItemListVO vo = voMap.get(item.getId());
            if (vo == null) {
                continue;
            }
            List<FoodMealMenuItemSpec> specs = specsByMenuItemId.getOrDefault(item.getId(), List.of());
            vo.setSpecs(specs.stream().map(this::toMenuItemSpecVO).collect(Collectors.toList()));
        }
    }

    private void applyDishDisplay(List<FoodMealMenuItemListVO> itemVOList, List<FoodMealMenuItem> items) {
        if (CollUtil.isEmpty(itemVOList) || CollUtil.isEmpty(items)) {
            return;
        }
        Map<String, FoodMealMenuItemListVO> voMap = itemVOList.stream().collect(Collectors.toMap(FoodMealMenuItemListVO::getId, vo -> vo));
        Map<String, FoodDish> dishMap = foodDishService.mapDishByIds(items.stream()
            .filter(item -> "dish".equals(item.getItemType()))
            .map(FoodMealMenuItem::getDishId)
            .filter(StrUtil::isNotBlank)
            .distinct()
            .toList());
        for (FoodMealMenuItem item : items) {
            FoodMealMenuItemListVO vo = voMap.get(item.getId());
            FoodDish dish = "dish".equals(item.getItemType()) ? dishMap.get(item.getDishId()) : null;
            if (vo == null || dish == null) {
                continue;
            }
            vo.setItemName(dish.getName());
            vo.setItemImage(dish.getImageUrl());
            vo.setImages(sourceImages(dish));
            vo.setTags(parseStringList(dish.getTagsJson()));
            vo.setDescriptionHtml(dish.getDescriptionHtml());
        }
    }

    private List<String> sourceImages(FoodDish dish) {
        List<String> images = parseStringList(dish.getImagesJson());
        return images.isEmpty() && StrUtil.isNotBlank(dish.getImageUrl()) ? List.of(dish.getImageUrl()) : images;
    }

    private List<String> parseStringList(String json) {
        if (StrUtil.isBlank(json)) {
            return List.of();
        }
        try {
            return JSONUtil.parseArray(json).stream()
                .map(value -> value == null ? "" : String.valueOf(value).trim())
                .filter(StrUtil::isNotBlank)
                .toList();
        } catch (Exception e) {
            return List.of();
        }
    }

    private FoodMealMenuItemListVO.Spec toMenuItemSpecVO(FoodMealMenuItemSpec spec) {
        FoodMealMenuItemListVO.Spec vo = new FoodMealMenuItemListVO.Spec();
        vo.setId(spec.getId());
        vo.setName(spec.getSpecName());
        vo.setDescription(spec.getSpecDescription());
        vo.setSalePrice(money(spec.getSalePrice()));
        vo.setStockQuantity(spec.getStockQuantity());
        vo.setSoldQuantity(spec.getSoldQuantity());
        vo.setRemainingQuantity(remainingQuantity(spec));
        vo.setSoldOut(vo.getRemainingQuantity() != null && vo.getRemainingQuantity() <= 0);
        vo.setLimitQuantityPerUser(spec.getLimitQuantityPerUser());
        vo.setSortNo(spec.getSortNo());
        return vo;
    }

    private FoodMealRestaurantVO mealServiceArea(String serviceAreaId) {
        if (StrUtil.isBlank(serviceAreaId)) {
            return null;
        }
        FoodMealServiceArea serviceArea = foodMealServiceAreaService.get(serviceAreaId);
        return serviceArea == null ? null : toMealRestaurantVO(serviceArea);
    }

    private FoodMealRestaurantVO toMealRestaurantVO(FoodMealServiceArea serviceArea) {
        FoodMealRestaurantVO vo = new FoodMealRestaurantVO();
        vo.setId(serviceArea.getId());
        vo.setName(serviceArea.getAreaName());
        vo.setIcon(serviceArea.getServiceIcon());
        vo.setLocationLabel(serviceArea.getLocationLabel());
        vo.setAddress(serviceArea.getPickupLocation());
        vo.setFloorLabel(StrUtil.blankToDefault(serviceArea.getFloorLabel(), serviceArea.getLocationLabel()));
        vo.setPhone(serviceArea.getPhone());
        vo.setOpenHours(serviceArea.getOpenHours());
        boolean open = serviceArea.getStatus() != null && serviceArea.getStatus() == STATUS_ENABLED;
        vo.setOpenStatus(open ? "open" : "closed");
        vo.setOpenStatusText(StrUtil.blankToDefault(serviceArea.getOpenStatusText(), open ? "已开放" : "未开放"));
        vo.setPickupLocation(StrUtil.blankToDefault(serviceArea.getPickupLocation(), serviceArea.getLocationLabel()));
        return vo;
    }

    private String serviceAreaName(FoodMealMenu menu) {
        return StrUtil.blankToDefault(menu.getServiceAreaName(), serviceAreaName(menu.getServiceAreaId()));
    }

    private String serviceAreaName(String serviceAreaId) {
        if (StrUtil.isBlank(serviceAreaId)) {
            return "";
        }
        Map<String, String> nameMap = foodMealServiceAreaService.mapAreaNameByIds(List.of(serviceAreaId));
        return StrUtil.blankToDefault(nameMap.get(serviceAreaId), "");
    }

    private String serviceAreaPickupLocation(String serviceAreaId) {
        FoodMealRestaurantVO serviceArea = mealServiceArea(serviceAreaId);
        return serviceArea == null ? null : serviceArea.getPickupLocation();
    }

    private Long deadlineRemainSeconds(LocalDate mealDate, String deadlineTime) {
        if (mealDate == null || StrUtil.isBlank(deadlineTime)) {
            return null;
        }
        try {
            LocalDateTime deadline = LocalDateTime.parse(mealDate + "T" + deadlineTime);
            return Math.max(Duration.between(LocalDateTime.now(), deadline).getSeconds(), 0L);
        } catch (Exception ignored) {
            return null;
        }
    }

    private LocalDate normalizeWeekStart(LocalDate weekStart) {
        LocalDate date = weekStart == null ? LocalDate.now() : weekStart;
        return date.with(DayOfWeek.MONDAY);
    }

    private FoodMealDateStatusVO buildDateStatus(LocalDate mealDate, MealOrderableResult result, LocalDateTime serverTime) {
        FoodMealDateStatusVO vo = new FoodMealDateStatusVO();
        vo.setMealDate(mealDate);
        vo.setOrderable(result.orderable());
        vo.setStatus(result.status());
        vo.setReason(result.reason());
        vo.setDeadlineTime(result.deadlineTime());
        vo.setServerTime(serverTime);
        vo.setDeadlineRemainSeconds(deadlineRemainSeconds(mealDate, result.deadlineTime()));
        return vo;
    }

    private MealOrderableResult chooseDateStatusResult(List<MealOrderableResult> results) {
        if (CollUtil.isEmpty(results)) {
            return MealOrderableResult.fail(DATE_STATUS_UNAVAILABLE, "当前日期不可预订", null);
        }
        return results.stream()
            .filter(MealOrderableResult::orderable)
            .findFirst()
            .orElseGet(() -> results.stream()
                .min(Comparator.comparingInt(result -> dateStatusPriority(result.status())))
                .orElse(MealOrderableResult.fail(DATE_STATUS_UNAVAILABLE, "当前日期不可预订", null)));
    }

    private int dateStatusPriority(String status) {
        if (DATE_STATUS_EXPIRED.equals(status)) return 1;
        if (DATE_STATUS_AFTER_DEADLINE.equals(status)) return 2;
        if (DATE_STATUS_NOT_STARTED.equals(status)) return 3;
        if (DATE_STATUS_OUT_OF_RANGE.equals(status)) return 4;
        if (DATE_STATUS_NO_MENU.equals(status)) return 5;
        return 9;
    }

    private void validateCancelRule(FoodMealOrder order) {
        FoodMealRule rule = chooseMealRule(order.getServiceAreaId(), order.getMealType());
        if (rule == null || StrUtil.isBlank(rule.getCancelRuleType()) || CANCEL_RULE_ANYTIME.equals(rule.getCancelRuleType())) {
            return;
        }
        if (CANCEL_RULE_NONE.equals(rule.getCancelRuleType())) {
            throw new BizErrorException("当前订餐规则不允许取消");
        }
        if (CANCEL_RULE_BEFORE_DEADLINE.equals(rule.getCancelRuleType())) {
            LocalDateTime deadline = order.getMenuDate().atStartOfDay();
            if (rule.getFixedDeadlineTime() != null) {
                deadline = LocalDateTime.of(order.getMenuDate(), rule.getFixedDeadlineTime());
            }
            if (rule.getCancelDeadlineHours() != null && rule.getCancelDeadlineHours() > 0) {
                deadline = deadline.minusHours(rule.getCancelDeadlineHours());
            }
            if (LocalDateTime.now().isAfter(deadline)) {
                throw new BizErrorException("已超过取消截止时间");
            }
        }
    }

    private Integer remainingQuantity(FoodMealMenuItem menuItem) {
        if (menuItem.getStockQuantity() == null) {
            return null;
        }
        int soldQuantity = menuItem.getSoldQuantity() == null ? 0 : menuItem.getSoldQuantity();
        return Math.max(menuItem.getStockQuantity() - soldQuantity, 0);
    }

    private Integer remainingQuantity(FoodMealMenuItemSpec spec) {
        if (spec.getStockQuantity() == null) {
            return null;
        }
        int soldQuantity = spec.getSoldQuantity() == null ? 0 : spec.getSoldQuantity();
        return Math.max(spec.getStockQuantity() - soldQuantity, 0);
    }

    private boolean matchMealType(String ruleMealTypes, String mealType) {
        if (StrUtil.isBlank(ruleMealTypes)) {
            return true;
        }
        String normalized = normalizeMealType(mealType);
        for (String item : ruleMealTypes.split(",")) {
            if (StrUtil.equals(normalized, normalizeMealType(item))) {
                return true;
            }
        }
        return false;
    }

    private boolean equalsMealType(String left, String right) {
        if (StrUtil.isBlank(left) || StrUtil.isBlank(right)) {
            return false;
        }
        return StrUtil.equals(normalizeMealType(left), normalizeMealType(right));
    }

    private Set<String> mealTypeCandidates(String mealType) {
        Set<String> candidates = new HashSet<>();
        if (StrUtil.isBlank(mealType)) {
            return candidates;
        }
        String normalized = normalizeMealType(mealType);
        candidates.add(mealType.trim());
        candidates.add(normalized);
        candidates.add(normalized.toLowerCase());
        return candidates;
    }

    private String normalizeMealType(String mealType) {
        if (mealType == null) {
            return null;
        }
        Map<String, String> mappings = new HashMap<>();
        mappings.put("BREAKFAST", "BREAKFAST");
        mappings.put("LUNCH", "LUNCH");
        mappings.put("DINNER", "DINNER");
        mappings.put("SNACK", "SNACK");
        mappings.put("breakfast", "BREAKFAST");
        mappings.put("lunch", "LUNCH");
        mappings.put("dinner", "DINNER");
        mappings.put("snack", "SNACK");
        return mappings.getOrDefault(mealType.trim(), mealType.trim().toUpperCase());
    }

    private String mealTypeLabel(String mealType) {
        return switch (normalizeMealType(mealType)) {
            case "BREAKFAST" -> "早餐";
            case "LUNCH" -> "午餐";
            case "DINNER" -> "晚餐";
            case "SNACK" -> "加餐";
            default -> mealType;
        };
    }

    private BigDecimal remain(BigDecimal limitAmount, BigDecimal usedAmount) {
        if (limitAmount == null) {
            return null;
        }
        BigDecimal remainAmount = money(limitAmount).subtract(money(usedAmount));
        return remainAmount.compareTo(BigDecimal.ZERO) < 0 ? ZERO : money(remainAmount);
    }

    private BigDecimal min(BigDecimal left, BigDecimal right) {
        return left.compareTo(right) <= 0 ? left : right;
    }

    private BigDecimal money(BigDecimal amount) {
        if (amount == null) {
            return ZERO;
        }
        return amount.setScale(2, RoundingMode.HALF_UP);
    }

    private void insertOrder(FoodMealOrder order) {
        foodMealOrderService.insertOrder(order);
    }

    private void updateOrder(FoodMealOrder order) {
        foodMealOrderService.updateOrder(order);
    }

    private void insertOrderItem(FoodMealOrderItem orderItem) {
        foodMealOrderItemService.insertOrderItem(orderItem);
    }

    private void updateAccount(FoodCompanyAccount account) {
        foodCompanyAccountService.updateAccount(account);
    }

    private void insertAccountLog(FoodCompanyAccountLog log) {
        foodCompanyAccountLogService.insertLog(log);
    }

    private void insertSubsidyUse(FoodCompanySubsidyUse use) {
        foodCompanySubsidyUseService.insertSubsidyUse(use);
    }

    private void updateSubsidyUse(FoodCompanySubsidyUse use) {
        foodCompanySubsidyUseService.updateSubsidyUse(use);
    }

    private FoodCompanyMealOrderResultVO toOrderResult(FoodMealOrder order, PayOrderIntentVO intent) {
        return toOrderResult(order, intent, null);
    }

    private FoodCompanyMealOrderResultVO toOrderResult(FoodMealOrder order, PayOrderIntentVO intent, List<FoodMealOrderItem> items) {
        FoodCompanyMealOrderResultVO vo = new FoodCompanyMealOrderResultVO();
        vo.setId(order.getId());
        vo.setOrderNo(order.getOrderNo());
        vo.setOrderType(order.getOrderType());
        vo.setCompanyId(order.getCompanyId());
        vo.setCompanyName(order.getCompanyName());
        vo.setEmployeeId(order.getEmployeeId());
        vo.setMemberId(order.getMemberId());
        vo.setRestaurantId(order.getRestaurantId());
        vo.setRestaurantName(StrUtil.blankToDefault(order.getRestaurantName(), order.getServiceAreaName()));
        vo.setServiceAreaId(order.getServiceAreaId());
        vo.setServiceAreaName(order.getServiceAreaName());
        vo.setMenuId(order.getMenuId());
        vo.setMealDate(order.getMenuDate());
        vo.setMealType(order.getMealType());
        vo.setTotalAmount(order.getTotalAmount());
        vo.setDiscountAmount(order.getDiscountAmount());
        vo.setCompanyPayAmount(order.getCompanyPayAmount());
        vo.setEmployeePayAmount(order.getUserPayAmount());
        fillNetPayAmounts(vo, order);
        vo.setOrderStatus(order.getOrderStatus());
        vo.setPayStatus(order.getPayStatus());
        vo.setPickupCode(order.getPickupCode());
        vo.setPayTime(order.getPayTime());
        vo.setRefundAmount(money(order.getRefundAmount()));
        vo.setRefundApplyTime(order.getRefundApplyTime());
        vo.setRefundReason(order.getRefundReason());
        vo.setRefundTime(order.getRefundTime());
        vo.setVerifyTime(order.getVerifyTime());
        vo.setVerifyUserId(order.getVerifyUserId());
        vo.setVerifyUserName(order.getVerifyUserName());
        vo.setPayOrderId(intent != null ? intent.getPayOrderId() : order.getTradeOrderId());
        vo.setPayOrderNo(intent != null ? intent.getOutTradeNo() : order.getUnifiedOrderId());
        vo.setDeliveryType(order.getDeliveryType());
        vo.setAddressId(order.getAddressId());
        vo.setReceiverName(order.getReceiverName());
        vo.setReceiverMobile(order.getReceiverMobile());
        vo.setProvince(order.getProvince());
        vo.setProvinceCode(order.getProvinceCode());
        vo.setCity(order.getCity());
        vo.setCityCode(order.getCityCode());
        vo.setDistrict(order.getDistrict());
        vo.setDistrictCode(order.getDistrictCode());
        vo.setDetailAddress(order.getDetailAddress());
        vo.setFullAddress(order.getFullAddress());
        vo.setLongitude(order.getLongitude());
        vo.setLatitude(order.getLatitude());
        vo.setRemark(order.getRemark());
        vo.setCreateDate(order.getCreateDate());
        if (CollUtil.isNotEmpty(items)) {
            vo.setItems(items.stream().map(this::toOrderItemVO).collect(Collectors.toList()));
        }
        vo.setTenantId(intent != null ? intent.getTenantId() : order.getTenantId());
        return vo;
    }

    private void fillNetPayAmounts(FoodCompanyMealOrderResultVO vo, FoodMealOrder order) {
        BigDecimal companyPayAmount = money(order.getCompanyPayAmount());
        BigDecimal employeePayAmount = money(order.getUserPayAmount());
        if (ORDER_STATUS_REFUNDED.equals(order.getOrderStatus()) || PAY_STATUS_REFUNDED.equals(order.getPayStatus())) {
            vo.setCompanyNetPayAmount(ZERO);
            vo.setEmployeeNetPayAmount(ZERO);
            return;
        }
        vo.setCompanyNetPayAmount(companyPayAmount);
        vo.setEmployeeNetPayAmount(employeePayAmount);
    }

    private FoodCompanyMealOrderResultVO.Item toOrderItemVO(FoodMealOrderItem item) {
        FoodCompanyMealOrderResultVO.Item vo = new FoodCompanyMealOrderResultVO.Item();
        vo.setId(item.getId());
        vo.setMenuItemId(item.getMenuItemId());
        vo.setItemType(item.getItemType());
        vo.setItemName(item.getItemName());
        vo.setItemImage(item.getItemImage());
        vo.setSalePrice(money(item.getSalePrice()));
        vo.setQuantity(item.getQuantity());
        vo.setTotalAmount(money(item.getTotalAmount()));
        vo.setDiscountAmount(money(item.getDiscountAmount()));
        vo.setCompanyPayAmount(money(item.getCompanyPayAmount()));
        vo.setEmployeePayAmount(money(item.getUserPayAmount()));
        return vo;
    }

    private FoodCompanyEmployeeCompanyVO toEmployeeCompanyVO(FoodCompanyEmployee employee) {
        FoodCompany company = foodCompanyService.getById(employee.getCompanyId());
        if (company == null || !DEL_NORMAL.equals(company.getDelFlag()) || !Integer.valueOf(STATUS_ENABLED).equals(company.getStatus())) {
            return null;
        }
        FoodCompanyAccount account = foodCompanyAccountService.getByCompanyId(company.getId(), company.getTenantCode());
        FoodCompanyEmployeeCompanyVO vo = new FoodCompanyEmployeeCompanyVO();
        vo.setCompanyId(company.getId());
        vo.setCompanyCode(company.getCompanyCode());
        vo.setCompanyName(company.getCompanyName());
        vo.setEmployeeId(employee.getId());
        vo.setEmployeeNo(employee.getEmployeeNo());
        vo.setEmployeeName(employee.getEmployeeName());
        vo.setMobile(employee.getMobile());
        vo.setMemberId(employee.getMemberId());
        if (account != null) {
            vo.setAccountId(account.getId());
            vo.setAvailableBalance(money(account.getAvailableBalance()));
            vo.setFrozenBalance(money(account.getFrozenBalance()));
        }
        return vo;
    }

    private static String generateOrderNo() {
        return ORDER_NO_PREFIX + LocalDateTime.now().format(NO_TIME_FMT) + IdUtil.fastSimpleUUID().substring(0, 8).toUpperCase();
    }

    private static String generatePickupCode() {
        return RandomUtil.randomNumbers(6);
    }

    private record MealOrderRequestItem(FoodMealMenuItem menuItem, FoodMealMenuItemSpec spec, int quantity, BigDecimal salePrice) {
    }

    private record MealOrderableResult(boolean orderable, String status, String reason, String deadlineTime) {
        static MealOrderableResult ok(String deadlineTime) {
            return new MealOrderableResult(true, DATE_STATUS_ORDERABLE, null, deadlineTime);
        }

        static MealOrderableResult fail(String status, String reason, String deadlineTime) {
            return new MealOrderableResult(false, status, reason, deadlineTime);
        }
    }

    private record EmployeeContext(FoodCompany company, FoodCompanyEmployee employee, FoodCompanyAccount account) {
    }
}
