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.PayRefundApi;
import com.jeesharp.business.modules.finance.api.ProfitSharingApi;
import com.jeesharp.business.modules.finance.payrefund.dto.PayRefundCreateDTO;
import com.jeesharp.business.modules.finance.paymentrecord.enums.PayRecordBizSceneEnum;
import com.jeesharp.business.modules.food.application.support.FoodMealPayAssembler;
import com.jeesharp.business.modules.food.application.support.FoodPersonalMealRefundCoordinator;
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.FoodPersonalMealRefundDTO;
import com.jeesharp.business.modules.food.dto.FoodPersonalMealRefundTriggerDTO;
import com.jeesharp.business.modules.food.dto.FoodPersonalMealOrderCancelDTO;
import com.jeesharp.business.modules.food.dto.FoodPersonalMealOrderCreateDTO;
import com.jeesharp.business.modules.food.dto.FoodPersonalMealOrderPaidDTO;
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.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.*;
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;

/**
 * 个人订餐应用服务（与美食团购 {@link FoodBookingOrderApplicationService} 独立）。
 *
 * <p><b>主流程</b></p>
 * <pre>
 * createOrder → 扣菜品已售数量 → preparePay → 微信回调 FoodMealWechatPayCallbackAdapter → handlePaid
 * cancel      → 回滚已售数量
 * applyRefund → PayRefundApi.createAndExecute → handleRefundSuccess / FoodMealPayRefundSuccessHandler
 * verify      → 取餐核销 → scheduleSharingByBizOrder
 * </pre>
 * <p>表：{@code app_food_meal_order}；无优惠券；创建、支付、取消、核销同步统一订单中心。</p>
 *
 * @author jeesharp
 * @since 2026-06-09
 */
@Slf4j
@Service
@AllArgsConstructor
@Transactional(readOnly = true)
public class FoodPersonalMealApplicationService {

