MealOrderServiceImpl.java 15.2 KB
package com.infoloop.tianting.service.impl;

import com.infoloop.tianting.context.LoginContextHolder;
import com.infoloop.tianting.enums.QueryMealOrderStatusEnum;
import com.infoloop.tianting.exception.ClientEndExceptions;
import com.infoloop.tianting.exception.ErrorCodeEnum;
import com.infoloop.tianting.mealorderservice.*;
import com.infoloop.tianting.model.common.CreatedResult;
import com.infoloop.tianting.model.dto.MealOrderDTO;
import com.infoloop.tianting.model.vo.DeliveryRuleVO;
import com.infoloop.tianting.model.vo.DinerMealSuspensionRecordVO;
import com.infoloop.tianting.model.vo.MealOrderVO;
import com.infoloop.tianting.service.MealOrderService;
import com.infoloop.tianting.service.client.ClientInventoryServiceRpcClient;
import com.infoloop.tianting.service.client.DeliveryRuleServiceRpcClient;
import com.infoloop.tianting.service.client.MealOrderServiceRpcClient;
import com.infoloop.tianting.utils.DateUtil;
import com.infoloop.tianting.service.SuspensionService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.time.DayOfWeek;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;

@Slf4j
@Service
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
public class MealOrderServiceImpl implements MealOrderService {

    private final MealOrderServiceRpcClient mealOrderServiceRpcClient;

    private final ClientInventoryServiceRpcClient clientInventoryServiceRpcClient;

    private final DeliveryRuleServiceRpcClient deliveryRuleServiceRpcClient;

    private final SuspensionService suspensionService;

    @Override
    public List<MealOrderVO> queryMealOrders(MealOrderDTO.QueryMealOrderDTO queryMealOrderDTO) {
        final var loginInfo = LoginContextHolder.getLoginInfo();
        var mealOrderRpcResponses = mealOrderServiceRpcClient.queryMealOrdersByCondition(loginInfo.getEnterpriseId(), List.of(loginInfo.getId()), null, null);
        final var today = LocalDate.now();
        final var lastWeekStart = today.minusWeeks(1).with(DayOfWeek.MONDAY).atStartOfDay().atZone(DateUtil.CHINA_ZONE).toInstant().toEpochMilli();
        final var lastWeekEnd = today.minusWeeks(1).with(DayOfWeek.SUNDAY).atTime(LocalTime.MAX).atZone(DateUtil.CHINA_ZONE).toInstant().toEpochMilli();
        final var thisWeekStart = today.with(DayOfWeek.MONDAY).atStartOfDay().atZone(DateUtil.CHINA_ZONE).toInstant().toEpochMilli();
        final var thisWeekEnd = today.with(DayOfWeek.SUNDAY).atTime(LocalTime.MAX).atZone(DateUtil.CHINA_ZONE).toInstant().toEpochMilli();
        if (queryMealOrderDTO.getStatus() == QueryMealOrderStatusEnum.COMPLETED) {
            mealOrderRpcResponses = mealOrderRpcResponses.stream()
                    .filter(e -> e.getReserveDate() < lastWeekStart)
                    .toList();
        } else if (queryMealOrderDTO.getStatus() == QueryMealOrderStatusEnum.UNDER_WAY) {
            mealOrderRpcResponses = mealOrderRpcResponses.stream()
                    .filter(e -> e.getReserveDate() >= lastWeekStart && e.getReserveDate() <= lastWeekEnd)
                    .toList();
        } else if (queryMealOrderDTO.getStatus() == QueryMealOrderStatusEnum.BE_BOOKED) {
            mealOrderRpcResponses = mealOrderRpcResponses.stream()
                    .filter(e -> e.getReserveDate() >= thisWeekStart && e.getReserveDate() <= thisWeekEnd)
                    .toList();
        }
        return mealOrderRpcResponses.stream()
                .map(this::getMealOrderById)
                .toList();
    }

