package com.designhub.service; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.designhub.entity.*; import com.designhub.mapper.*; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.math.BigDecimal; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.LocalTime; import java.util.*; /** * 进度检查服务 - 每日检查设计师是否更新了进度 */ @Slf4j @Service @RequiredArgsConstructor public class ProgressCheckService { private final TaskMapper taskMapper; private final TaskItemMapper taskItemMapper; private final UserMapper userMapper; private final ProgressDeductionLogMapper progressDeductionLogMapper; private final SystemConfigService systemConfigService; private final DingTalkService dingTalkService; private final DepartmentMapper departmentMapper; /** * 每天18点执行进度检查 * cron: 秒 分 时 日 月 周 */ @Scheduled(cron = "0 0 18 * * ?") public void checkDailyProgressUpdate() { log.info("========== 开始每日进度更新检查 =========="); try { // 检查是否启用 String enabledStr = systemConfigService.getConfigValue("progress.daily_check_enabled"); if (!"true".equalsIgnoreCase(enabledStr)) { log.info("每日进度检查功能已禁用,跳过检查"); return; } // 获取扣分配置 String deductionStr = systemConfigService.getConfigValue("progress.no_update_deduction"); BigDecimal deductionScore = new BigDecimal(deductionStr != null ? deductionStr : "1"); String notifyStr = systemConfigService.getConfigValue("progress.notify_on_deduction"); boolean notifyEnabled = "true".equalsIgnoreCase(notifyStr); LocalDate today = LocalDate.now(); LocalDateTime startOfDay = today.atStartOfDay(); LocalDateTime endOfDay = today.atTime(LocalTime.MAX); // 1. 检查主任务 List inProgressTasks = taskMapper.selectList( new LambdaQueryWrapper() .eq(Task::getStatus, "in_progress") .eq(Task::getDeleted, 0) .isNotNull(Task::getReceiverId) ); Map> userMissedTasks = new HashMap<>(); for (Task task : inProgressTasks) { // 检查今天是否更新过进度 boolean updatedToday = task.getUpdateTime() != null && task.getUpdateTime().isAfter(startOfDay) && task.getUpdateTime().isBefore(endOfDay); if (!updatedToday) { Long userId = task.getReceiverId(); // 检查今天是否已经对该任务扣过分 Long existingCount = progressDeductionLogMapper.selectCount( new LambdaQueryWrapper() .eq(ProgressDeductionLog::getUserId, userId) .eq(ProgressDeductionLog::getTaskId, task.getId()) .isNull(ProgressDeductionLog::getTaskItemId) .eq(ProgressDeductionLog::getDeductionDate, today) ); if (existingCount == 0) { // 记录扣分 ProgressDeductionLog deductionLog = new ProgressDeductionLog(); deductionLog.setUserId(userId); deductionLog.setTaskId(task.getId()); deductionLog.setDeductionDate(today); deductionLog.setDeductionScore(deductionScore); deductionLog.setReason("当日未更新任务进度"); deductionLog.setNotified(notifyEnabled ? 1 : 0); deductionLog.setCreateTime(LocalDateTime.now()); progressDeductionLogMapper.insert(deductionLog); // 记录待通知 userMissedTasks.computeIfAbsent(userId, k -> new ArrayList<>()) .add(task.getTitle()); log.info("用户 {} 任务【{}】今日未更新进度,扣除 {} 分", userId, task.getTitle(), deductionScore); } } } // 2. 检查子任务 List inProgressItems = taskItemMapper.selectList( new LambdaQueryWrapper() .eq(TaskItem::getStatus, "in_progress") .isNotNull(TaskItem::getReceiverId) ); for (TaskItem item : inProgressItems) { boolean updatedToday = item.getUpdateTime() != null && item.getUpdateTime().isAfter(startOfDay) && item.getUpdateTime().isBefore(endOfDay); if (!updatedToday) { Long userId = item.getReceiverId(); // 检查今天是否已经对该子任务扣过分 Long existingCount = progressDeductionLogMapper.selectCount( new LambdaQueryWrapper() .eq(ProgressDeductionLog::getUserId, userId) .eq(ProgressDeductionLog::getTaskItemId, item.getId()) .eq(ProgressDeductionLog::getDeductionDate, today) ); if (existingCount == 0) { // 记录扣分 ProgressDeductionLog deductionLog = new ProgressDeductionLog(); deductionLog.setUserId(userId); deductionLog.setTaskId(item.getTaskId()); deductionLog.setTaskItemId(item.getId()); deductionLog.setDeductionDate(today); deductionLog.setDeductionScore(deductionScore); deductionLog.setReason("当日未更新子任务进度"); deductionLog.setNotified(notifyEnabled ? 1 : 0); deductionLog.setCreateTime(LocalDateTime.now()); progressDeductionLogMapper.insert(deductionLog); // 记录待通知 userMissedTasks.computeIfAbsent(userId, k -> new ArrayList<>()) .add("子任务#" + item.getGroupNo()); log.info("用户 {} 子任务【#{}】今日未更新进度,扣除 {} 分", userId, item.getGroupNo(), deductionScore); } } } // 3. 发送钉钉通知 if (notifyEnabled && !userMissedTasks.isEmpty()) { sendDingTalkNotifications(userMissedTasks, deductionScore); } log.info("========== 每日进度检查完成,共处理 {} 位设计师 ==========", userMissedTasks.size()); } catch (Exception e) { log.error("每日进度检查执行失败", e); } } /** * 发送钉钉通知 */ private void sendDingTalkNotifications(Map> userMissedTasks, BigDecimal deductionScore) { for (Map.Entry> entry : userMissedTasks.entrySet()) { Long userId = entry.getKey(); List tasks = entry.getValue(); try { User user = userMapper.selectById(userId); if (user == null || user.getPhone() == null) { // 如果没有手机号,无法发送钉钉通知 log.warn("用户 {} 没有绑定手机号,跳过钉钉通知", userId); continue; } StringBuilder content = new StringBuilder(); content.append("⚠️ **进度更新提醒**\n\n"); content.append("您有 **").append(tasks.size()).append("** 个进行中的任务今日未更新进度:\n\n"); for (int i = 0; i < Math.min(tasks.size(), 5); i++) { content.append("• ").append(tasks.get(i)).append("\n"); } if (tasks.size() > 5) { content.append("• ...等共 ").append(tasks.size()).append(" 个任务\n"); } content.append("\n**已扣除积分:").append(deductionScore.multiply(BigDecimal.valueOf(tasks.size()))).append(" 分**"); content.append("\n\n💡 温馨提示:请每日下班前更新任务进度,以便跟进项目状态。"); // 使用手机号作为钉钉用户标识发送通知 dingTalkService.sendWorkNotification( java.util.Collections.singletonList(user.getPhone()), "进度更新提醒", content.toString() ); log.info("已向用户 {} 发送进度提醒通知", user.getRealName()); } catch (Exception e) { log.error("向用户 {} 发送钉钉通知失败", userId, e); } } } /** * 手动触发进度检查(用于测试) */ @Transactional(rollbackFor = Exception.class) public void manualCheckProgress() { log.info("手动触发进度检查"); checkDailyProgressUpdate(); } }