diff --git a/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/controller/admin/assigntasksinfo/AssignTasksInfoController.java b/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/controller/admin/assigntasksinfo/AssignTasksInfoController.java index 8f8c315..c04d0ad 100644 --- a/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/controller/admin/assigntasksinfo/AssignTasksInfoController.java +++ b/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 import com.zteits.urbanops.module.garden.dal.dataobject.assigntasksinfo.AssignTasksInfoDO; import com.zteits.urbanops.module.garden.service.assigntasks.AssignTasksService; import com.zteits.urbanops.module.garden.service.assigntasksinfo.AssignTasksInfoService; +import com.zteits.urbanops.module.garden.service.assigntasksinfo.QuartzRemindService; import com.zteits.urbanops.module.garden.service.unitintegraltotal.UnitIntegralService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; @@ -28,7 +29,10 @@ import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import java.io.IOException; +import java.time.LocalDateTime; +import java.time.ZoneId; import java.util.ArrayList; +import java.util.Date; import java.util.List; import java.util.stream.Collectors; @@ -166,4 +170,26 @@ public class AssignTasksInfoController { public CommonResult feedback(@RequestBody AssignTasksInfoUpdReqVO assignTaskCompany) { return success(assignTasksInfoService.feedbackAssignTaskCompany(assignTaskCompany)); } + + + @Resource + private QuartzRemindService quartzRemindService; + + @Operation(summary = "到期提醒") + @GetMapping(value = "/createDeadlineRemind") + public void deadlineRemindTest(@RequestParam("id") Long id) { + // 2. 注册Quartz任务:到期前3小时触发 + try { + LocalDateTime now = LocalDateTime.now(); + LocalDateTime triggerTime = now.plusHours(3).plusMinutes(3); // 现在往后推3小时3分钟 + Date triggerDate = Date.from( + triggerTime.atZone(ZoneId.systemDefault()) // 绑定时区 + .toInstant() // 转为Instant(含时区的时间戳) + ); + quartzRemindService.registerRemindJob(id, triggerDate); + } catch (Exception e) { + throw new RuntimeException("任务创建失败"); + } + } + } diff --git a/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/job/task/ExpireRemindJob.java b/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/job/task/ExpireRemindJob.java new file mode 100644 index 0000000..7364477 --- /dev/null +++ b/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/job/task/ExpireRemindJob.java @@ -0,0 +1,176 @@ +package com.zteits.urbanops.module.garden.job.task; + +import com.alibaba.fastjson.JSONObject; +import com.baomidou.mybatisplus.core.toolkit.StringUtils; +import com.zteits.urbanops.module.garden.dal.dataobject.assigntasksinfo.AssignTasksInfoDO; +import com.zteits.urbanops.module.garden.enums.AssignTasksConstants; +import com.zteits.urbanops.module.garden.service.assigntasksinfo.AssignTasksInfoService; +import com.zteits.urbanops.module.garden.service.assigntasksinfo.QuartzRemindService; +import com.zteits.urbanops.module.system.api.sms.SmsCodeApi; +import com.zteits.urbanops.module.system.api.sms.dto.code.SmsCodeSendReqDTO; +import com.zteits.urbanops.module.system.api.user.dto.AdminUserRespDTO; +import jakarta.annotation.Resource; +import org.quartz.Job; +import org.quartz.JobExecutionContext; +import org.quartz.JobExecutionException; +import org.quartz.SchedulerException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; + +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +import static com.zteits.urbanops.framework.common.util.servlet.ServletUtils.getClientIP; +import static com.zteits.urbanops.module.garden.service.assigntasksinfo.AssignTasksInfoServiceImpl.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +/** + * @Classname ExpireRemindJob + * @Description 任务到期提醒Job(Quartz一次性触发) + * @Date 2026/3/9 19:27 + * @Created by wangqian + */ +@Service +public class ExpireRemindJob implements Job { + private static final Logger log = LoggerFactory.getLogger(ExpireRemindJob.class); + private static final DateTimeFormatter DEADLINE_FORMATTER = DateTimeFormatter + .ofPattern(FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + .withZone(ZoneId.of("GMT+8")); // 时区与之前配置一致,避免偏移 + @Resource + private AssignTasksInfoService assignTasksInfoService; + @Resource + private QuartzRemindService quartzRemindService; + + @Resource + private SmsCodeApi smsCodeApi; + + @Override + public void execute(JobExecutionContext context) throws JobExecutionException { + // 1. 从JobDataMap获取任务ID,增加空值校验和日志上下文 + Long taskId = (Long) context.getMergedJobDataMap().get("taskId"); + if (taskId == null) { + log.error("【到期提醒Job】执行失败:任务ID为空"); + return; + } + log.info("【到期提醒Job】开始执行,任务ID:{}", taskId); + + // 2. 统一封装任务移除逻辑,避免重复代码 + try { + // 2.1 查询任务信息,增加空指针和业务状态校验 + AssignTasksInfoDO task = assignTasksInfoService.getAssignTasksInfo(taskId); + if (task == null) { + log.warn("【到期提醒Job】任务不存在,任务ID:{},执行移除触发器", taskId); + removeRemindJobSafely(taskId); + return; + } + + // 2.2 任务已完成(finish=1),无需提醒,直接移除触发器 + if (1 == task.getFinish()) { + log.info("【到期提醒Job】任务已完成,无需提醒,任务ID:{},执行移除触发器", taskId); + removeRemindJobSafely(taskId); + return; + } + // 2.3 新增判断:到期时间距离当前时间小于3小时,无需提醒 + LocalDateTime deadline = task.getDeadline(); + LocalDateTime now = LocalDateTime.now(); + // 计算当前时间到到期时间的时长(单位:小时) + long hoursUntilDeadline = java.time.Duration.between(now, deadline).toHours(); + // 核心判断:到期前不足3小时(包括已过期),不提醒 + if (hoursUntilDeadline < 3) { + log.info("【到期提醒Job】任务到期前不足3小时,无需提醒,任务ID:{},到期时间:{},当前时间:{}", + taskId, deadline, now); + removeRemindJobSafely(taskId); + return; + } + + // 3. 核心提醒逻辑(示例:短信/站内信/邮件) + //executeRemindLogic(task); + + // 4. 提醒成功后,更新任务状态+移除触发器(核心优化:成功也移除) + updateTaskRemindStatus(taskId); + removeRemindJobSafely(taskId); + log.info("【到期提醒Job】执行成功,任务ID:{}", taskId); + + } catch (Exception e) { + // 5. 全局异常捕获,保证Job不中断,记录详细日志+尝试移除触发器 + log.error("【到期提醒Job】执行异常,任务ID:{}", taskId, e); + removeRemindJobSafely(taskId); + // 可选:抛出异常让Quartz标记Job执行失败(根据业务需求决定) + // throw new JobExecutionException("到期提醒任务执行失败", e, false); + } + } + + /** + * 执行具体的提醒逻辑(解耦核心业务,便于维护) + * @param task 任务信息 + */ + private void executeRemindLogic(AssignTasksInfoDO task) { + String taskName = task.getTaskName(); + LocalDateTime deadline = task.getDeadline(); + String formattedDeadline = Objects.nonNull(deadline) + ? DEADLINE_FORMATTER.format(deadline) + : ""; // 或 null + String remindContent = String.format("任务【%s】即将到期,到期时间:%s,请及时处理", taskName, deadline); + + List matchUserList = assignTasksInfoService.getUsersByCompanyId(task.getCompanyId()); + Map templateParams = new HashMap(); + templateParams.put("companyName", task.getCompanyName()); + templateParams.put("taskName", task.getTaskName()); + templateParams.put("deadline", formattedDeadline); + // 5. 发送短信通知 + log.info("【到期提醒逻辑】发送短信 signMap: {}", JSONObject.toJSONString(templateParams)); + Boolean sendFlag = false; + for (AdminUserRespDTO adminUserRespDTO : matchUserList) { + String phonenumber = adminUserRespDTO.getMobile(); + if (StringUtils.isEmpty(phonenumber)) { + continue; + } + SmsCodeSendReqDTO reqDTO = new SmsCodeSendReqDTO(); + reqDTO.setScene(AssignTasksConstants.ASSIGN_TASK_SCENE); + reqDTO.setMobile(phonenumber);//手机号 + reqDTO.setTemplateParams(templateParams);//短信参数 + reqDTO.setCreateIp(getClientIP()); + //smsCodeApi.openSendSmsCode(reqDTO); + log.info("提交单位任务成功,已通知填报人:手机号:{},任务ID={}", phonenumber, task.getId()); + sendFlag = true; + } + log.info("【到期提醒逻辑】执行提醒,任务名称:{},到期时间:{},提醒内容:{}", taskName, deadline, remindContent); + } + + /** + * 更新任务提醒状态(解耦状态更新逻辑,便于扩展) + * @param taskId 任务ID + */ + private void updateTaskRemindStatus(Long taskId) { + try { + // 实际业务:更新数据库中任务的提醒状态(如remind_status=1 表示已提醒) + //assignTasksInfoService.updateRemindStatus(taskId, 1); + log.debug("【到期提醒Job】更新任务提醒状态成功,任务ID:{}", taskId); + } catch (Exception e) { + // 状态更新失败单独捕获,不影响触发器移除 + log.error("【到期提醒Job】更新任务提醒状态失败,任务ID:{}", taskId, e); + } + } + + /** + * 安全移除提醒任务(封装移除逻辑,避免重复try-catch,保证移除操作不抛异常) + * @param taskId 任务ID + */ + private void removeRemindJobSafely(Long taskId) { + if (taskId == null) { + return; + } + try { + quartzRemindService.removeRemindJob(taskId); + log.debug("【到期提醒Job】移除触发器成功,任务ID:{}", taskId); + } catch (SchedulerException e) { + // 移除失败仅记录日志,不中断主流程 + log.error("【到期提醒Job】移除触发器失败,任务ID:{}", taskId, e); + } + } +} diff --git a/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/service/assigntasksinfo/AssignTasksInfoService.java b/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/service/assigntasksinfo/AssignTasksInfoService.java index 616f11b..68827ff 100644 --- a/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/service/assigntasksinfo/AssignTasksInfoService.java +++ b/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 import com.zteits.urbanops.module.garden.controller.admin.assigntasksinfo.vo.AssignTasksInfoUpdReqVO; import com.zteits.urbanops.module.garden.dal.dataobject.assigntasks.AssignTasksDO; import com.zteits.urbanops.module.garden.dal.dataobject.assigntasksinfo.AssignTasksInfoDO; +import com.zteits.urbanops.module.system.api.user.dto.AdminUserRespDTO; import jakarta.validation.Valid; import java.util.List; @@ -102,4 +103,12 @@ public interface AssignTasksInfoService { * @Return int */ public int feedbackAssignTaskCompany(@Valid AssignTasksInfoUpdReqVO tdAssignTaskCompany); + + /** + * @Author wangqian + * @Description 根据单位获取单位管理员 + * @Date 2026/3/9 20:45 + * @Param companyId + */ + List getUsersByCompanyId(Long companyId); } diff --git a/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/service/assigntasksinfo/AssignTasksInfoServiceImpl.java b/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/service/assigntasksinfo/AssignTasksInfoServiceImpl.java index 63e928b..c849c35 100644 --- a/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/service/assigntasksinfo/AssignTasksInfoServiceImpl.java +++ b/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; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.apache.commons.collections4.CollectionUtils; +import org.quartz.SchedulerException; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.validation.annotation.Validated; @@ -33,10 +34,7 @@ import java.time.LocalDateTime; import java.time.ZoneId; import java.time.format.DateTimeFormatter; import java.time.temporal.ChronoUnit; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; +import java.util.*; import java.util.stream.Collectors; import static com.zteits.urbanops.framework.common.exception.enums.GlobalErrorCodeConstants.BAD_REQUEST; @@ -76,6 +74,9 @@ public class AssignTasksInfoServiceImpl implements AssignTasksInfoService { @Resource private UnitIntegralService unitIntegralService; + @Resource + private QuartzRemindService quartzRemindService; + // 时间格式常量(与之前保持一致) public static final String FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND = "yyyy-MM-dd HH:mm:ss"; @@ -169,6 +170,16 @@ public class AssignTasksInfoServiceImpl implements AssignTasksInfoService { assignTasksInfoMapper.insert(assignTaskInfo); // add by 20260307 初始化单位积分 unitIntegralService.createUnitIntegral(CommonConstants.UNIT_INTEGRAL_UNIT_TYPE_01, String.valueOf(companyId), assignTaskInfo.getCompanyName()); + try { + //保存成功后创建定时器,3小时执行 + Date triggerDate = Date.from( + assignTasks.getDeadline().atZone(ZoneId.systemDefault()) // 绑定时区 + .toInstant() // 转为Instant(含时区的时间戳) + ); + quartzRemindService.registerRemindJob(assignTaskInfo.getId(), triggerDate); + } catch (SchedulerException exception) { + throw exception0(BAD_REQUEST.getCode(),"创建到期定时提醒异常"); + } } } } @@ -330,6 +341,36 @@ public class AssignTasksInfoServiceImpl implements AssignTasksInfoService { throw exception(ASSIGN_TASKS_INFO_NOT_EXISTS); } } + + @Override + public List getUsersByCompanyId(Long companyId) { + //用户角色 + List userIds = roleApi.getUserIdsByRoleCode(AssignTasksConstants.ASSIGN_ROLE_KEY); + if (CollectionUtils.isEmpty(userIds)) { + //未配置 + throw exception0(BAD_REQUEST.getCode(),"未配置子公司管理员角色"); + } + //填报人信息 + List users = adminUserApi.getUserList(userIds); + if(CollectionUtils.isEmpty(users)) { + //没有权限 + throw exception0(BAD_REQUEST.getCode(),"未配置分公司管理员"); + } + + // 收集所有匹配companyId的用户,无匹配则返回空集合(而非null) + List matchUserList = users.stream() + .filter(user -> { + // 防御性判空:过滤掉null用户或deptId为空的用户 + if (user == null || user.getDeptId() == null) { + return false; + } + // 安全比较companyId和deptId(兼容null) + return Objects.equals(companyId, user.getDeptId()); + }) + .collect(Collectors.toList()); + return matchUserList; + } + /** * @Author wangqian * @Description 获取单位当前积分 diff --git a/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/service/assigntasksinfo/QuartzRemindService.java b/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/service/assigntasksinfo/QuartzRemindService.java new file mode 100644 index 0000000..6a3f236 --- /dev/null +++ b/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/service/assigntasksinfo/QuartzRemindService.java @@ -0,0 +1,90 @@ +package com.zteits.urbanops.module.garden.service.assigntasksinfo; + +import com.zteits.urbanops.module.garden.job.task.ExpireRemindJob; +import lombok.extern.slf4j.Slf4j; +import org.quartz.*; +import org.quartz.impl.matchers.GroupMatcher; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Set; + +/** + * 动态注册到期提醒Quartz任务的服务 + */ +@Service +@Slf4j +public class QuartzRemindService { + // 任务组名(自定义,便于管理) + private static final String REMIND_JOB_GROUP = "EXPIRE_REMIND_GROUP"; + + @Autowired + private Scheduler scheduler; + + /** + * 动态注册“到期前3小时提醒”任务 + * + * @param jobId 待提醒的任务信息 + * @throws SchedulerException 调度器异常 + */ + public void registerRemindJob(Long jobId, Date expireTime) throws SchedulerException { + // 1. 校验参数 + if (jobId == null || expireTime == null) { + throw new IllegalArgumentException("任务信息不完整,无法注册提醒"); + } + + // 2. 计算触发时间:到期时间 - 3小时 + Date triggerTime = new Date(expireTime.getTime() - 3 * 60 * 60 * 1000L); + + // 3. 定义JobKey(唯一标识:任务ID + 组名) + JobKey jobKey = new JobKey("REMIND_JOB_" + jobId, REMIND_JOB_GROUP); + + // 4. 检查任务是否已存在,存在则先删除 + if (scheduler.checkExists(jobKey)) { + scheduler.deleteJob(jobKey); + } + + // 5. 构建JobDetail(绑定自定义的ExpireRemindJob) + JobDetail jobDetail = JobBuilder.newJob(ExpireRemindJob.class).withIdentity(jobKey) + // 传递参数(任务ID)到Job中 + .usingJobData("taskId", jobId).storeDurably(false).build(); + + // 6. 构建Trigger(一次性触发,触发时间为计算的“到期前3小时”) + Trigger trigger = TriggerBuilder.newTrigger() + .withIdentity("REMIND_TRIGGER_" + jobId, REMIND_JOB_GROUP) + .startAt(triggerTime) + // 只触发一次(到期提醒只需一次) + .withSchedule(SimpleScheduleBuilder.simpleSchedule().withMisfireHandlingInstructionFireNow()).build(); + + // 7. 注册任务到调度器 + scheduler.scheduleJob(jobDetail, trigger); + SimpleDateFormat sf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + log.info("【动态注册到期提醒任务成功】任务ID:{},触发时间:{}", jobId, sf.format(triggerTime)); + } + + /** + * 移除指定的提醒任务 + * + * @param taskId 任务ID + * @throws SchedulerException 调度器异常 + */ + public void removeRemindJob(Long taskId) throws SchedulerException { + JobKey jobKey = new JobKey("REMIND_JOB_" + taskId, REMIND_JOB_GROUP); + if (scheduler.checkExists(jobKey)) { + scheduler.deleteJob(jobKey); + log.info("【移除到期提醒任务成功】任务ID:{}", taskId); + } + } + + /** + * 查看所有已注册的提醒任务 + * + * @return 任务Key集合 + * @throws SchedulerException 调度器异常 + */ + public Set listAllRemindJobs() throws SchedulerException { + return scheduler.getJobKeys(GroupMatcher.jobGroupEquals(REMIND_JOB_GROUP)); + } +}