OrderReminderTask.java 13.4 KB
package com.infoloop.tianting.logic.task;

import com.infoloop.tianting.deliveryruleservice.Rule;
import com.infoloop.tianting.mealorderservice.PendingOrderSubscriptionMessageRpcResponse;
import com.infoloop.tianting.mealorderservice.SingleMpAccountRpcResponse;
import com.infoloop.tianting.model.dto.WxUserDto.SendSubscriptionMessageResponse;
import com.infoloop.tianting.service.OrderReminderService;
import com.infoloop.tianting.service.client.DeliveryRuleServiceRpcClient;
import com.infoloop.tianting.service.client.MealOrderServiceRpcClient;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

import java.time.DayOfWeek;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;

import static com.infoloop.tianting.constant.ConfigConstants.ASYNC_EXECUTOR;

/**
 * 订餐提醒定时任务
 * 在订餐周期开启前(周一 9:00)向已授权的家长发送微信服务通知
 * 
 * 推送逻辑:
 * 1. 查询所有待发送的订阅消息(未删除的记录)
 * 2. 按 openId 去重,每个用户只推送一次
 * 3. 通过 openId 查询活跃的小程序用户
 * 4. 根据配送规则配置生成订餐时间提示
 * 5. 发送消息,发送成功后标记为已删除
 */
@Slf4j
@Component
public class OrderReminderTask {

    private static final int ENTERPRISE_ID = 449;
    
    /**
     * 温馨提醒内容
     */
    private static final String TIPS = "请点击卡片,准时进入小程序完成预订";

    /**
     * 订餐时间配置
     */
    private record OrderTimeConfig(String startTime, String endTime) {}

    private final MealOrderServiceRpcClient mealOrderServiceRpcClient;
    private final DeliveryRuleServiceRpcClient deliveryRuleServiceRpcClient;
    private final OrderReminderService orderReminderService;
    private final Executor asyncExecutor;

    public OrderReminderTask(
            MealOrderServiceRpcClient mealOrderServiceRpcClient,
            DeliveryRuleServiceRpcClient deliveryRuleServiceRpcClient,
            OrderReminderService orderReminderService,
            @Qualifier(ASYNC_EXECUTOR) Executor asyncExecutor) {
        this.mealOrderServiceRpcClient = mealOrderServiceRpcClient;
        this.deliveryRuleServiceRpcClient = deliveryRuleServiceRpcClient;
        this.orderReminderService = orderReminderService;
        this.asyncExecutor = asyncExecutor;
    }