    @Override
    public CreatedResult<MealOrderVO> createMealOrder(MealOrderDTO.CreateMealOrderDTO createMealOrderDTO) {
        final var loginInfo = LoginContextHolder.getLoginInfo();
        final var deliveryRule = this.getDeliveryRule();
        final var now = LocalDateTime.now();
        final var nowWithinRange = isNowWithinRange(deliveryRule, now);
        if (!nowWithinRange) {
            throw ClientEndExceptions.IncorrectRequestValue.build(ErrorCodeEnum.MEAL_ORDER_TIME_NOT_IN_RANGE);
        }

        // 1. 获取订餐人信息
        final var dinerById = mealOrderServiceRpcClient.getDinerById(loginInfo.getEnterpriseId(), createMealOrderDTO.getDinerId());
        if (dinerById == null) {
            throw ClientEndExceptions.IncorrectRequestValue.build(ErrorCodeEnum.DINER_NOT_FOUND);
        }

        // 2. 校验订餐人关联关系
        final var mpAccountDinerRefs = mealOrderServiceRpcClient.queryMpAccountDinerRefsByCondition(loginInfo.getEnterpriseId(), Collections.singletonList(loginInfo.getId()), Collections.singletonList(createMealOrderDTO.getDinerId()));
        if (mpAccountDinerRefs.size() != 1) {
            throw ClientEndExceptions.IncorrectRequestValue.build(ErrorCodeEnum.DINER_NOT_FOUND);
        }

        // 3. 校验客户状态
        final var clientById = clientInventoryServiceRpcClient.getClientById(loginInfo.getEnterpriseId(), dinerById.getClientId());
        if (clientById.getClient().getStatus() != 1) {
            throw ClientEndExceptions.IncorrectRequestValue.build(ErrorCodeEnum.CLIENT_NOT_ENABLED);
        }

        // 4. 校验班级信息
        final var gradeClassById = mealOrderServiceRpcClient.getGradeClassById(loginInfo.getEnterpriseId(), dinerById.getClassId());
        if (gradeClassById == null) {
            throw ClientEndExceptions.IncorrectRequestValue.build(ErrorCodeEnum.GRADE_CLASS_NOT_FOUND);
        }

        // 5. 校验停餐记录(学校/班级/学生三个维度最新记录的多段并集区间)
        final var intervals = suspensionService.getSuspensionIntervalsForDiner(
                loginInfo.getEnterpriseId(),
                dinerById.getClientId(),
                dinerById.getClassId(),
                dinerById.getId()
        );
        if (intervals != null && !intervals.isEmpty()) {
            // 校验每个订餐日期是否在停餐时间段内
            for (var meal : createMealOrderDTO.getMeals()) {
                final long mealDateMs = DateUtil.parseDate(meal.getDate()).getTime();
                final boolean inSuspension = intervals.stream()
                        .anyMatch(i -> mealDateMs >= i.startAt && mealDateMs <= i.endAt);
                if (inSuspension) {
                    throw ClientEndExceptions.IncorrectRequestValue.build(ErrorCodeEnum.DINER_MEAL_SUSPENDED);
                }
            }
        }

        // 6. 校验菜单信息
        final var latestMealMenuRecords = mealOrderServiceRpcClient.queryLatestMealMenuRecordsByClientIds(loginInfo.getEnterpriseId(), Collections.singletonList(dinerById.getClientId()));
        if (latestMealMenuRecords.isEmpty()) {
//            throw ClientEndExceptions.IncorrectRequestValue.build(ErrorCodeEnum.MEAL_MENU_NOT_FOUND);
        }

        // 7. 校验是否已预订
        final var reserveDinerMealOrders = mealOrderServiceRpcClient.queryReserveMealOrdersByDinerIds(loginInfo.getEnterpriseId(), Collections.singletonList(createMealOrderDTO.getDinerId()));
        if (!reserveDinerMealOrders.isEmpty()) {
            throw ClientEndExceptions.IncorrectRequestValue.build(ErrorCodeEnum.DINER_ALREADY_RESERVED);
        }

        // 8. 创建订单
        final var mealOrder = mealOrderServiceRpcClient.createMealOrder(loginInfo, MealOrderCreation.newBuilder()
                .setClientId(dinerById.getClientId())
                .setMenuId(latestMealMenuRecords.isEmpty() ? 0 : latestMealMenuRecords.get(0).getId())
                .setAccountId(loginInfo.getId())
                .setOrderMethod(MealOrderOrderMethodEnum.MEAL_ORDER_METHOD_MICRO)
                .setDinerId(createMealOrderDTO.getDinerId())
                .setDinerName(dinerById.getName())
                .setClientName(clientById.getClient().getName())
                .setGradeName(gradeClassById.getGradeName())
                .setClassName(gradeClassById.getClassName())
                .setStudentNo(dinerById.getStudentNo())
                .setReserveDate(now.atZone(DateUtil.CHINA_ZONE).toInstant().toEpochMilli())
                .addAllMeals(createMealOrderDTO.getMeals().stream().map(e -> MenuSchedule.newBuilder()
                        .setDate(e.getDate())
                        .setMenu(e.getMenu())
                        .build()).toList())
                .build());
        return CreatedResult.<MealOrderVO>builder().id(mealOrder.getId()).isCreated(mealOrder.getIsCreated()).build();
    }

