|
|
@@ -0,0 +1,160 @@
|
|
|
+package com.etotem.cfc.task;
|
|
|
+
|
|
|
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
|
|
+import com.etotem.cfc.config.DatabaseInitializer;
|
|
|
+import com.etotem.cfc.entity.Task;
|
|
|
+import com.etotem.cfc.entity.User;
|
|
|
+import com.etotem.cfc.entity.UserNotification;
|
|
|
+import com.etotem.cfc.mapper.TaskMapper;
|
|
|
+import com.etotem.cfc.mapper.UserMapper;
|
|
|
+import com.etotem.cfc.mapper.UserNotificationMapper;
|
|
|
+import com.etotem.cfc.service.WechatWaitMsgService;
|
|
|
+import lombok.extern.slf4j.Slf4j;
|
|
|
+import org.springframework.scheduling.annotation.Scheduled;
|
|
|
+import org.springframework.stereotype.Component;
|
|
|
+
|
|
|
+import javax.annotation.Resource;
|
|
|
+import java.util.Calendar;
|
|
|
+import java.util.Date;
|
|
|
+import java.util.List;
|
|
|
+
|
|
|
+/**
|
|
|
+ * 每日任务打卡提醒(webhook):提醒时间(任务 deadline)过 1 小时仍未打卡 → 微信待办消息推送。
|
|
|
+ * <p>
|
|
|
+ * 扫描范围:今日到期的「家长执行型」待打卡任务(executor_type=parent,status=pending),
|
|
|
+ * deadline 已超过 now-60min(提醒时间 +1 小时宽限)且当天未推送过 → 调 WechatWaitMsgService 推送。
|
|
|
+ * 幂等:以 user_notifications (type=task_checkin_webhook, ref_type=task, ref_id=任务ID, 当日) 判重,
|
|
|
+ * 同一任务同一自然日最多推送一次;webhook 失败不写标记,下一分钟自动重试。
|
|
|
+ */
|
|
|
+@Slf4j
|
|
|
+@Component
|
|
|
+public class TaskCheckinWebhookReminderTask {
|
|
|
+
|
|
|
+ /** 宽限分钟数:提醒时间(deadline)过后多久未打卡才推送 */
|
|
|
+ private static final long GRACE_MINUTES = 60L;
|
|
|
+
|
|
|
+ /** 单次调度最多处理条数,避免选数异常时形成消息轰炸 */
|
|
|
+ private static final int MAX_PROCESS_PER_RUN = 20;
|
|
|
+
|
|
|
+ private static final String NOTIFY_TYPE = "task_checkin_webhook";
|
|
|
+ private static final String REF_TYPE = "task";
|
|
|
+
|
|
|
+ @Resource
|
|
|
+ private TaskMapper taskMapper;
|
|
|
+
|
|
|
+ @Resource
|
|
|
+ private UserMapper userMapper;
|
|
|
+
|
|
|
+ @Resource
|
|
|
+ private UserNotificationMapper userNotificationMapper;
|
|
|
+
|
|
|
+ @Resource
|
|
|
+ private WechatWaitMsgService wechatWaitMsgService;
|
|
|
+
|
|
|
+ /** 每分钟执行一次 */
|
|
|
+ @Scheduled(cron = "0 * * * * ?")
|
|
|
+ public void remindUncheckedTasks() {
|
|
|
+ if (!DatabaseInitializer.isMigrationsComplete()) {
|
|
|
+ log.info("数据库迁移尚未完成,跳过本轮打卡提醒");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ Date now = new Date();
|
|
|
+ Calendar cal = Calendar.getInstance();
|
|
|
+ cal.set(Calendar.HOUR_OF_DAY, 0);
|
|
|
+ cal.set(Calendar.MINUTE, 0);
|
|
|
+ cal.set(Calendar.SECOND, 0);
|
|
|
+ cal.set(Calendar.MILLISECOND, 0);
|
|
|
+ Date dayStart = cal.getTime();
|
|
|
+
|
|
|
+ Date threshold = new Date(now.getTime() - GRACE_MINUTES * 60_000L);
|
|
|
+
|
|
|
+ // 今日到期 + 已过提醒时间1小时 + 仍未打卡(pending)的家长执行型任务
|
|
|
+ List<Task> tasks = taskMapper.selectList(new LambdaQueryWrapper<Task>()
|
|
|
+ .eq(Task::getIsTemplate, 0)
|
|
|
+ .eq(Task::getExecutorType, "parent")
|
|
|
+ .eq(Task::getStatus, "pending")
|
|
|
+ .ge(Task::getDeadline, dayStart)
|
|
|
+ .lt(Task::getDeadline, threshold)
|
|
|
+ .orderByAsc(Task::getDeadline)
|
|
|
+ .last("LIMIT " + MAX_PROCESS_PER_RUN));
|
|
|
+ if (tasks == null || tasks.isEmpty()) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ int sent = 0;
|
|
|
+ for (Task task : tasks) {
|
|
|
+ try {
|
|
|
+ if (task.getExecutorId() == null) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ if (alreadySentToday(task.getExecutorId(), task.getId(), dayStart)) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ boolean ok = wechatWaitMsgService.sendWaitMsg(buildContent(task));
|
|
|
+ if (ok) {
|
|
|
+ markSent(task.getExecutorId(), task, now);
|
|
|
+ sent++;
|
|
|
+ }
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.warn("打卡提醒推送失败 taskId={}, error={}", task.getId(), e.getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
+ log.info("打卡提醒完成:候选{}条,推送成功{}条", tasks.size(), sent);
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.warn("打卡提醒任务异常: {}", e.getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 生成推送内容:问候用户昵称 + 任务标题(含说明摘要) */
|
|
|
+ private String buildContent(Task task) {
|
|
|
+ String name = "您";
|
|
|
+ try {
|
|
|
+ if (task.getExecutorId() != null) {
|
|
|
+ User u = userMapper.selectById(task.getExecutorId());
|
|
|
+ if (u != null && u.getNickname() != null && !u.getNickname().isEmpty()) {
|
|
|
+ name = u.getNickname();
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } catch (Exception e) {
|
|
|
+ // 昵称查询失败不阻塞推送
|
|
|
+ }
|
|
|
+ StringBuilder sb = new StringBuilder();
|
|
|
+ sb.append("「").append(name).append("」,您的健康任务已超过提醒时间 1 小时还没打卡:")
|
|
|
+ .append(task.getTitle());
|
|
|
+ if (task.getDescription() != null && !task.getDescription().isEmpty()) {
|
|
|
+ String desc = task.getDescription();
|
|
|
+ if (desc.length() > 60) {
|
|
|
+ desc = desc.substring(0, 60) + "…";
|
|
|
+ }
|
|
|
+ sb.append("(").append(desc).append(")");
|
|
|
+ }
|
|
|
+ sb.append("。记得及时完成打卡哦~");
|
|
|
+ return sb.toString();
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 同一任务同一自然日只推送一次 */
|
|
|
+ private boolean alreadySentToday(Long userId, Long taskId, Date dayStart) {
|
|
|
+ Long cnt = userNotificationMapper.selectCount(new LambdaQueryWrapper<UserNotification>()
|
|
|
+ .eq(UserNotification::getUserId, userId)
|
|
|
+ .eq(UserNotification::getType, NOTIFY_TYPE)
|
|
|
+ .eq(UserNotification::getRefType, REF_TYPE)
|
|
|
+ .eq(UserNotification::getRefId, taskId)
|
|
|
+ .ge(UserNotification::getCreatedAt, dayStart));
|
|
|
+ return cnt != null && cnt > 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 推送成功落标记(同时作为站内通知展示给用户) */
|
|
|
+ private void markSent(Long userId, Task task, Date now) {
|
|
|
+ UserNotification n = new UserNotification();
|
|
|
+ n.setUserId(userId);
|
|
|
+ n.setType(NOTIFY_TYPE);
|
|
|
+ n.setTitle("任务打卡提醒");
|
|
|
+ n.setContent("「" + task.getTitle() + "」已超过提醒时间 1 小时未打卡,记得完成哦~");
|
|
|
+ n.setRefType(REF_TYPE);
|
|
|
+ n.setRefId(task.getId());
|
|
|
+ n.setIsRead(0);
|
|
|
+ n.setCreatedAt(now);
|
|
|
+ userNotificationMapper.insert(n);
|
|
|
+ }
|
|
|
+}
|