diff --git a/src/main/java/com/project/exam/domain/service/NotifyExamRecordDomainService.java b/src/main/java/com/project/exam/domain/service/NotifyExamRecordDomainService.java new file mode 100644 index 0000000..fc80b72 --- /dev/null +++ b/src/main/java/com/project/exam/domain/service/NotifyExamRecordDomainService.java @@ -0,0 +1,9 @@ +package com.project.exam.domain.service; + +import com.project.exam.domain.dto.ExamRecordDTO; + +public interface NotifyExamRecordDomainService { + + void notifyExamRecord(ExamRecordDTO dto); + +} diff --git a/src/main/java/com/project/exam/domain/service/impl/NotifyExamRecordDomainServiceImpl.java b/src/main/java/com/project/exam/domain/service/impl/NotifyExamRecordDomainServiceImpl.java new file mode 100644 index 0000000..8daa7cf --- /dev/null +++ b/src/main/java/com/project/exam/domain/service/impl/NotifyExamRecordDomainServiceImpl.java @@ -0,0 +1,61 @@ +package com.project.exam.domain.service.impl; + +import com.github.tingyugetc520.ali.dingtalk.api.DtService; +import com.github.tingyugetc520.ali.dingtalk.bean.message.DtCorpConversationMessage; +import com.github.tingyugetc520.ali.dingtalk.bean.message.DtCorpConversationMsgSendResult; +import com.github.tingyugetc520.ali.dingtalk.bean.message.DtMessage; +import com.project.ding.utils.SecurityUtils; +import com.project.exam.domain.dto.ExamRecordDTO; +import com.project.exam.domain.service.NotifyExamRecordDomainService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.Collections; + +@Service +@Slf4j +public class NotifyExamRecordDomainServiceImpl implements NotifyExamRecordDomainService { + + @Autowired + private DtService dtService; + + @Override + public void notifyExamRecord(ExamRecordDTO dto) { + String userId = SecurityUtils.getUserId(); + + // 不阻碍正常业务 + try { + String markdown = String.format("### 【%s】考试成绩已公布,请留意查收\n\n", dto.getTaskDTO().getName()) + + String.format("考试成绩:%.2f\n\n", dto.getScore()) + + String.format("考试结果:%s\n\n", dto.getPass() ? "考试通过" : "考试不通过") + + String.format("%s\n\n", dto.getPass() ? "恭喜考试通过,请继续保持!可以自行查看考试结果" : + "遗憾考试不通过,请再接再厉!可以查看考试结果或尝试重新补考"); + DtCorpConversationMessage message = DtCorpConversationMessage.builder() + .agentId(dtService.getDtConfigStorage().getAgentId()) + .userIds(Collections.singletonList(userId)) + .msg(DtMessage.ACTIONCARD() + .title("产品知识考核") + .markdown(markdown) + .singleTitle("查看详情") + // todo 修改为对应页面 + .singleUrl("https://www.baidu.com/").build()) + .build(); + DtCorpConversationMsgSendResult result = dtService.getCorpConversationMsgService().send(message); + if (result.getErrCode() == 0) { + log.info(">>> [成绩公布通知] 成功发送通知 ,考试任务名:{} ,用户:{}, ExamRecord:{}", dto.getTaskDTO().getName() , userId , dto.getId() + ); + } else { + log.error(">>> [成绩公布通知] 发送失败.考试任务名:{} ,用户:{}, ExamRecord:{}, 错误码: {}, 错误信息: {}", dto.getTaskDTO().getName() , userId , dto.getId() , + result.getErrCode(), result.getErrMsg()); + } + + } catch (Exception e) { + log.error(">>> [成绩公布通知] 钉钉接口调用异常, 考试任务名:{} ,用户:{}, ExamRecord:{}, 原因: {}", dto.getTaskDTO().getName() , userId , + dto.getId() , e.getMessage()); + } + + + + } +} diff --git a/src/main/java/com/project/exam/domain/service/impl/SubmitPaperDomainServiceImpl.java b/src/main/java/com/project/exam/domain/service/impl/SubmitPaperDomainServiceImpl.java index 50e9770..b5b2ab9 100644 --- a/src/main/java/com/project/exam/domain/service/impl/SubmitPaperDomainServiceImpl.java +++ b/src/main/java/com/project/exam/domain/service/impl/SubmitPaperDomainServiceImpl.java @@ -5,6 +5,7 @@ import com.project.base.domain.exception.BusinessErrorException; import com.project.exam.domain.dto.ExamRecordDTO; import com.project.exam.domain.entity.ExamRecordEntity; import com.project.exam.domain.service.BuildExamRecordDomainService; +import com.project.exam.domain.service.NotifyExamRecordDomainService; import com.project.exam.domain.service.SubmitPaperDomainService; import com.project.exam.mapper.ExamRecordMapper; import com.project.task.domain.dto.TaskDTO; @@ -40,6 +41,9 @@ public class SubmitPaperDomainServiceImpl implements SubmitPaperDomainService { @Autowired private BuildExamRecordDomainService buildExamRecordDomainService; + @Autowired + private NotifyExamRecordDomainService notifyExamRecordDomainService; + @Override @Transactional(rollbackFor = Exception.class) public ExamRecordDTO submitPaper(Long recordId) throws Exception { @@ -83,9 +87,13 @@ public class SubmitPaperDomainServiceImpl implements SubmitPaperDomainService { // 更新 TaskUser 冗余字段 updateTaskUserStatus(taskUser , isPassed , recordId); + // 构建dto ExamRecordDTO dto = record.toDTO(ExamRecordDTO::new); + // 后面需要用到这个taskDTO dto.setTaskDTO(task.toDTO(TaskDTO::new)); + // 发送通知 + notifyExamRecordDomainService.notifyExamRecord(dto); return buildExamRecordDomainService.buildDTO(dto); } diff --git a/src/main/java/com/project/task/domain/job/TaskNotifyJob.java b/src/main/java/com/project/task/domain/job/TaskNotifyJob.java new file mode 100644 index 0000000..d433971 --- /dev/null +++ b/src/main/java/com/project/task/domain/job/TaskNotifyJob.java @@ -0,0 +1,46 @@ +package com.project.task.domain.job; + + +import com.project.base.config.ScheduledTaskProperties; +import com.project.task.domain.service.NotifyTaskDomainService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; + +import java.util.Date; + + +@Service +@Slf4j +public class TaskNotifyJob { + @Autowired + private ScheduledTaskProperties taskProps; + + @Autowired + private NotifyTaskDomainService notifyTaskDomainService; + + /** + * 每天上午 9:00 执行通知 + */ + @Scheduled(cron = "0 0 9 * * ?") + public void run() { + if ("dev".equalsIgnoreCase(taskProps.getOwner())) { + return; + } + log.info(">>> [每日通知任务] 启动执行..."); + Date today = new Date(); + try { + // 处理新开始的任务 + notifyTaskDomainService.notifyNewTasks(today); + + // 处理7天截止的任务 + notifyTaskDomainService.notifyDeadlineTasks(today); + + } catch (Exception e) { + log.error(">>> [每日通知任务] 执行异常", e); + } + log.info(">>> [每日通知任务] 执行完毕。"); + } + +} diff --git a/src/main/java/com/project/task/domain/service/NotifyTaskDomainService.java b/src/main/java/com/project/task/domain/service/NotifyTaskDomainService.java new file mode 100644 index 0000000..4676a93 --- /dev/null +++ b/src/main/java/com/project/task/domain/service/NotifyTaskDomainService.java @@ -0,0 +1,8 @@ +package com.project.task.domain.service; + +import java.util.Date; + +public interface NotifyTaskDomainService { + void notifyNewTasks(Date today); + void notifyDeadlineTasks(Date today); +} diff --git a/src/main/java/com/project/task/domain/service/impl/NotifyTaskDomainServiceImpl.java b/src/main/java/com/project/task/domain/service/impl/NotifyTaskDomainServiceImpl.java new file mode 100644 index 0000000..95a24ef --- /dev/null +++ b/src/main/java/com/project/task/domain/service/impl/NotifyTaskDomainServiceImpl.java @@ -0,0 +1,159 @@ +package com.project.task.domain.service.impl; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.collection.ListUtil; +import cn.hutool.core.date.DateUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.github.tingyugetc520.ali.dingtalk.api.DtService; +import com.github.tingyugetc520.ali.dingtalk.bean.message.DtCorpConversationMessage; +import com.github.tingyugetc520.ali.dingtalk.bean.message.DtCorpConversationMsgSendResult; +import com.github.tingyugetc520.ali.dingtalk.bean.message.DtMessage; +import com.github.tingyugetc520.ali.dingtalk.error.DtErrorException; +import com.project.task.domain.dto.TaskDTO; +import com.project.task.domain.entity.TaskEntity; +import com.project.task.domain.entity.TaskUserEntity; +import com.project.task.domain.enums.TaskUserStatusEnum; +import com.project.task.domain.service.NotifyTaskDomainService; +import com.project.task.mapper.TaskMapper; +import com.project.task.mapper.TaskUserMapper; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.text.SimpleDateFormat; +import java.util.Arrays; +import java.util.Date; +import java.util.List; + +@Service +@Slf4j +public class NotifyTaskDomainServiceImpl implements NotifyTaskDomainService { + @Autowired + private TaskMapper taskMapper; + + @Autowired + private TaskUserMapper taskUserMapper; + @Autowired + private DtService dtService; + + private final SimpleDateFormat sdf = new SimpleDateFormat("yyyy年MM月dd日"); + + + @Override + public void notifyNewTasks(Date today) { + // 1. 查找今天 00:00:00 到 23:59:59 之间开始的任务 + Date startOfDay = DateUtil.beginOfDay(today); + Date endOfDay = DateUtil.endOfDay(today); + + List startTasks = taskMapper.selectList(new LambdaQueryWrapper() + .between(TaskEntity::getStartTime, startOfDay, endOfDay)) + .stream().map(entity -> entity.toDTO(TaskDTO::new)).toList(); + + for (TaskDTO task : startTasks) { + // 获取该任务下所有考生 + List userIds = taskUserMapper.selectList(new LambdaQueryWrapper() + .select(TaskUserEntity::getUserId) + .eq(TaskUserEntity::getTaskId, task.getId())) + .stream().map(TaskUserEntity::getUserId).toList(); + + sendBatchNewTasks(userIds , task); + } + } + @Override + public void notifyDeadlineTasks(Date today) { + Date startOfDay = DateUtil.beginOfDay(DateUtil.offsetDay(today , -6)); + Date endOfDay = DateUtil.endOfDay(DateUtil.offsetDay(today , -6)); + List startTasks = taskMapper.selectList(new LambdaQueryWrapper() + .between(TaskEntity::getStartTime, startOfDay, endOfDay)) + .stream().map(entity -> entity.toDTO(TaskDTO::new)).toList(); + for (TaskDTO task : startTasks) { + // 获取该任务下所有未通过考生 + List userIds = taskUserMapper.selectList(new LambdaQueryWrapper() + .select(TaskUserEntity::getUserId) + .in(TaskUserEntity::getStatus , Arrays.asList(TaskUserStatusEnum.Not_Start.getValue() , TaskUserStatusEnum.Fail.getValue())) + .eq(TaskUserEntity::getTaskId, task.getId())) + .stream().map(TaskUserEntity::getUserId).toList(); + + sendBatchDeadlineTasks(userIds , task); + } + } + private void sendBatchNewTasks(List userIds , TaskDTO taskDTO) { + if (CollUtil.isEmpty(userIds)) { + return; + } + StringBuilder markdown = new StringBuilder(); + markdown.append(String.format("### 【%s】已经开始,请按时完成\n\n" , taskDTO.getName())) + .append(String.format("产品线:%s\n\n" , taskDTO.getLineName())) + .append(String.format("开考时间:%s\n\n" ,sdf.format(taskDTO.getStartTime()))) + .append(String.format("截止时间:%s\n\n" , sdf.format(taskDTO.getEndTime()))) + .append(String.format("注意事项:%s" , taskDTO.getNote())); + + ListUtil.partition(userIds, 100).forEach(batch -> { + DtCorpConversationMessage message = DtCorpConversationMessage.builder() + .agentId(dtService.getDtConfigStorage().getAgentId()) + .userIds(batch) + .msg(DtMessage.ACTIONCARD() + .title("产品知识考核") + .markdown(markdown.toString()) + .singleTitle("查看详情") + // todo 修改为对应页面 + .singleUrl("https://www.baidu.com/").build()) + .build(); + try { + DtCorpConversationMsgSendResult result = dtService.getCorpConversationMsgService().send(message); + + if (result.getErrCode() == 0) { + log.info(">>> [考试任务开始通知] 成功发送新任务通知. 任务名: {}, 批次人数: {}, 任务ID: {}", + taskDTO.getName(), batch.size(), result.getTaskId()); + } else { + log.warn(">>> [考试任务开始通知] 发送失败. 错误码: {}, 错误信息: {}", result.getErrCode(), result.getErrMsg()); + } + + } catch (DtErrorException e) { + log.error(">>> [考试任务开始通知] 钉钉接口调用异常, 任务名: {}, 原因: {}", taskDTO.getName(), e.getMessage()); + } + }); + } + + + + + + private void sendBatchDeadlineTasks(List userIds , TaskDTO taskDTO) { + if (CollUtil.isEmpty(userIds)) { + return; + } + StringBuilder markdown = new StringBuilder(); + markdown.append(String.format("### 【%s】即将结束,请按时完成\n\n" , taskDTO.getName())) + .append(String.format("产品线:%s\n\n" , taskDTO.getLineName())) + .append(String.format("开考时间:%s\n\n" ,sdf.format(taskDTO.getStartTime()))) + .append(String.format("截止时间:%s\n\n" , sdf.format(taskDTO.getEndTime()))) + .append(String.format("注意事项:%s" , taskDTO.getNote())); + + ListUtil.partition(userIds, 100).forEach(batch -> { + DtCorpConversationMessage message = DtCorpConversationMessage.builder() + .agentId(dtService.getDtConfigStorage().getAgentId()) + .userIds(batch) + .msg(DtMessage.ACTIONCARD() + .title("产品知识考核") + .markdown(markdown.toString()) + .singleTitle("查看详情") + // todo 修改为对应页面 + .singleUrl("https://www.baidu.com/").build()) + .build(); + try { + DtCorpConversationMsgSendResult result = dtService.getCorpConversationMsgService().send(message); + + if (result.getErrCode() == 0) { + log.info(">>> [考试任务即将结束通知] 成功发送新任务通知. 任务名: {}, 批次人数: {}, 任务ID: {}", + taskDTO.getName(), batch.size(), result.getTaskId()); + } else { + log.warn(">>> [考试任务即将结束通知] 发送失败. 错误码: {}, 错误信息: {}", result.getErrCode(), result.getErrMsg()); + } + + } catch (DtErrorException e) { + log.error(">>> [考试任务即将结束通知] 钉钉接口调用异常, 任务名: {}, 原因: {}", taskDTO.getName(), e.getMessage()); + } + }); + } +} diff --git a/src/main/resources/logback-spring.xml b/src/main/resources/logback-spring.xml new file mode 100644 index 0000000..b25a44d --- /dev/null +++ b/src/main/resources/logback-spring.xml @@ -0,0 +1,54 @@ + + + + + + + + + + logs/replenish-inventory.log + + + + logs/archive/replenish-%d{yyyy-MM-dd}.%i.log.gz + + + 10MB + + + 3 + + + 100MB + + + + ${FILE_LOG_PATTERN} + UTF-8 + + + + + + + + + + logs/exam-notice.log + + logs/archive/exam-notice-%d{yyyy-MM-dd}.%i.log.gz + 5MB + 3 + 50MB + + ${FILE_LOG_PATTERN} + + + + + + + + + \ No newline at end of file