    @Override
    public MealOrderVO getMealOrderById(SingleMealOrderRpcResponse mealOrder) {
        return MealOrderVO.builder()
                .id(mealOrder.getId())
                .clientId(mealOrder.getClientId())
                .menuId(mealOrder.getMenuId())
                .accountId(mealOrder.getAccountId())
                .orderMethod(mealOrder.getOrderMethod())
                .dinerId(mealOrder.getDinerId())
                .dinerName(mealOrder.getDinerName())
                .clientName(mealOrder.getClientName())
                .gradeName(mealOrder.getGradeName())
                .className(mealOrder.getClassName())
                .studentNo(mealOrder.getStudentNo())
                .reserveDate(DateUtil.formatDate(mealOrder.getReserveDate()))
                .meals(mealOrder.getMealsList().stream().map(e -> MealOrderVO.Meals.builder().date(e.getDate()).menu(e.getMenu()).build()).toList())
                .build();
    }

    @Override
    public DeliveryRuleVO getDeliveryRule() {
        final var rule = deliveryRuleServiceRpcClient.getDefaultDeliveryTemplateAndRule(LoginContextHolder.getEnterpriseId()).getRule();
        if (rule.getConfigJson().getRulesList().isEmpty()) {
            return DeliveryRuleVO.builder().build();
        }
        return DeliveryRuleVO.builder()
                .startTime(rule.getConfigJson().getRules(0).getStartTime())
                .endTime(rule.getConfigJson().getRules(0).getEndTime())
                .monday(rule.getConfigJson().getRules(0).getMonday())
                .tuesday(rule.getConfigJson().getRules(0).getTuesday())
                .wednesday(rule.getConfigJson().getRules(0).getWednesday())
                .thursday(rule.getConfigJson().getRules(0).getThursday())
                .friday(rule.getConfigJson().getRules(0).getFriday())
                .saturday(rule.getConfigJson().getRules(0).getSaturday())
                .sunday(rule.getConfigJson().getRules(0).getSunday())
                .build();
    }

    private static boolean isNowWithinRange(DeliveryRuleVO rule, LocalDateTime now) {
        if (rule == null || rule.getStartTime() == null || rule.getEndTime() == null) {
            return false;
        }
        final var startTime = LocalTime.parse(rule.getStartTime(), DateUtil.HH_MM_SS);
        final var endTime = LocalTime.parse(rule.getEndTime(), DateUtil.HH_MM_SS);
        final var enabledDays = new ArrayList<DayOfWeek>();
        if (Boolean.TRUE.equals(rule.getMonday())) enabledDays.add(DayOfWeek.MONDAY);
        if (Boolean.TRUE.equals(rule.getTuesday())) enabledDays.add(DayOfWeek.TUESDAY);
        if (Boolean.TRUE.equals(rule.getWednesday())) enabledDays.add(DayOfWeek.WEDNESDAY);
        if (Boolean.TRUE.equals(rule.getThursday())) enabledDays.add(DayOfWeek.THURSDAY);
        if (Boolean.TRUE.equals(rule.getFriday())) enabledDays.add(DayOfWeek.FRIDAY);
        if (Boolean.TRUE.equals(rule.getSaturday())) enabledDays.add(DayOfWeek.SATURDAY);
        if (Boolean.TRUE.equals(rule.getSunday())) enabledDays.add(DayOfWeek.SUNDAY);
        if (enabledDays.isEmpty()) {
            return false;
        }
        final var minDay = Collections.min(enabledDays);
        final var maxDay = Collections.max(enabledDays);
        final var today = now.toLocalDate();
        final var startDateTime = getThisWeekDate(today, minDay).atTime(startTime);
        final var endDateTime = getThisWeekDate(today, maxDay).atTime(endTime);
        return !now.isBefore(startDateTime) && !now.isAfter(endDateTime);
    }