    private static final String DEL_NORMAL = "0";
    private static final int STATUS_ENABLED = 1;
    private static final String MENU_STATUS_PUBLISHED = "published";
    private static final String ITEM_STATUS_PUBLISHED = "published";
    private static final String MENU_SCOPE_PUBLIC = "public";
    private static final String MENU_SCOPE_ALL = "all";
    private static final String RULE_SCOPE_ALL = "all";
    private static final String RULE_SCOPE_PERSONAL = "personal";
    private static final String ORDER_TYPE_PERSONAL = "personal";
    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 int DEFAULT_HOT_ITEM_LIMIT = 5;
    private static final int MAX_HOT_ITEM_LIMIT = 20;
    private static final int HOT_ITEM_STAT_DAYS = 30;
    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 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 ORDER_NO_PREFIX = "FPM";
    private static final DateTimeFormatter NO_TIME_FMT = DateTimeFormatter.ofPattern("yyyyMMddHHmmss");
    private static final BigDecimal ZERO = BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP);

    private final FoodMealMenuService foodMealMenuService;
    private final FoodMealMenuItemService foodMealMenuItemService;
    private final FoodMealMenuItemSpecService foodMealMenuItemSpecService;
    private final FoodDishService foodDishService;
    private final FoodMealRuleService foodMealRuleService;
    private final FoodMealOrderService foodMealOrderService;
    private final FoodMealOrderItemService foodMealOrderItemService;
    private final FoodMealServiceAreaService foodMealServiceAreaService;
    private final ProfitSharingApi profitSharingApi;
    private final MerchantReceivableApi merchantReceivableApi;
    private final PayRefundApi payRefundApi;
    private final TradeOrderFacadeApi tradeOrderFacadeApi;
    private final MallMemberAddressApplicationService mallMemberAddressApplicationService;
    private final FoodPersonalMealRefundCoordinator foodPersonalMealRefundCoordinator;
    private final FoodMealPayAssembler foodMealPayAssembler;

    public List<FoodMealRestaurantVO> listMealRestaurants() {
        return foodMealServiceAreaService.listOnline().stream()
            .map(this::toMealRestaurantVO)
            .collect(Collectors.toList());
    }

    public List<LocalDate> listBookableDates(String restaurantId) {
        FoodMealRule rule = chooseMealRule(restaurantId, null);
        int futureDays = rule != null && rule.getBookableFutureDays() != null && rule.getBookableFutureDays() >= 0
            ? rule.getBookableFutureDays()
            : 7;
        log.info("[可订日期] restaurantId={}, ruleId={}, futureDays={}, advanceDays={}, advanceHours={}, fixedDeadlineTime={}, bookableFutureDays={}",
            restaurantId, rule != null ? rule.getId() : "null", futureDays,
            rule != null ? rule.getAdvanceDays() : null,
            rule != null ? rule.getAdvanceHours() : null,
            rule != null ? rule.getFixedDeadlineTime() : null,
            rule != null ? rule.getBookableFutureDays() : null);
        LocalDate today = LocalDate.now();
        List<LocalDate> dates = new ArrayList<>();
        for (int i = 0; i <= futureDays; i++) {
            LocalDate date = today.plusDays(i);
            List<FoodMealMenu> menus = listPublishedPersonalMenus(restaurantId, date, null);
            log.info("[可订日期] date={}, menusFound={}", date, menus.size());
            boolean hasOrderableMeal = menus.stream()
                .map(FoodMealMenu::getMealType)
                .filter(StrUtil::isNotBlank)
                .map(this::normalizeMealType)
                .distinct()
                .anyMatch(mealType -> {
                    MealOrderableResult result = checkMealOrderable(restaurantId, mealType, date);
                    log.info("[可订日期]   date={}, mealType={}, orderable={}, reason={}", date, mealType, result.orderable(), result.reason());
                    return result.orderable();
                });
            if (hasOrderableMeal) {
                dates.add(date);
            }
        }
        log.info("[可订日期] 最终可订日期列表: {}", dates);
        return dates;
    }

    public List<FoodMealDateStatusVO> listDateStatuses(String serviceAreaId, LocalDate weekStart) {
        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 = listPublishedPersonalMenus(serviceAreaId, date, null);
            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 restaurantId, LocalDate mealDate) {
        List<FoodMealMenu> menus = listPublishedPersonalMenus(restaurantId, mealDate, null);
        return menus.stream()
            .map(FoodMealMenu::getMealType)
            .filter(StrUtil::isNotBlank)
            .map(this::normalizeMealType)
            .distinct()
            .filter(type -> checkMealOrderable(restaurantId, type, mealDate).orderable())
            .map(type -> Map.of("value", type, "label", mealTypeLabel(type)))
            .collect(Collectors.toList());
    }

    public FoodPersonalMealMenuVO getMenu(String restaurantId, LocalDate mealDate, String mealType) {
        List<FoodMealMenu> menus = listPublishedPersonalMenus(restaurantId, mealDate, mealType);
        if (CollUtil.isEmpty(menus)) {
            return null;
        }
        FoodMealMenu menu = menus.get(0);
        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 List<FoodMealHotItemVO> listHotItems(String serviceAreaId, String mealType, Integer limit) {
        int pageSize = limit == null || limit <= 0 ? DEFAULT_HOT_ITEM_LIMIT : Math.min(limit, MAX_HOT_ITEM_LIMIT);
        FoodMealHotItemQueryVO query = new FoodMealHotItemQueryVO();
        query.setServiceAreaId(serviceAreaId);
        query.setRestaurantId(serviceAreaId);
        query.setMealType(StrUtil.isBlank(mealType) ? null : normalizeMealType(mealType));
        query.setStartDate(LocalDate.now().minusDays(HOT_ITEM_STAT_DAYS));
        query.setEndDate(LocalDate.now());
        query.setLimit(pageSize);
        return foodMealOrderItemService.listHotItems(query, WebFrameworkUtils.getLoginUserTenantCode());
    }

    public FoodMealMenuItemListVO getMenuItemDetail(String menuItemId) {
        if (StrUtil.isBlank(menuItemId)) {
            return null;
        }
        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;
        }
        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 FoodPersonalMealTrialVO trial(FoodPersonalMealOrderCreateDTO dto) {
        FoodMealMenu menu = getValidMenu(dto.getMenuId(), dto.getMealDate(), dto.getMealType());
        validateMealRule(menu.getServiceAreaId(), dto.getMealType(), dto.getMealDate());
        List<FoodMealMenuItem> menuItems = getValidMenuItems(dto, false);
        List<MealOrderRequestItem> requestItems = buildRequestItems(dto, menu, menuItems, false);
        BigDecimal totalAmount = ZERO;
        List<FoodPersonalMealTrialVO.Item> trialItems = new ArrayList<>();
        for (MealOrderRequestItem requestItem : requestItems) {
            FoodMealMenuItem menuItem = requestItem.menuItem();
            FoodMealMenuItemSpec spec = requestItem.spec();
            int quantity = requestItem.quantity();
            BigDecimal salePrice = requestItem.salePrice();
            BigDecimal itemTotalAmount = money(salePrice).multiply(BigDecimal.valueOf(quantity));
            itemTotalAmount = money(itemTotalAmount);
            totalAmount = totalAmount.add(itemTotalAmount);
            FoodPersonalMealTrialVO.Item item = new FoodPersonalMealTrialVO.Item();
            item.setMenuItemId(menuItem.getId());
            fillTrialSpecSnapshot(item, spec);
            item.setItemName(menuItem.getItemName());
            item.setSalePrice(money(salePrice));
            item.setQuantity(quantity);
            item.setTotalAmount(itemTotalAmount);
            item.setStockQuantity(spec == null ? menuItem.getStockQuantity() : spec.getStockQuantity());
            item.setSoldQuantity(spec == null ? menuItem.getSoldQuantity() : spec.getSoldQuantity());
            item.setRemainingQuantity(spec == null ? remainingQuantity(menuItem) : remainingQuantity(spec));
            item.setLimitQuantityPerUser(spec == null ? menuItem.getLimitQuantityPerUser() : spec.getLimitQuantityPerUser());
            trialItems.add(item);
        }
        FoodPersonalMealTrialVO vo = new FoodPersonalMealTrialVO();
        vo.setMenuId(menu.getId());
        vo.setMealDate(menu.getMenuDate());
        vo.setMealType(normalizeMealType(menu.getMealType()));
        vo.setTotalAmount(money(totalAmount));
        vo.setItems(trialItems);
        return vo;
    }

    /** 创建待支付订单：校验菜单/规则/限购 → 写主单与明细 → 增加菜品已售数量。 */
    @Transactional(readOnly = false)
    public FoodPersonalMealOrderVO createOrder(FoodPersonalMealOrderCreateDTO dto) {
        FoodMealMenu menu = getValidMenu(dto.getMenuId(), dto.getMealDate(), dto.getMealType());
        cancelStalePendingOrders(dto.getMemberId(), dto.getMenuId());
        validateMealRule(menu.getServiceAreaId(), dto.getMealType(), dto.getMealDate());
        List<FoodMealMenuItem> menuItems = getValidMenuItems(dto, true);
        List<MealOrderRequestItem> requestItems = buildRequestItems(dto, menu, menuItems, true);
        BigDecimal totalAmount = ZERO;
        for (MealOrderRequestItem requestItem : requestItems) {
            totalAmount = totalAmount.add(money(requestItem.salePrice()).multiply(BigDecimal.valueOf(requestItem.quantity())));
        }
        totalAmount = money(totalAmount);
        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, menu, totalAmount);
        fillDeliverySnapshot(order, address, pickup ? DELIVERY_TYPE_PICKUP : DELIVERY_TYPE_DELIVERY, dto.getRemark());
        insertOrder(order);
        List<FoodMealOrderItem> orderItems = buildOrderItems(order, requestItems);
        for (FoodMealOrderItem item : orderItems) {
            insertOrderItem(item);
        }
        increaseSoldQuantity(orderItems);
        syncTradeOrderOnCreate(order, orderItems);
        return toOrderVO(order, orderItems);
    }

    /** 组装微信 JSAPI 预支付参数（金额、商户单号、回调路径）；由 Controller 再调 WechatJsapiPayOrderPrepayService。 */
    public FoodPersonalMealPayPrepareVO preparePay(String orderId, String memberId) {
        FoodMealOrder order = foodMealOrderService.get(orderId);
        if (order == null || !DEL_NORMAL.equals(order.getDelFlag()) || !ORDER_TYPE_PERSONAL.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("当前订单不可支付");
        }
        return foodMealPayAssembler.buildPrepareVo(order);
    }

    @Transactional(readOnly = false)
    public FoodPersonalMealOrderVO bindPayOrder(String orderId, String memberId, String payOrderId, String outTradeNo) {
        FoodMealOrder order = foodMealOrderService.lockById(orderId, WebFrameworkUtils.getLoginUserTenantCode());
        if (order == null || !ORDER_TYPE_PERSONAL.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("当前订单不可支付");
        }
        order.setTradeOrderId(payOrderId);
        order.setUnifiedOrderId(outTradeNo);
        updateOrder(order);
        return getOrderDetail(order.getId(), memberId);
    }

    /** 个人订餐支付成功确认（微信回调入口）：幂等校验金额 → 待支付改已支付 → 生成取餐码 → 同步统一单 → 触发结算。 */
    @Transactional(readOnly = false)
    public FoodPersonalMealOrderVO handlePaid(FoodPersonalMealOrderPaidDTO 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 toOrderVO(order, listOrderItems(order.getId()));
        }
        if (!ORDER_TYPE_PERSONAL.equals(order.getOrderType())
                || !ORDER_STATUS_PENDING_PAY.equals(order.getOrderStatus())
                || !PAY_STATUS_UNPAID.equals(order.getPayStatus())) {
            throw new BizErrorException("当前订单不可支付确认");
        }
        assertPaidAmountMatched(order, dto.getPaidAmount());
        // 核心状态流转 + 生成取餐码
        order.setOrderStatus(ORDER_STATUS_PAID);
        order.setPayStatus(PAY_STATUS_PAID);
        order.setPayTime(dto.getPaidTime() != null ? dto.getPaidTime() : LocalDateTime.now());
        if (StrUtil.isBlank(order.getPickupCode())) {
            order.setPickupCode(generatePickupCode());
        }
        updateOrder(order);
        try {
            syncTradeOrderMarkPaid(order);
        } catch (Exception ex) {
            log.warn("个人订餐同步统一订单已支付失败 orderId={}: {}", order.getId(), ex.getMessage());
        }
        triggerFinanceSettleOnPaySuccess(dto, order);
        return toOrderVO(order, listOrderItems(order.getId()));
    }

    @Transactional(readOnly = false)
    public FoodPersonalMealOrderVO cancel(FoodPersonalMealOrderCancelDTO dto, String memberId) {
        FoodMealOrder order = foodMealOrderService.lockById(dto.getOrderId(), WebFrameworkUtils.getLoginUserTenantCode());
        if (order == null || !ORDER_TYPE_PERSONAL.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())) {
            log.warn("个人订餐取消状态非法 orderId={}, orderNo={}, orderStatus={}, payStatus={}",
                    order.getId(), order.getOrderNo(), order.getOrderStatus(), order.getPayStatus());
            throw new BizErrorException("仅待支付订单可取消");
        }
        validateCancelRule(order);
        List<FoodMealOrderItem> items = listOrderItems(order.getId());
        releaseSoldQuantity(items);
        log.info("个人订餐取消库存已释放 orderId={}, orderNo={}, itemCount={}", order.getId(), order.getOrderNo(), items.size());
        order.setOrderStatus(ORDER_STATUS_CANCELLED);
        order.setCancelTime(LocalDateTime.now());
        order.setCancelReason(StrUtil.blankToDefault(dto.getCancelReason(), "用户取消"));
        updateOrder(order);
        syncTradeOrderCancel(order);
        log.info("个人订餐订单已取消 orderId={}, orderNo={}, memberId={}, orderStatus={}, cancelReason={}",
                order.getId(), order.getOrderNo(), order.getMemberId(), order.getOrderStatus(), order.getCancelReason());
        return toOrderVO(order, items);
    }

    @Transactional(readOnly = false)
    public FoodPersonalMealOrderVO applyRefund(FoodMealOrderRefundApplyDTO dto, String memberId) {
        FoodMealOrder order = foodMealOrderService.lockById(dto.getOrderId(), WebFrameworkUtils.getLoginUserTenantCode());
        if (order == null || !ORDER_TYPE_PERSONAL.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())) {
            return toOrderVO(order, listOrderItems(order.getId()));
        }
        if (!ORDER_STATUS_PAID.equals(order.getOrderStatus()) || !PAY_STATUS_PAID.equals(order.getPayStatus())) {
            throw new BizErrorException("仅已支付订单可申请退款");
        }
        validateCancelRule(order);
        order.setOrderStatus(ORDER_STATUS_REFUNDING);
        order.setRefundApplyTime(LocalDateTime.now());
        order.setRefundReason(StrUtil.blankToDefault(dto.getRefundReason(), "用户申请退款"));
        updateOrder(order);
        PayRefundCreateDTO refundDto = new PayRefundCreateDTO();
        refundDto.setBizScene(PayRecordBizSceneEnum.FOOD_MEAL_ORDER.getCode());
        refundDto.setBizOrderId(order.getId());
        refundDto.setRefundAmount(money(order.getUserPayAmount()));
        refundDto.setReason(order.getRefundReason());
        payRefundApi.createAndExecute(refundDto);
        return toOrderVO(order, listOrderItems(order.getId()));
    }

    /**
     * 管理端发起微信退款（财务统一 Facade）。
     *
     * @return 支付退款单主键
     */
    @Transactional(readOnly = false)
    public String triggerMealRefund(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_PERSONAL.equals(order.getOrderType())) {
            throw new BizErrorException("个人订餐订单不存在");
        }
        assertRefundable(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 refundAmount = dto.getRefundAmount();
        if (refundAmount == null || refundAmount.compareTo(BigDecimal.ZERO) <= 0) {
            refundAmount = money(order.getUserPayAmount());
        }
        PayRefundCreateDTO refundDto = new PayRefundCreateDTO();
        refundDto.setBizScene(PayRecordBizSceneEnum.FOOD_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 confirmMealRefund(FoodPersonalMealRefundDTO dto) {
        if (dto == null || StrUtil.isBlank(dto.getOrderId())) {
            throw new BizErrorException("订单ID不能为空");
        }
        foodPersonalMealRefundCoordinator.applyRefundSuccess(
                dto.getOrderId().trim(),
                StrUtil.blankToDefault(dto.getRefundReason(), "个人订餐退款"),
                null,
                null);
    }

    /** 微信退款成功回调：关闭订单并回滚菜品已售数量（幂等）。 */
    @Transactional(readOnly = false)
    public FoodPersonalMealOrderVO handleRefundSuccess(FoodMealOrderRefundSuccessDTO dto) {
        log.info("个人订餐退款回调开始 orderId={}, payOrderId={}, payOrderNo={}, refundAmount={}, refundTime={}",
                dto.getOrderId(), dto.getPayOrderId(), dto.getPayOrderNo(), dto.getRefundAmount(), dto.getRefundTime());
        FoodMealOrder order = lockOrderForRefund(dto);
        if (order == null) {
            log.warn("个人订餐退款回调失败，订单不存在 orderId={}, payOrderId={}, payOrderNo={}", dto.getOrderId(), dto.getPayOrderId(), dto.getPayOrderNo());
            throw new BizErrorException("个人订餐订单不存在");
        }
        if (!ORDER_TYPE_PERSONAL.equals(order.getOrderType())) {
            throw new BizErrorException("订单类型不匹配");
        }
        foodPersonalMealRefundCoordinator.applyRefundSuccess(dto);
        FoodMealOrder refreshed = foodMealOrderService.get(order.getId());
        return toOrderVO(refreshed != null ? refreshed : order, listOrderItems(order.getId()));
    }

    @Transactional(readOnly = false)
    public FoodPersonalMealOrderVO verify(FoodMealOrderVerifyDTO dto) {
        FoodMealOrder order = foodMealOrderService.lockById(dto.getOrderId(), WebFrameworkUtils.getLoginUserTenantCode());
        if (order == null || !ORDER_TYPE_PERSONAL.equals(order.getOrderType())) {
            throw new BizErrorException("个人订餐订单不存在");
        }
        if (ORDER_STATUS_PICKED.equals(order.getOrderStatus())) {
            return toOrderVO(order, listOrderItems(order.getId()));
        }
        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 toOrderVO(order, listOrderItems(order.getId()));
    }

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

    public FoodPersonalMealOrderVO getOrderDetail(String orderId, String memberId) {
        FoodMealOrder order = foodMealOrderService.get(orderId);
        if (order == null || !DEL_NORMAL.equals(order.getDelFlag()) || !ORDER_TYPE_PERSONAL.equals(order.getOrderType())) {
            return null;
        }
        if (StrUtil.isNotBlank(memberId) && !StrUtil.equals(memberId, order.getMemberId())) {
            return null;
        }
        return toOrderVO(order, listOrderItems(orderId));
    }

    private List<FoodMealMenu> listPublishedPersonalMenus(String restaurantId, LocalDate mealDate, String mealType) {
        return foodMealMenuService.listPublishedMenus(restaurantId, mealDate, mealType,
            Set.of(MENU_SCOPE_PUBLIC, MENU_SCOPE_ALL), mealTypeCandidates(mealType));
    }

    private FoodMealMenu getValidMenu(String menuId, LocalDate mealDate, String mealType) {
        FoodMealMenu menu = foodMealMenuService.get(menuId);
        if (menu == null || !DEL_NORMAL.equals(menu.getDelFlag()) || !MENU_STATUS_PUBLISHED.equals(menu.getPublishStatus())) {
            throw new BizErrorException("菜单不存在或未发布");
        }
        if (!mealDate.equals(menu.getMenuDate()) || !equalsMealType(menu.getMealType(), mealType)) {
            throw new BizErrorException("菜单日期或餐别不匹配");
        }
        if (!MENU_SCOPE_PUBLIC.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_PERSONAL));
        if (CollUtil.isEmpty(rules)) {
            return null;
        }
        return rules.stream()
            .filter(rule -> StrUtil.isBlank(rule.getServiceAreaId()) || StrUtil.isBlank(serviceAreaId) || StrUtil.equals(rule.getServiceAreaId(), serviceAreaId))
            .filter(rule -> StrUtil.isBlank(rule.getMealType()) || StrUtil.isBlank(mealType) || 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_PERSONAL.equals(rule.getScopeType())) {
            score += 1;
        }
        return score;
    }

    private List<FoodMealMenuItem> getValidMenuItems(FoodPersonalMealOrderCreateDTO dto, boolean forUpdate) {
        Set<String> requestIds = dto.getItems().stream()
            .map(FoodPersonalMealOrderCreateDTO.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(FoodPersonalMealOrderCreateDTO 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(FoodPersonalMealOrderCreateDTO.Item::getSpecId)
            .filter(StrUtil::isNotBlank)
            .collect(Collectors.toCollection(HashSet::new));
        Map<String, FoodMealMenuItemSpec> specMap = getSpecMap(requestSpecIds, forUpdate);
        Map<String, FoodPersonalMealOrderCreateDTO.Item> itemMap = new HashMap<>();
        Map<String, Integer> quantityMap = new HashMap<>();
        for (FoodPersonalMealOrderCreateDTO.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, FoodPersonalMealOrderCreateDTO.Item> entry : itemMap.entrySet()) {
            FoodPersonalMealOrderCreateDTO.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 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.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 void cancelStalePendingOrders(String memberId, String menuId) {
        List<FoodMealOrder> staleOrders = foodMealOrderService.listActiveByMemberIdAndMenuId(memberId, menuId,
            List.of(ORDER_STATUS_PENDING_PAY));
        if (CollUtil.isEmpty(staleOrders)) {
            return;
        }
        for (FoodMealOrder staleOrder : staleOrders) {
            List<FoodMealOrderItem> items = listOrderItems(staleOrder.getId());
            releaseSoldQuantity(items);
            staleOrder.setOrderStatus(ORDER_STATUS_CANCELLED);
            staleOrder.setCancelTime(LocalDateTime.now());
            staleOrder.setCancelReason("重复下单自动取消");
            foodMealOrderService.updateOrder(staleOrder);
            syncTradeOrderCancel(staleOrder);
            log.info("个人订餐自动取消旧订单 orderId={}, orderNo={}", staleOrder.getId(), staleOrder.getOrderNo());
        }
    }

    private FoodMealOrder buildOrder(FoodPersonalMealOrderCreateDTO dto, FoodMealMenu menu, BigDecimal totalAmount) {
        FoodMealOrder order = new FoodMealOrder();
        order.setId(IdUtil.getSnowflakeNextIdStr());
        order.setOrderNo(generateOrderNo());
        order.setOrderType(ORDER_TYPE_PERSONAL);
        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(dto.getMemberId());
        order.setTotalAmount(totalAmount);
        order.setDiscountAmount(ZERO);
        order.setCompanyPayAmount(ZERO);
        order.setUserPayAmount(totalAmount);
        order.setRefundAmount(ZERO);
        order.setOrderStatus(ORDER_STATUS_PENDING_PAY);
        order.setPayStatus(PAY_STATUS_UNPAID);
        order.setPickupCode(generatePickupCode());
        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<FoodMealOrderItem> items = new ArrayList<>();
        for (MealOrderRequestItem requestItem : requestItems) {
            FoodMealMenuItem menuItem = requestItem.menuItem();
            FoodMealMenuItemSpec spec = requestItem.spec();
            int quantity = requestItem.quantity();
            BigDecimal totalAmount = money(requestItem.salePrice()).multiply(BigDecimal.valueOf(quantity));
            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(money(requestItem.salePrice()));
            orderItem.setQuantity(quantity);
            orderItem.setTotalAmount(money(totalAmount));
            orderItem.setDiscountAmount(ZERO);
            orderItem.setCompanyPayAmount(ZERO);
            orderItem.setUserPayAmount(money(totalAmount));
            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_MEAL.getValue());
        dto.setTitle(StrUtil.blankToDefault(order.getServiceAreaName(), "个人订餐") + "-" + mealTypeLabel(order.getMealType()));
        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", "personal");
        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_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) {
        ensureTradeOrderLinked(order);
        if (StrUtil.isBlank(order.getTradeCenterOrderId())) {
            return;
        }
        TradeOrder tradeOrder = tradeOrderFacadeApi.get(order.getTradeCenterOrderId());
        if (tradeOrder != null && TradeOrderStatusEnum.PENDING_PAY.getValue().equals(tradeOrder.getStatus())) {
            tradeOrderFacadeApi.markPaid(order.getTradeCenterOrderId(), order.getTradeOrderId(), "WECHAT_MINI");
        }
    }

    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, false);
    }

    private void triggerFinanceSettleOnPaySuccess(FoodPersonalMealOrderPaidDTO dto, FoodMealOrder order) {
        if (dto == null || StrUtil.isBlank(dto.getOrderId())) {
            return;
        }
        String bizOrderId = dto.getOrderId().trim();
        String financePayOrderId = StrUtil.blankToDefault(dto.getPayOrderId(), null);
        if (StrUtil.isBlank(financePayOrderId) && order != null) {
            financePayOrderId = order.getTradeOrderId();
        }
        if (StrUtil.isBlank(financePayOrderId)) {
            log.warn("个人订餐支付成功缺少 financePayOrderId，跳过结算挂接 orderId={}", bizOrderId);
            return;
        }
        String bizScene = PayRecordBizSceneEnum.FOOD_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_MEAL_ORDER.getCode(), orderId.trim());
        } catch (Exception ex) {
            log.warn("个人订餐核销触发分账失败 orderId={}: {}", orderId, ex.getMessage());
        }
    }

    private static void assertRefundable(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 FoodMealOrder lockOrderForPaid(FoodPersonalMealOrderPaidDTO 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 List<FoodMealOrderItem> listOrderItems(String orderId) {
        return foodMealOrderItemService.listByOrderId(orderId);
    }

    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 FoodMealRestaurantVO mealServiceArea(String serviceAreaId) {
        if (StrUtil.isBlank(serviceAreaId)) {
            return null;
        }
        FoodMealServiceArea serviceArea = foodMealServiceAreaService.get(serviceAreaId);
        return serviceArea == null ? null : toMealRestaurantVO(serviceArea);
    }

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

    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 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 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.setServiceAreaId(item.getServiceAreaId());
        vo.setTemplateId(item.getTemplateId());
        vo.setTemplateItemId(item.getTemplateItemId());
        vo.setRestaurantId(item.getRestaurantId());
        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 FoodPersonalMealOrderVO toOrderVO(FoodMealOrder order, List<FoodMealOrderItem> items) {
        FoodPersonalMealOrderVO vo = new FoodPersonalMealOrderVO();
        vo.setId(order.getId());
        vo.setOrderNo(order.getOrderNo());
        vo.setServiceAreaId(order.getServiceAreaId());
        vo.setServiceAreaName(StrUtil.blankToDefault(order.getServiceAreaName(), serviceAreaName(order.getServiceAreaId())));
        vo.setRestaurantId(order.getRestaurantId());
        vo.setRestaurantName(vo.getServiceAreaName());
        vo.setMenuId(order.getMenuId());
        vo.setMealDate(order.getMenuDate());
        vo.setMealType(order.getMealType());
        vo.setTotalAmount(money(order.getTotalAmount()));
        vo.setUserPayAmount(money(order.getUserPayAmount()));
        vo.setOrderStatus(order.getOrderStatus());
        vo.setPayStatus(order.getPayStatus());
        vo.setPickupCode(order.getPickupCode());
        vo.setPayTime(order.getPayTime());
        vo.setCancelTime(order.getCancelTime());
        vo.setCancelReason(order.getCancelReason());
        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.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());
        vo.setItems(items.stream().map(this::toOrderItemVO).collect(Collectors.toList()));
        return vo;
    }

    private FoodPersonalMealOrderVO.Item toOrderItemVO(FoodMealOrderItem item) {
        FoodPersonalMealOrderVO.Item vo = new FoodPersonalMealOrderVO.Item();
        vo.setId(item.getId());
        vo.setMenuItemId(item.getMenuItemId());
        vo.setSpecId(item.getSpecId());
        vo.setSpecName(item.getSpecName());
        vo.setSpecDescription(item.getSpecDescription());
        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()));
        return vo;
    }

    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 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 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 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 void fillTrialSpecSnapshot(FoodPersonalMealTrialVO.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 BigDecimal money(BigDecimal amount) {
        if (amount == null) {
            return ZERO;
        }
        return amount.setScale(2, RoundingMode.HALF_UP);
    }

    private static String generateOrderNo() {
        return ORDER_NO_PREFIX + LocalDateTime.now().format(NO_TIME_FMT) + RandomUtil.randomNumbers(6);
    }

    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);
        }
    }
}
