MealOrderServiceImpl.java 19.4 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 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.Instant;
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.Objects;
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;

    @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. 校验停餐记录
        var currentTime = System.currentTimeMillis();
        // 5.1 先查询学校级别的停餐信息
        var schoolSuspensionRecords = mealOrderServiceRpcClient.queryDinerMealSuspensionRecordsByCondition(
                loginInfo.getEnterpriseId(),
                List.of(dinerById.getClientId()),
                null,
                null
        );

        if (schoolSuspensionRecords != null && !schoolSuspensionRecords.isEmpty()) {
            var latestSchoolSuspension = schoolSuspensionRecords.stream()
                    .max((a, b) -> Long.compare(a.getCreatedAt(), b.getCreatedAt()))
                    .orElse(null);

            if (latestSchoolSuspension != null &&
                    currentTime >= latestSchoolSuspension.getStartAt() &&
                    currentTime <= latestSchoolSuspension.getEndAt()) {
                throw ClientEndExceptions.IncorrectRequestValue.build(ErrorCodeEnum.DINER_MEAL_SUSPENDED);
            }
        }

        // 5.2 再查询学生级别的停餐信息
        var studentSuspensionRecords = mealOrderServiceRpcClient.queryDinerMealSuspensionRecordsByCondition(
                loginInfo.getEnterpriseId(),
                List.of(dinerById.getClientId()),
                List.of(dinerById.getClassId()),
                List.of(dinerById.getId())
        );

        if (studentSuspensionRecords != null && !studentSuspensionRecords.isEmpty()) {
            var latestStudentSuspension = studentSuspensionRecords.stream()
                    .max((a, b) -> Long.compare(a.getCreatedAt(), b.getCreatedAt()))
                    .orElse(null);

            if (latestStudentSuspension != null &&
                    currentTime >= latestStudentSuspension.getStartAt() &&
                    currentTime <= latestStudentSuspension.getEndAt()) {
                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);
        var dinerById = diners.stream().collect(Collectors.toMap(SingleDinerRpcResponse::getId, diner -> diner));

        // 4. 获取所有不重复的clientId
        var uniqueClientIds = diners.stream()
                .map(diner -> Long.valueOf(diner.getClientId()))
                .distinct()
                .toList();

        // 5. 批量获取所有学校的停餐信息
        final var now = Instant.now();
        var schoolSuspensionRecords = mealOrderServiceRpcClient.queryDinerMealSuspensionRecordsByCondition(
                loginInfo.getEnterpriseId(),
                null,
                null,
                null
        ).stream().filter(r -> r.getClassId() == 0).filter(r -> r.getEndAt() > now.toEpochMilli()).toList();

        // 6. 批量获取所有学生的停餐信息
        var studentSuspensionRecords = mealOrderServiceRpcClient.queryDinerMealSuspensionRecordsByCondition(
                loginInfo.getEnterpriseId(),
                null,
                null,
                dinerIds
        ).stream().filter(r -> r.getEndAt() > now.toEpochMilli()).toList();

        // 7. 构建学校停餐信息Map
        var schoolSuspensionMap = schoolSuspensionRecords.stream()
                .collect(Collectors.groupingBy(
                        SingleDinerMealSuspensionRecordRpcResponse::getClientId,
                        Collectors.collectingAndThen(
                                Collectors.maxBy((a, b) -> Long.compare(a.getCreatedAt(), b.getCreatedAt())),
                                opt -> opt.map(o -> convertToVO(o, dinerById)).orElse(null)
                        )
                ));

        // 8. 构建学生停餐信息Map
        var studentSuspensionMap = studentSuspensionRecords.stream()
                .collect(Collectors.groupingBy(
                        SingleDinerMealSuspensionRecordRpcResponse::getDinerId,
                        Collectors.collectingAndThen(
                                Collectors.maxBy((a, b) -> Long.compare(a.getCreatedAt(), b.getCreatedAt())),
                                opt -> opt.map(o -> convertToVO(o, dinerById)).orElse(null)
                        )
                ));

        // 9. 获取每个订餐人的停餐信息
        return diners.stream()
                .map(diner -> {
                    // 获取学校级别的停餐信息
                    var schoolSuspension = schoolSuspensionMap.get(Long.valueOf(diner.getClientId()).intValue());
                    // 获取学生级别的停餐信息
                    var studentSuspension = studentSuspensionMap.get(Long.valueOf(diner.getId()).intValue());

                    // 如果两个都没有停餐信息,返回null
                    if (schoolSuspension == null && studentSuspension == null) {
                        return null;
                    }

                    // 如果只有学校级别的停餐信息
                    if (schoolSuspension != null && studentSuspension == null) {
                        return schoolSuspension;
                    }

                    // 如果只有学生级别的停餐信息
                    if (schoolSuspension == null && studentSuspension != null) {
                        return studentSuspension;
                    }

                    // 如果两个都有停餐信息,合并时间范围
                    return DinerMealSuspensionRecordVO.builder()
                            .id(schoolSuspension.getId())
                            .enterpriseId(schoolSuspension.getEnterpriseId())
                            .clientId(schoolSuspension.getClientId())
                            .classId(studentSuspension.getClassId())
                            .dinerId(studentSuspension.getDinerId())
                            .dinerName(studentSuspension.getDinerName())
                            .studentNo(studentSuspension.getStudentNo())
                            .startAt(new Date(Math.min(schoolSuspension.getStartAt().getTime(), studentSuspension.getStartAt().getTime())))
                            .endAt(new Date(Math.max(schoolSuspension.getEndAt().getTime(), studentSuspension.getEndAt().getTime())))
                            .comment("")
                            .build();
                })
                .filter(Objects::nonNull)
                .distinct()
                .toList();
    }

    /**
     * 将RPC响应转换为VO对象
     */
    private DinerMealSuspensionRecordVO convertToVO(SingleDinerMealSuspensionRecordRpcResponse response,
                                                    Map<Integer, SingleDinerRpcResponse> dinerById) {
        if (response == null) {
            return null;
        }

        return DinerMealSuspensionRecordVO.builder()
                .id(Long.valueOf(response.getId()))
                .enterpriseId(Long.valueOf(response.getEnterpriseId()))
                .clientId(Long.valueOf(response.getClientId()))
                .classId(Long.valueOf(response.getClassId()))
                .dinerId(Long.valueOf(response.getDinerId()))
                .dinerName(dinerById.get(response.getDinerId()) != null ? dinerById.get(response.getDinerId()).getName() : null)
                .studentNo(dinerById.get(response.getDinerId()) != null ? dinerById.get(response.getDinerId()).getStudentNo() : null)
                .startAt(new Date(response.getStartAt()))
                .endAt(new Date(response.getEndAt()))
                .comment(response.getComment())
                .build();
    }
}