Jelajahi Sumber

Phase13: FollowUpTask补T+21/T+30,新增PreClassReminderTask(T-7/T-3/T-1)

liaoxg 2 minggu lalu
induk
melakukan
d8111354e8

+ 6 - 0
train-backend/src/main/java/com/train/service/FollowUpTask.java

@@ -49,6 +49,12 @@ public class FollowUpTask {
             if (elapsedDays >= 7) {
                 ensureLog(plan.getUid(), "followup_t7", "T+7 回访:7 天行动计划完成度自检,欢迎约 1v1");
             }
+            if (elapsedDays >= 21) {
+                ensureLog(plan.getUid(), "followup_t21", "T+21 跟进:约线上复盘会(教练陪跑入口)");
+            }
+            if (elapsedDays >= 30) {
+                ensureLog(plan.getUid(), "followup_t30", "T+30 留存统计与续课推荐:L1/L2 工作坊与系统营");
+            }
         }
         log.info("FollowUpTask 完成一轮 T+ 跟进扫描,共 {} 条计划", plans.size());
     }

+ 99 - 0
train-backend/src/main/java/com/train/service/PreClassReminderTask.java

@@ -0,0 +1,99 @@
+package com.train.service;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.train.entity.TrainAuditLog;
+import com.train.entity.TrainClass;
+import com.train.entity.TrainUser;
+import com.train.mapper.TrainAuditLogMapper;
+import com.train.mapper.TrainClassMapper;
+import com.train.mapper.TrainUserMapper;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Component;
+
+import javax.annotation.Resource;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * 课前提醒定时任务(FR-PRE-02):扫描已设置 startAt 的 active 班次,
+ * 对已进班的学员在 T-7 / T-3 / T-1 触发提醒,落 audit_log(幂等)。
+ * test-mode:仅打日志;生产可在此处调用 SubscribeMessageService.sendToUser 推送订阅消息。
+ *
+ * 触发点判定:班次 startAt 为基准,相对天数落入窗口且当天未触发过则落账一次。
+ */
+@Slf4j
+@Component
+public class PreClassReminderTask {
+
+    @Resource
+    private TrainClassMapper trainClassMapper;
+    @Resource
+    private TrainUserMapper trainUserMapper;
+    @Resource
+    private TrainAuditLogMapper trainAuditLogMapper;
+
+    private static final long DAY_MILLIS = 24L * 60 * 60 * 1000;
+
+    /** 每日 10:00 执行(错开 FollowUpTask 的 09:30) */
+    @Scheduled(cron = "0 0 10 * * ?")
+    public void runPreClassReminder() {
+        List<TrainClass> classes = trainClassMapper.selectList(
+                new LambdaQueryWrapper<TrainClass>()
+                        .eq(TrainClass::getStatus, "active")
+                        .isNotNull(TrainClass::getStartAt));
+        Date now = new Date();
+        int totalReminders = 0;
+        for (TrainClass cls : classes) {
+            if (cls.getStartAt() == null || cls.getStartAt().getTime() <= now.getTime()) {
+                continue; // 已开课或未设时间
+            }
+            long daysAhead = (cls.getStartAt().getTime() - now.getTime()) / DAY_MILLIS;
+            String milestone = null;
+            String detail = null;
+            if (daysAhead >= 6 && daysAhead <= 8) {
+                milestone = "preclass_t7";
+                detail = "T-7 课前提醒:距离「" + cls.getName() + "」还有 7 天,请预装 WorkBuddy 并准备三本账数据";
+            } else if (daysAhead >= 2 && daysAhead <= 4) {
+                milestone = "preclass_t3";
+                detail = "T-3 课前提醒:距离「" + cls.getName() + "」还有 3 天,请完成课前 3 件事(装机/数据/最乱一本账)";
+            } else if (daysAhead >= 0 && daysAhead <= 1) {
+                milestone = "preclass_t1";
+                detail = "T-1 课前提醒:明天「" + cls.getName() + "」开课," + (cls.getPlace() == null ? "" : "地点:" + cls.getPlace()) + ",记得准时到场";
+            }
+            if (milestone == null) {
+                continue;
+            }
+            List<TrainUser> users = trainUserMapper.selectList(
+                    new LambdaQueryWrapper<TrainUser>().eq(TrainUser::getClassId, cls.getId()));
+            for (TrainUser u : users) {
+                if (ensureLog(u.getId(), milestone, detail)) {
+                    totalReminders++;
+                }
+            }
+        }
+        if (totalReminders > 0) {
+            log.info("PreClassReminderTask 本轮下发 {} 条课前提醒", totalReminders);
+        }
+    }
+
+    /** 幂等:同 action + targetId 同日已存在则跳过;返回 true 表示本次落账 */
+    private boolean ensureLog(Long uid, String action, String detail) {
+        Long exists = trainAuditLogMapper.selectCount(
+                new LambdaQueryWrapper<TrainAuditLog>()
+                        .eq(TrainAuditLog::getAction, action)
+                        .eq(TrainAuditLog::getTargetId, uid));
+        if (exists != null && exists > 0) {
+            return false;
+        }
+        TrainAuditLog entry = new TrainAuditLog();
+        entry.setAction(action);
+        entry.setActorUid(null);
+        entry.setTargetType("user");
+        entry.setTargetId(uid);
+        entry.setDetail(detail);
+        entry.setTs(new Date());
+        trainAuditLogMapper.insert(entry);
+        return true;
+    }
+}