Commit 9fa6ecb74222e26a177ea51e2b0ffd43e15c9cc1

Authored by wangqian
1 parent f0a8bead

派单增加到期前3小时短信提醒任务

urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/controller/admin/assigntasksinfo/AssignTasksInfoController.java
... ... @@ -16,6 +16,7 @@ import com.zteits.urbanops.module.garden.dal.dataobject.assigntasks.AssignTasksD
16 16 import com.zteits.urbanops.module.garden.dal.dataobject.assigntasksinfo.AssignTasksInfoDO;
17 17 import com.zteits.urbanops.module.garden.service.assigntasks.AssignTasksService;
18 18 import com.zteits.urbanops.module.garden.service.assigntasksinfo.AssignTasksInfoService;
  19 +import com.zteits.urbanops.module.garden.service.assigntasksinfo.QuartzRemindService;
19 20 import com.zteits.urbanops.module.garden.service.unitintegraltotal.UnitIntegralService;
20 21 import io.swagger.v3.oas.annotations.Operation;
21 22 import io.swagger.v3.oas.annotations.Parameter;
... ... @@ -28,7 +29,10 @@ import org.springframework.validation.annotation.Validated;
28 29 import org.springframework.web.bind.annotation.*;
29 30  
30 31 import java.io.IOException;
  32 +import java.time.LocalDateTime;
  33 +import java.time.ZoneId;
31 34 import java.util.ArrayList;
  35 +import java.util.Date;
