Forráskód Böngészése

feat(task): 微信打卡提醒按用户手机号定向msgLabel;重复任务支持earliest_start阶段起点

- WechatWaitMsgService 新增 sendWaitMsg(content,label) 重载,label为空回退全局配置
- WebhookNotificationChannel 以接收用户 users.phone 作为 msgLabel(按人定向)
- RepeatTaskGenerator 以模板 earliest_start 作为实例生成起点(分阶段任务门控)
Xiaogang Liao 1 hete
szülő
commit
00110cfb2a

+ 12 - 2
cfc-backend/src/main/java/com/etotem/cfc/service/WechatWaitMsgService.java

@@ -49,16 +49,26 @@ public class WechatWaitMsgService {
     private final ObjectMapper objectMapper = new ObjectMapper();
 
     /**
-     * 推送一条待办消息。成功返回 true;任何异常均捕获并记录 WARN,返回 false(由调用方决定是否重试)。
+     * 推送一条待办消息(使用全局 msgLabel)。成功返回 true;任何异常均捕获并记录 WARN,返回 false。
      */
     public boolean sendWaitMsg(String content) {
+        return sendWaitMsg(content, null);
+    }
+
+    /**
+     * 推送一条待办消息,可指定 msgLabel(如按接收人手机号定向推送)。
+     * <p>
+     * label 为空时回退全局 {@link #resolveMsgLabel()}(sys_config → yml 默认值)。
+     * 成功返回 true;任何异常均捕获并记录 WARN,返回 false(由调用方决定是否重试)。
+     */
+    public boolean sendWaitMsg(String content, String label) {
         try {
             HttpHeaders headers = new HttpHeaders();
             headers.setContentType(MediaType.APPLICATION_JSON);
             headers.set("Authorization", waitMsgToken);
 
             Map<String, String> body = new HashMap<>();
-            body.put("msgLabel", resolveMsgLabel());
+            body.put("msgLabel", (label != null && !label.trim().isEmpty()) ? label.trim() : resolveMsgLabel());
             body.put("content", content);
 
             String json = objectMapper.writeValueAsString(body);

+ 28 - 2
cfc-backend/src/main/java/com/etotem/cfc/service/notification/WebhookNotificationChannel.java

@@ -1,6 +1,8 @@
 package com.etotem.cfc.service.notification;
 
 import com.etotem.cfc.dto.NotificationMessage;
+import com.etotem.cfc.entity.User;
+import com.etotem.cfc.mapper.UserMapper;
 import com.etotem.cfc.service.WechatWaitMsgService;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -13,6 +15,9 @@ import javax.annotation.Resource;
  * <p>
  * 复用 WechatWaitMsgService 向微信待办接口推送;URL/token 见
  * etotem.webhook.wait-msg.*(缺省内联默认值),msgLabel 经 sys_config 可配。
+ * <p>
+ * 定向推送:若接收用户配置了手机号,则以手机号作为 msgLabel(按人定向),
+ * 否则回退全局 msgLabel(sys_config → yml 默认值)。
  */
 @Component
 public class WebhookNotificationChannel implements NotificationChannel {
@@ -22,6 +27,9 @@ public class WebhookNotificationChannel implements NotificationChannel {
     @Resource
     private WechatWaitMsgService wechatWaitMsgService;
 
+    @Resource
+    private UserMapper userMapper;
+
     @Override
     public String getChannelType() {
         return "wechat";
@@ -34,9 +42,27 @@ public class WebhookNotificationChannel implements NotificationChannel {
 
     @Override
     public void send(NotificationMessage message) {
-        boolean ok = wechatWaitMsgService.sendWaitMsg(message.getContent());
+        // 优先用接收人手机号作为 msgLabel(按用户定向推送);无手机号则用全局默认
+        String label = resolveUserPhone(message.getUserId());
+        boolean ok = wechatWaitMsgService.sendWaitMsg(message.getContent(), label);
         if (!ok) {
-            log.warn("微信待办推送失败: userId={}, title={}", message.getUserId(), message.getTitle());
+            log.warn("微信待办推送失败: userId={}, title={}, label={}", message.getUserId(), message.getTitle(), label);
+        }
+    }
+
+    /** 查询接收用户手机号作为定向 msgLabel;失败或无手机号返回 null(走全局默认) */
+    private String resolveUserPhone(Long userId) {
+        if (userId == null) {
+            return null;
+        }
+        try {
+            User u = userMapper.selectById(userId);
+            if (u != null && u.getPhone() != null && !u.getPhone().trim().isEmpty()) {
+                return u.getPhone().trim();
+            }
+        } catch (Exception e) {
+            log.warn("查询用户手机号失败 userId={}, error={}", userId, e.getMessage());
         }
+        return null;
     }
 }

+ 16 - 0
cfc-backend/src/main/java/com/etotem/cfc/task/RepeatTaskGenerator.java

@@ -177,6 +177,12 @@ public class RepeatTaskGenerator {
 * @param useNow 是否使用当前时间作为截止时间(hourly 任务使用)
 */
 private void createTaskFromTemplate(Task template, Date targetDate, boolean useNow) {
+// 周期起点:模板 earliest_start 为方案周期开始日,未到起点不生成实例(用于分阶段任务,
+// 如"第二阶段/第三阶段"任务在各自起始日之前不应出现)。
+if (!useNow && template.getEarliestStart() != null && targetDate != null
+        && targetDate.before(startOfDay(template.getEarliestStart()))) {
+    return;
+}
 // D3 周期边界:模板 deadline 的日期部分即方案周期终点,超过后不再生成实例。
 // 实例 deadline = targetDate + 模板时分,故以 targetDate 与模板 deadline 比较。
 if (!useNow && template.getDeadline() != null && targetDate != null
@@ -248,4 +254,14 @@ if (newTask.getPrerequisiteTaskId() != null) {
 taskMapper.insert(newTask);
 log.info("已生成重复任务: {} (截止: {})", newTask.getTitle(), newTask.getDeadline());
 }
+
+private Date startOfDay(Date date) {
+    Calendar cal = Calendar.getInstance();
+    cal.setTime(date);
+    cal.set(Calendar.HOUR_OF_DAY, 0);
+    cal.set(Calendar.MINUTE, 0);
+    cal.set(Calendar.SECOND, 0);
+    cal.set(Calendar.MILLISECOND, 0);
+    return cal.getTime();
+}
 }