    /**
     * 每周一 9:00 执行订餐提醒推送
     */
    @Scheduled(cron = "0 0 9 * * MON")
    public void executeTask() {
        log.info("OrderReminderTask started");
        
        try {
            // 1. 获取配送规则配置,用于生成订餐时间提示
            OrderTimeConfig orderTimeConfig = getOrderTimeConfigFromRule();
            if (orderTimeConfig == null) {
                log.warn("Failed to get order time config from rule, using default");
                orderTimeConfig = new OrderTimeConfig("请查看小程序", "请查看小程序");
            }
            log.info("Order time config: startTime={}, endTime={}", orderTimeConfig.startTime(), orderTimeConfig.endTime());

            // 2. 查询所有待发送的订阅消息(未删除的记录)
            List<PendingOrderSubscriptionMessageRpcResponse> pendingMessages = 
                    mealOrderServiceRpcClient.queryPendingOrderSubscriptionMessages(ENTERPRISE_ID);
            
            if (pendingMessages.isEmpty()) {
                log.info("No pending order subscription messages to send");
                return;
            }
            log.info("Found {} pending order subscription messages", pendingMessages.size());

            // 3. 提取所有 openId,查询活跃的小程序用户
            Set<String> openIds = pendingMessages.stream()
                    .map(PendingOrderSubscriptionMessageRpcResponse::getOpenId)
                    .collect(Collectors.toSet());
            
            Map<String, SingleMpAccountRpcResponse> activeMpAccountMap = getActiveMpAccountMap(openIds);
            log.info("Found {} active mp accounts from {} openIds", activeMpAccountMap.size(), openIds.size());

            int totalMessages = pendingMessages.size();
            AtomicInteger skippedInactive = new AtomicInteger(0);
            AtomicInteger skippedDuplicate = new AtomicInteger(0);
            AtomicInteger sentCount = new AtomicInteger(0);
            List<Long> sentMessageIds = Collections.synchronizedList(new ArrayList<>());
            Set<String> sentOpenIds = ConcurrentHashMap.newKeySet(); // 线程安全的 Set,用于 openId 去重

            // 4. 按 openId 去重,筛选出需要发送的消息
            List<PendingOrderSubscriptionMessageRpcResponse> messagesToSend = new ArrayList<>();
            for (PendingOrderSubscriptionMessageRpcResponse message : pendingMessages) {
                String openId = message.getOpenId();

                // 检查用户是否活跃
                if (!activeMpAccountMap.containsKey(openId)) {
                    log.debug("Skipping message {} - user {} is not active", message.getId(), openId);
                    skippedInactive.incrementAndGet();
                    sentMessageIds.add(message.getId());
                    continue;
                }

                // openId 去重:每个用户只推送一次
                if (!sentOpenIds.add(openId)) {
                    log.debug("Skipping message {} - already queued for openId: {}", message.getId(), openId);
                    skippedDuplicate.incrementAndGet();
                    sentMessageIds.add(message.getId());
                    continue;
                }

                messagesToSend.add(message);
            }

            log.info("Messages to send: {}, Skipped inactive: {}, Skipped duplicate: {}",
                    messagesToSend.size(), skippedInactive.get(), skippedDuplicate.get());

            // 5. 多线程并发发送消息
            final OrderTimeConfig finalOrderTimeConfig = orderTimeConfig;
            List<CompletableFuture<Void>> futures = messagesToSend.stream()
                    .map(message -> CompletableFuture.runAsync(() -> {
                        try {
                            boolean success = sendReminderMessage(message.getOpenId(), message, finalOrderTimeConfig);
                            if (success) {
                                sentMessageIds.add(message.getId());
                                sentCount.incrementAndGet();
                            }
                        } catch (Exception e) {
                            log.error("Failed to send reminder to openId: {}, messageId: {}",
                                    message.getOpenId(), message.getId(), e);
                        }
                    }, asyncExecutor))
                    .toList();

            // 等待所有任务完成
            CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();

            // 6. 批量标记已发送的消息(逻辑删除)
            if (!sentMessageIds.isEmpty()) {
                int affectedRows = mealOrderServiceRpcClient.batchDeleteOrderSubscriptionMessagesByIds(
                        ENTERPRISE_ID, sentMessageIds);
                log.info("Marked {} messages as sent", affectedRows);
            }

            log.info("OrderReminderTask completed. Total: {}, Sent: {}, SkippedInactive: {}, SkippedDuplicate: {}",
                    totalMessages, sentCount.get(), skippedInactive.get(), skippedDuplicate.get());

        } catch (Exception e) {
            log.error("OrderReminderTask failed", e);
        }
    }

    /**
     * 从配送规则配置中获取订餐时间配置
     * 返回开始时间和结束时间,格式:周x xx:xx
     */
    private OrderTimeConfig getOrderTimeConfigFromRule() {
        try {
            final var response = deliveryRuleServiceRpcClient.getDefaultDeliveryTemplateAndRule(ENTERPRISE_ID);
            final var rule = response.getRule();
            if (rule == null || rule.getConfigJson().getRulesList().isEmpty()) {
                log.warn("Delivery rule not configured");
                return null;
            }

            final var cfg = rule.getConfigJson().getRules(0);
            final String startTimeStr = cfg.getStartTime();
            final String endTimeStr = cfg.getEndTime();
            if (startTimeStr == null || endTimeStr == null) {
                log.warn("Delivery rule time config incomplete");
                return null;
            }

            // 获取启用的日期
            List<DayOfWeek> enabledDays = getEnabledDays(cfg);
            if (enabledDays.isEmpty()) {
                log.warn("No enabled days in delivery rule");
                return null;
            }

            // 格式化时间(去掉秒)
            String startTime = formatTime(startTimeStr);
            String endTime = formatTime(endTimeStr);

            // 获取第一个和最后一个启用日
            DayOfWeek firstDay = enabledDays.get(0);
            DayOfWeek lastDay = enabledDays.get(enabledDays.size() - 1);

            // 计算本周的实际日期(微信time类型需要具体日期格式)
            LocalDate today = LocalDate.now();
            LocalDate startDate = today.with(java.time.temporal.TemporalAdjusters.nextOrSame(firstDay));
            LocalDate endDate = today.with(java.time.temporal.TemporalAdjusters.nextOrSame(lastDay));
            
            // 如果结束日期在开始日期之前,说明跨周了,结束日期加一周
            if (endDate.isBefore(startDate)) {
                endDate = endDate.plusWeeks(1);
            }

            // 格式化为微信接受的格式:yyyy年MM月dd日 HH:mm
            java.time.format.DateTimeFormatter dateFormatter = java.time.format.DateTimeFormatter.ofPattern("yyyy年MM月dd日");
            String formattedStartTime = startDate.format(dateFormatter) + " " + startTime;
            String formattedEndTime = endDate.format(dateFormatter) + " " + endTime;
            
            return new OrderTimeConfig(formattedStartTime, formattedEndTime);

        } catch (Exception e) {
            log.error("Failed to get order time config from rule", e);
            return null;
        }
    }