32 36 import java.util.List;
33 37 import java.util.stream.Collectors;
34 38  
... ... @@ -166,4 +170,26 @@ public class AssignTasksInfoController {
166 170 public CommonResult feedback(@RequestBody AssignTasksInfoUpdReqVO assignTaskCompany) {
167 171 return success(assignTasksInfoService.feedbackAssignTaskCompany(assignTaskCompany));
168 172 }
  173 +
  174 +
  175 + @Resource
  176 + private QuartzRemindService quartzRemindService;
  177 +
  178 + @Operation(summary = "到期提醒")
  179 + @GetMapping(value = "/createDeadlineRemind")
  180 + public void deadlineRemindTest(@RequestParam("id") Long id) {
  181 + // 2. 注册Quartz任务:到期前3小时触发
  182 + try {
  183 + LocalDateTime now = LocalDateTime.now();
  184 + LocalDateTime triggerTime = now.plusHours(3).plusMinutes(3); // 现在往后推3小时3分钟
  185 + Date triggerDate = Date.from(
  186 + triggerTime.atZone(ZoneId.systemDefault()) // 绑定时区
  187 + .toInstant() // 转为Instant(含时区的时间戳)
  188 + );
  189 + quartzRemindService.registerRemindJob(id, triggerDate);
  190 + } catch (Exception e) {
  191 + throw new RuntimeException("任务创建失败");
  192 + }
  193 + }
  194 +
169 195 }
... ...
urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/job/task/ExpireRemindJob.java 0 → 100644
  1 +package com.zteits.urbanops.module.garden.job.task;
  2 +
  3 +import com.alibaba.fastjson.JSONObject;
  4 +import com.baomidou.mybatisplus.core.toolkit.StringUtils;
  5 +import com.zteits.urbanops.module.garden.dal.dataobject.assigntasksinfo.AssignTasksInfoDO;
  6 +import com.zteits.urbanops.module.garden.enums.AssignTasksConstants;
  7 +import com.zteits.urbanops.module.garden.service.assigntasksinfo.AssignTasksInfoService;
  8 +import com.zteits.urbanops.module.garden.service.assigntasksinfo.QuartzRemindService;
  9 +import com.zteits.urbanops.module.system.api.sms.SmsCodeApi;
  10 +import com.zteits.urbanops.module.system.api.sms.dto.code.SmsCodeSendReqDTO;
  11 +import com.zteits.urbanops.module.system.api.user.dto.AdminUserRespDTO;
  12 +import jakarta.annotation.Resource;
  13 +import org.quartz.Job;
  14 +import org.quartz.JobExecutionContext;
  15 +import org.quartz.JobExecutionException;
  16 +import org.quartz.SchedulerException;
  17 +import org.slf4j.Logger;
  18 +import org.slf4j.LoggerFactory;
  19 +import org.springframework.stereotype.Service;
  20 +
  21 +import java.time.LocalDateTime;
  22 +import java.time.ZoneId;
  23 +import java.time.format.DateTimeFormatter;
  24 +import java.util.HashMap;
  25 +import java.util.List;
  26 +import java.util.Map;
  27 +import java.util.Objects;
  28 +
  29 +import static com.zteits.urbanops.framework.common.util.servlet.ServletUtils.getClientIP;
  30 +import static com.zteits.urbanops.module.garden.service.assigntasksinfo.AssignTasksInfoServiceImpl.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
  31 +
  32 +/**
  33 + * @Classname ExpireRemindJob
  34 + * @Description 任务到期提醒Job(Quartz一次性触发)
  35 + * @Date 2026/3/9 19:27
  36 + * @Created by wangqian
  37 + */
  38 +@Service
  39 +public class ExpireRemindJob implements Job {
  40 + private static final Logger log = LoggerFactory.getLogger(ExpireRemindJob.class);
  41 + private static final DateTimeFormatter DEADLINE_FORMATTER = DateTimeFormatter
  42 + .ofPattern(FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
  43 + .withZone(ZoneId.of("GMT+8")); // 时区与之前配置一致,避免偏移
  44 + @Resource
  45 + private AssignTasksInfoService assignTasksInfoService;
  46 + @Resource
  47 + private QuartzRemindService quartzRemindService;
  48 +
  49 + @Resource
  50 + private SmsCodeApi smsCodeApi;
  51 +
  52 + @Override
  53 + public void execute(JobExecutionContext context) throws JobExecutionException {
  54 + // 1. 从JobDataMap获取任务ID,增加空值校验和日志上下文
  55 + Long taskId = (Long) context.getMergedJobDataMap().get("taskId");
  56 + if (taskId == null) {
  57 + log.error("【到期提醒Job】执行失败:任务ID为空");
  58 + return;
  59 + }
  60 + log.info("【到期提醒Job】开始执行,任务ID:{}", taskId);
  61 +
  62 + // 2. 统一封装任务移除逻辑,避免重复代码
  63 + try {
  64 + // 2.1 查询任务信息,增加空指针和业务状态校验
  65 + AssignTasksInfoDO task = assignTasksInfoService.getAssignTasksInfo(taskId);
  66 + if (task == null) {
  67 + log.warn("【到期提醒Job】任务不存在,任务ID:{},执行移除触发器", taskId);
  68 + removeRemindJobSafely(taskId);
  69 + return;
  70 + }
  71 +
  72 + // 2.2 任务已完成(finish=1),无需提醒,直接移除触发器
  73 + if (1 == task.getFinish()) {
  74 + log.info("【到期提醒Job】任务已完成,无需提醒,任务ID:{},执行移除触发器", taskId);
  75 + removeRemindJobSafely(taskId);
  76 + return;
  77 + }
  78 + // 2.3 新增判断:到期时间距离当前时间小于3小时,无需提醒
  79 + LocalDateTime deadline = task.getDeadline();
  80 + LocalDateTime now = LocalDateTime.now();
  81 + // 计算当前时间到到期时间的时长(单位:小时)
  82 + long hoursUntilDeadline = java.time.Duration.between(now, deadline).toHours();
  83 + // 核心判断:到期前不足3小时(包括已过期),不提醒
  84 + if (hoursUntilDeadline < 3) {
  85 + log.info("【到期提醒Job】任务到期前不足3小时,无需提醒,任务ID:{},到期时间:{},当前时间:{}",
  86 + taskId, deadline, now);
  87 + removeRemindJobSafely(taskId);
  88 + return;
  89 + }
  90 +
  91 + // 3. 核心提醒逻辑(示例:短信/站内信/邮件)
  92 + //executeRemindLogic(task);
  93 +
  94 + // 4. 提醒成功后,更新任务状态+移除触发器(核心优化:成功也移除)
  95 + updateTaskRemindStatus(taskId);
  96 + removeRemindJobSafely(taskId);
  97 + log.info("【到期提醒Job】执行成功,任务ID:{}", taskId);
  98 +
  99 + } catch (Exception e) {
  100 + // 5. 全局异常捕获,保证Job不中断,记录详细日志+尝试移除触发器
  101 + log.error("【到期提醒Job】执行异常,任务ID:{}", taskId, e);
  102 + removeRemindJobSafely(taskId);
  103 + // 可选:抛出异常让Quartz标记Job执行失败(根据业务需求决定)
  104 + // throw new JobExecutionException("到期提醒任务执行失败", e, false);
  105 + }
  106 + }
  107 +
  108 + /**
  109 + * 执行具体的提醒逻辑(解耦核心业务,便于维护)
  110 + * @param task 任务信息
  111 + */
  112 + private void executeRemindLogic(AssignTasksInfoDO task) {
  113 + String taskName = task.getTaskName();
  114 + LocalDateTime deadline = task.getDeadline();
  115 + String formattedDeadline = Objects.nonNull(deadline)
  116 + ? DEADLINE_FORMATTER.format(deadline)
  117 + : ""; // 或 null
  118 + String remindContent = String.format("任务【%s】即将到期,到期时间:%s,请及时处理", taskName, deadline);
  119 +
  120 + List<AdminUserRespDTO> matchUserList = assignTasksInfoService.getUsersByCompanyId(task.getCompanyId());
  121 + Map<String, Object> templateParams = new HashMap<String, Object>();
  122 + templateParams.put("companyName", task.getCompanyName());
  123 + templateParams.put("taskName", task.getTaskName());
  124 + templateParams.put("deadline", formattedDeadline);
  125 + // 5. 发送短信通知
  126 + log.info("【到期提醒逻辑】发送短信 signMap: {}", JSONObject.toJSONString(templateParams));
  127 + Boolean sendFlag = false;
  128 + for (AdminUserRespDTO adminUserRespDTO : matchUserList) {
  129 + String phonenumber = adminUserRespDTO.getMobile();
  130 + if (StringUtils.isEmpty(phonenumber)) {
  131 + continue;
  132 + }
  133 + SmsCodeSendReqDTO reqDTO = new SmsCodeSendReqDTO();
  134 + reqDTO.setScene(AssignTasksConstants.ASSIGN_TASK_SCENE);
  135 + reqDTO.setMobile(phonenumber);//手机号
  136 + reqDTO.setTemplateParams(templateParams);//短信参数
  137 + reqDTO.setCreateIp(getClientIP());
  138 + //smsCodeApi.openSendSmsCode(reqDTO);
  139 + log.info("提交单位任务成功,已通知填报人:手机号:{},任务ID={}", phonenumber, task.getId());
  140 + sendFlag = true;
  141 + }
  142 + log.info("【到期提醒逻辑】执行提醒,任务名称:{},到期时间:{},提醒内容:{}", taskName, deadline, remindContent);
  143 + }
  144 +
  145 + /**
  146 + * 更新任务提醒状态(解耦状态更新逻辑,便于扩展)
  147 + * @param taskId 任务ID
  148 + */
  149 + private void updateTaskRemindStatus(Long taskId) {
  150 + try {
  151 + // 实际业务:更新数据库中任务的提醒状态(如remind_status=1 表示已提醒)
  152 + //assignTasksInfoService.updateRemindStatus(taskId, 1);
  153 + log.debug("【到期提醒Job】更新任务提醒状态成功,任务ID:{}", taskId);
  154 + } catch (Exception e) {
  155 + // 状态更新失败单独捕获,不影响触发器移除
  156 + log.error("【到期提醒Job】更新任务提醒状态失败,任务ID:{}", taskId, e);
  157 + }
  158 + }
  159 +
  160 + /**
  161 + * 安全移除提醒任务(封装移除逻辑,避免重复try-catch,保证移除操作不抛异常)
  162 + * @param taskId 任务ID
  163 + */
  164 + private void removeRemindJobSafely(Long taskId) {
  165 + if (taskId == null) {
  166 + return;
  167 + }
  168 + try {
  169 + quartzRemindService.removeRemindJob(taskId);
  170 + log.debug("【到期提醒Job】移除触发器成功,任务ID:{}", taskId);
  171 + } catch (SchedulerException e) {
  172 + // 移除失败仅记录日志,不中断主流程
  173 + log.error("【到期提醒Job】移除触发器失败,任务ID:{}", taskId, e);
  174 + }
  175 + }
  176 +}
... ...
urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/service/assigntasksinfo/AssignTasksInfoService.java
... ... @@ -6,6 +6,7 @@ import com.zteits.urbanops.module.garden.controller.admin.assigntasksinfo.vo.Ass
6 6 import com.zteits.urbanops.module.garden.controller.admin.assigntasksinfo.vo.AssignTasksInfoUpdReqVO;
7 7 import com.zteits.urbanops.module.garden.dal.dataobject.assigntasks.AssignTasksDO;
8 8 import com.zteits.urbanops.module.garden.dal.dataobject.assigntasksinfo.AssignTasksInfoDO;
  9 +import com.zteits.urbanops.module.system.api.user.dto.AdminUserRespDTO;
9 10 import jakarta.validation.Valid;
10 11  
11 12 import java.util.List;
... ... @@ -102,4 +103,12 @@ public interface AssignTasksInfoService {
102 103 * @Return int
103 104 */
104 105 public int feedbackAssignTaskCompany(@Valid AssignTasksInfoUpdReqVO tdAssignTaskCompany);
  106 +
  107 + /**
  108 + * @Author wangqian
  109 + * @Description 根据单位获取单位管理员
  110 + * @Date 2026/3/9 20:45
  111 + * @Param companyId
  112 + */
  113 + List<AdminUserRespDTO> getUsersByCompanyId(Long companyId);
105 114 }
... ...
urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/service/assigntasksinfo/AssignTasksInfoServiceImpl.java
... ... @@ -25,6 +25,7 @@ import com.zteits.urbanops.module.system.api.user.dto.AdminUserRespDTO;
25 25 import jakarta.annotation.Resource;
26 26 import lombok.extern.slf4j.Slf4j;
27 27 import org.apache.commons.collections4.CollectionUtils;
  28 +import org.quartz.SchedulerException;
28 29 import org.springframework.stereotype.Service;
29 30 import org.springframework.transaction.annotation.Transactional;
30 31 import org.springframework.validation.annotation.Validated;
... ... @@ -33,10 +34,7 @@ import java.time.LocalDateTime;
33 34 import java.time.ZoneId;
34 35 import java.time.format.DateTimeFormatter;
35 36 import java.time.temporal.ChronoUnit;
36   -import java.util.HashMap;
37   -import java.util.List;
38   -import java.util.Map;
39   -import java.util.Objects;
  37 +import java.util.*;
40 38 import java.util.stream.Collectors;
41 39  
42 40 import static com.zteits.urbanops.framework.common.exception.enums.GlobalErrorCodeConstants.BAD_REQUEST;
... ... @@ -76,6 +74,9 @@ public class AssignTasksInfoServiceImpl implements AssignTasksInfoService {
76 74 @Resource
77 75 private UnitIntegralService unitIntegralService;
78 76  
  77 + @Resource
  78 + private QuartzRemindService quartzRemindService;
  79 +
79 80 // 时间格式常量(与之前保持一致)
80 81 public static final String FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND = "yyyy-MM-dd HH:mm:ss";
81 82  
... ... @@ -169,6 +170,16 @@ public class AssignTasksInfoServiceImpl implements AssignTasksInfoService {
169 170 assignTasksInfoMapper.insert(assignTaskInfo);
170 171 // add by 20260307 初始化单位积分
171 172 unitIntegralService.createUnitIntegral(CommonConstants.UNIT_INTEGRAL_UNIT_TYPE_01, String.valueOf(companyId), assignTaskInfo.getCompanyName());
  173 + try {
  174 + //保存成功后创建定时器,3小时执行
  175 + Date triggerDate = Date.from(
  176 + assignTasks.getDeadline().atZone(ZoneId.systemDefault()) // 绑定时区
  177 + .toInstant() // 转为Instant(含时区的时间戳)
  178 + );
  179 + quartzRemindService.registerRemindJob(assignTaskInfo.getId(), triggerDate);
  180 + } catch (SchedulerException exception) {
  181 + throw exception0(BAD_REQUEST.getCode(),"创建到期定时提醒异常");
  182 + }
172 183 }
173 184 }
174 185 }
... ... @@ -330,6 +341,36 @@ public class AssignTasksInfoServiceImpl implements AssignTasksInfoService {
330 341 throw exception(ASSIGN_TASKS_INFO_NOT_EXISTS);
331 342 }
332 343 }
  344 +
  345 + @Override
  346 + public List<AdminUserRespDTO> getUsersByCompanyId(Long companyId) {
  347 + //用户角色
  348 + List<Long> userIds = roleApi.getUserIdsByRoleCode(AssignTasksConstants.ASSIGN_ROLE_KEY);
  349 + if (CollectionUtils.isEmpty(userIds)) {
  350 + //未配置
  351 + throw exception0(BAD_REQUEST.getCode(),"未配置子公司管理员角色");
  352 + }
  353 + //填报人信息
  354 + List<AdminUserRespDTO> users = adminUserApi.getUserList(userIds);
  355 + if(CollectionUtils.isEmpty(users)) {
  356 + //没有权限
  357 + throw exception0(BAD_REQUEST.getCode(),"未配置分公司管理员");
  358 + }
  359 +
  360 + // 收集所有匹配companyId的用户,无匹配则返回空集合(而非null)
  361 + List<AdminUserRespDTO> matchUserList = users.stream()
  362 + .filter(user -> {
  363 + // 防御性判空:过滤掉null用户或deptId为空的用户
  364 + if (user == null || user.getDeptId() == null) {
  365 + return false;
  366 + }
  367 + // 安全比较companyId和deptId(兼容null)
  368 + return Objects.equals(companyId, user.getDeptId());
  369 + })
  370 + .collect(Collectors.toList());
  371 + return matchUserList;
  372 + }
  373 +