    private static LocalDate getThisWeekDate(LocalDate now, DayOfWeek day) {
        LocalDate monday = now.with(DayOfWeek.MONDAY);
        return monday.plusDays(day.getValue() - 1L); // MONDAY 是 1,SUNDAY 是 7
    }

    @Override
    public List<DinerMealSuspensionRecordVO> queryDinerMealSuspensionRecordsByCondition() {
        final var loginInfo = LoginContextHolder.getLoginInfo();

        // 1. 获取当前登录用户关联的所有订餐人
        var mpAccountDinerRefs = mealOrderServiceRpcClient.queryMpAccountDinerRefsByCondition(
                loginInfo.getEnterpriseId(),
                Collections.singletonList(loginInfo.getId()),
                Collections.emptyList()
        );

        if (mpAccountDinerRefs.isEmpty()) {
            return Collections.emptyList();
        }

        // 2. 获取所有订餐人ID
        var dinerIds = mpAccountDinerRefs.stream()
                .map(SingleMpAccountDinerRefRpcResponse::getDinerId)
                .toList();

        // 3. 获取所有订餐人信息
        var diners = mealOrderServiceRpcClient.getDinersByIds(loginInfo.getEnterpriseId(), dinerIds);

        // 4. 使用公共停餐服务:批量计算每个订餐人的多段停餐区间
        Map<Integer, SingleDinerRpcResponse> dinerIdToDiner = diners.stream()
                .collect(Collectors.toMap(SingleDinerRpcResponse::getId, Function.identity()));

        Map<Integer, List<SuspensionService.SuspensionInterval>> dinerIdToIntervals =
                suspensionService.getSuspensionIntervalsForDiners(loginInfo.getEnterpriseId(), diners);

        List<DinerMealSuspensionRecordVO> result = new ArrayList<>();
        for (Map.Entry<Integer, List<SuspensionService.SuspensionInterval>> entry : dinerIdToIntervals.entrySet()) {
            Integer dinerId = entry.getKey();
            SingleDinerRpcResponse dinerInfo = dinerIdToDiner.get(dinerId);
            if (dinerInfo == null) {
                continue;
            }
            List<SuspensionService.SuspensionInterval> intervals = entry.getValue();
            if (intervals == null || intervals.isEmpty()) {
                continue;
            }
            List<DinerMealSuspensionRecordVO.Interval> voIntervals = intervals.stream()
                    .map(i -> DinerMealSuspensionRecordVO.Interval.builder()
                            .startAt(new Date(i.startAt))
                            .endAt(new Date(i.endAt))
                            .comment("")
                            .build())
                    .toList();

            result.add(DinerMealSuspensionRecordVO.builder()
                    .enterpriseId(Long.valueOf(dinerInfo.getEnterpriseId()))
                    .clientId(Long.valueOf(dinerInfo.getClientId()))
                    .classId(Long.valueOf(dinerInfo.getClassId()))
                    .dinerId(Long.valueOf(dinerInfo.getId()))
                    .dinerName(dinerInfo.getName())
                    .studentNo(dinerInfo.getStudentNo())
                    .intervals(voIntervals)
                    .build());
        }
        return result;
    }

    /**
     * 将RPC响应转换为VO对象
     */
    // 已改为统一从 SuspensionService 获取并集窗口,不再需要单条转换
}