    /**
     * 获取启用的日期列表(按周一到周日排序)
     */
    private List<DayOfWeek> getEnabledDays(Rule cfg) {
        List<DayOfWeek> enabledDays = new ArrayList<>();
        if (Boolean.TRUE.equals(cfg.getMonday())) enabledDays.add(DayOfWeek.MONDAY);
        if (Boolean.TRUE.equals(cfg.getTuesday())) enabledDays.add(DayOfWeek.TUESDAY);
        if (Boolean.TRUE.equals(cfg.getWednesday())) enabledDays.add(DayOfWeek.WEDNESDAY);
        if (Boolean.TRUE.equals(cfg.getThursday())) enabledDays.add(DayOfWeek.THURSDAY);
        if (Boolean.TRUE.equals(cfg.getFriday())) enabledDays.add(DayOfWeek.FRIDAY);
        if (Boolean.TRUE.equals(cfg.getSaturday())) enabledDays.add(DayOfWeek.SATURDAY);
        if (Boolean.TRUE.equals(cfg.getSunday())) enabledDays.add(DayOfWeek.SUNDAY);
        return enabledDays;
    }

    /**
     * 格式化时间字符串(HH:mm:ss -> HH:mm)
     */
    private String formatTime(String timeStr) {
        if (timeStr == null || timeStr.length() < 5) {
            return timeStr;
        }
        // 取前5位 HH:mm
        return timeStr.substring(0, 5);
    }

    /**
     * 获取活跃的小程序用户 Map(openId -> MpAccount)
     */
    private Map<String, SingleMpAccountRpcResponse> getActiveMpAccountMap(Set<String> openIds) {
        if (CollectionUtils.isEmpty(openIds)) {
            return Collections.emptyMap();
        }
        List<SingleMpAccountRpcResponse> allAccounts = mealOrderServiceRpcClient.queryAllActiveMpAccounts(ENTERPRISE_ID);
        return allAccounts.stream()
                .filter(account -> openIds.contains(account.getOpenId()))
                .collect(Collectors.toMap(SingleMpAccountRpcResponse::getOpenId, account -> account, (a, b) -> a));
    }

    /**
     * 发送单条提醒消息
     * 
     * 小程序模板字段:
     * - time4: 开始时间(周x xx:xx)
     * - time5: 结束时间(周x xx:xx)
     * - thing1: 温馨提醒
     */
    private boolean sendReminderMessage(String openId, PendingOrderSubscriptionMessageRpcResponse message, OrderTimeConfig orderTimeConfig) {
        String jumpPath = message.getJumpPath();
        
        // 发送消息
        SendSubscriptionMessageResponse response = orderReminderService.sendOrderReminder(
                openId,
                jumpPath,
                orderTimeConfig.startTime(),     // time4: 开始时间
                orderTimeConfig.endTime(),       // time5: 结束时间
                TIPS                             // thing1: 温馨提醒
        );
        
        // 判断发送结果
        if (response.getErrcode() != null && response.getErrcode() == 0) {
            return true;
        }
        
        // 记录失败原因
        log.warn("Failed to send reminder. openId: {}, errcode: {}, errmsg: {}", 
                openId, response.getErrcode(), response.getErrmsg());
        return false;
    }

}