333 374 /**
334 375 * @Author wangqian
335 376 * @Description 获取单位当前积分
... ...
urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/service/assigntasksinfo/QuartzRemindService.java 0 → 100644
  1 +package com.zteits.urbanops.module.garden.service.assigntasksinfo;
  2 +
  3 +import com.zteits.urbanops.module.garden.job.task.ExpireRemindJob;
  4 +import lombok.extern.slf4j.Slf4j;
  5 +import org.quartz.*;
  6 +import org.quartz.impl.matchers.GroupMatcher;
  7 +import org.springframework.beans.factory.annotation.Autowired;
  8 +import org.springframework.stereotype.Service;
  9 +
  10 +import java.text.SimpleDateFormat;
  11 +import java.util.Date;
  12 +import java.util.Set;
  13 +
  14 +/**
  15 + * 动态注册到期提醒Quartz任务的服务
  16 + */
  17 +@Service
  18 +@Slf4j
  19 +public class QuartzRemindService {
  20 + // 任务组名(自定义,便于管理)
  21 + private static final String REMIND_JOB_GROUP = "EXPIRE_REMIND_GROUP";
  22 +
  23 + @Autowired
  24 + private Scheduler scheduler;
  25 +
  26 + /**
  27 + * 动态注册“到期前3小时提醒”任务
  28 + *
  29 + * @param jobId 待提醒的任务信息
  30 + * @throws SchedulerException 调度器异常
  31 + */
  32 + public void registerRemindJob(Long jobId, Date expireTime) throws SchedulerException {
  33 + // 1. 校验参数
  34 + if (jobId == null || expireTime == null) {
  35 + throw new IllegalArgumentException("任务信息不完整,无法注册提醒");
  36 + }
  37 +
  38 + // 2. 计算触发时间:到期时间 - 3小时
  39 + Date triggerTime = new Date(expireTime.getTime() - 3 * 60 * 60 * 1000L);
  40 +
  41 + // 3. 定义JobKey(唯一标识:任务ID + 组名)
  42 + JobKey jobKey = new JobKey("REMIND_JOB_" + jobId, REMIND_JOB_GROUP);
  43 +
  44 + // 4. 检查任务是否已存在,存在则先删除
  45 + if (scheduler.checkExists(jobKey)) {
  46 + scheduler.deleteJob(jobKey);
  47 + }
  48 +
  49 + // 5. 构建JobDetail(绑定自定义的ExpireRemindJob)
  50 + JobDetail jobDetail = JobBuilder.newJob(ExpireRemindJob.class).withIdentity(jobKey)
  51 + // 传递参数(任务ID)到Job中
  52 + .usingJobData("taskId", jobId).storeDurably(false).build();
  53 +
  54 + // 6. 构建Trigger(一次性触发,触发时间为计算的“到期前3小时”)
  55 + Trigger trigger = TriggerBuilder.newTrigger()
  56 + .withIdentity("REMIND_TRIGGER_" + jobId, REMIND_JOB_GROUP)
  57 + .startAt(triggerTime)
  58 + // 只触发一次(到期提醒只需一次)
  59 + .withSchedule(SimpleScheduleBuilder.simpleSchedule().withMisfireHandlingInstructionFireNow()).build();
  60 +
  61 + // 7. 注册任务到调度器
  62 + scheduler.scheduleJob(jobDetail, trigger);
  63 + SimpleDateFormat sf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
  64 + log.info("【动态注册到期提醒任务成功】任务ID:{},触发时间:{}", jobId, sf.format(triggerTime));
  65 + }
  66 +
  67 + /**
  68 + * 移除指定的提醒任务
  69 + *
  70 + * @param taskId 任务ID
  71 + * @throws SchedulerException 调度器异常
  72 + */
  73 + public void removeRemindJob(Long taskId) throws SchedulerException {
  74 + JobKey jobKey = new JobKey("REMIND_JOB_" + taskId, REMIND_JOB_GROUP);
  75 + if (scheduler.checkExists(jobKey)) {
  76 + scheduler.deleteJob(jobKey);
  77 + log.info("【移除到期提醒任务成功】任务ID:{}", taskId);
  78 + }
  79 + }
  80 +
  81 + /**
  82 + * 查看所有已注册的提醒任务
  83 + *
  84 + * @return 任务Key集合
  85 + * @throws SchedulerException 调度器异常
  86 + */
  87 + public Set<JobKey> listAllRemindJobs() throws SchedulerException {
  88 + return scheduler.getJobKeys(GroupMatcher.jobGroupEquals(REMIND_JOB_GROUP));
  89 + }
  90 +}
... ...