From dceff515b7c8be9d9cfa4a10d9e8601c437c2a48 Mon Sep 17 00:00:00 2001 From: Your Name Date: Thu, 8 Jan 2026 14:46:41 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E6=AF=8F=E6=97=A5?= =?UTF-8?q?=E8=BF=9B=E5=BA=A6=E6=9B=B4=E6=96=B0=E6=A3=80=E6=9F=A5=E5=8A=9F?= =?UTF-8?q?=E8=83=BD=20-=20=E8=AE=BE=E8=AE=A1=E5=B8=88=E5=A4=A7=E5=8E=85?= =?UTF-8?q?=E6=8E=92=E8=A1=8C=E6=A6=9C=E9=BB=98=E8=AE=A4=E6=8A=98=E5=8F=A0?= =?UTF-8?q?=EF=BC=8C=E7=82=B9=E5=87=BB=E5=B1=95=E5=BC=80=20-=20=E7=B3=BB?= =?UTF-8?q?=E7=BB=9F=E8=AE=BE=E7=BD=AE=E6=B7=BB=E5=8A=A0=E6=AF=8F=E6=97=A5?= =?UTF-8?q?=E6=9C=AA=E6=9B=B4=E6=96=B0=E6=89=A3=E5=88=86=E9=85=8D=E7=BD=AE?= =?UTF-8?q?=E9=A1=B9=20-=20=E6=AF=8F=E5=A4=A918=E7=82=B9=E6=A3=80=E6=9F=A5?= =?UTF-8?q?=E8=AE=BE=E8=AE=A1=E5=B8=88=E6=98=AF=E5=90=A6=E6=9B=B4=E6=96=B0?= =?UTF-8?q?=E8=BF=9B=E5=BA=A6=EF=BC=8C=E6=9C=AA=E6=9B=B4=E6=96=B0=E5=88=99?= =?UTF-8?q?=E6=89=A3=E5=88=86=E5=B9=B6=E5=8F=91=E9=92=89=E9=92=89=E9=80=9A?= =?UTF-8?q?=E7=9F=A5=20-=20=E6=89=A3=E5=88=86=E5=90=8C=E6=AD=A5=E5=88=B0?= =?UTF-8?q?=E6=95=B0=E6=8D=AE=E5=88=86=E6=9E=90=E9=A1=B5=E9=9D=A2=E8=AE=BE?= =?UTF-8?q?=E8=AE=A1=E5=B8=88=E7=BB=A9=E6=95=88=E7=9A=84=E5=BB=B6=E8=AF=AF?= =?UTF-8?q?=E6=89=A3=E5=88=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../entity/ProgressDeductionLog.java | 62 +++++ .../mapper/ProgressDeductionLogMapper.java | 34 +++ .../designhub/service/AnalyticsService.java | 20 ++ .../service/ProgressCheckService.java | 215 ++++++++++++++++++ .../service/SystemConfigService.java | 6 + frontend/src/views/DesignerHall.vue | 44 ++-- 6 files changed, 363 insertions(+), 18 deletions(-) create mode 100644 backend/src/main/java/com/designhub/entity/ProgressDeductionLog.java create mode 100644 backend/src/main/java/com/designhub/mapper/ProgressDeductionLogMapper.java create mode 100644 backend/src/main/java/com/designhub/service/ProgressCheckService.java diff --git a/backend/src/main/java/com/designhub/entity/ProgressDeductionLog.java b/backend/src/main/java/com/designhub/entity/ProgressDeductionLog.java new file mode 100644 index 0000000..b8ad407 --- /dev/null +++ b/backend/src/main/java/com/designhub/entity/ProgressDeductionLog.java @@ -0,0 +1,62 @@ +package com.designhub.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; + +/** + * 进度未更新扣分记录实体 + */ +@Data +@TableName("progress_deduction_log") +public class ProgressDeductionLog { + + @TableId(type = IdType.AUTO) + private Long id; + + /** + * 设计师用户ID + */ + private Long userId; + + /** + * 任务ID + */ + private Long taskId; + + /** + * 子任务ID(如果是子任务) + */ + private Long taskItemId; + + /** + * 扣分日期 + */ + private LocalDate deductionDate; + + /** + * 扣除分数 + */ + private BigDecimal deductionScore; + + /** + * 扣分原因 + */ + private String reason; + + /** + * 是否已通知 + */ + private Integer notified; + + /** + * 创建时间 + */ + private LocalDateTime createTime; +} + diff --git a/backend/src/main/java/com/designhub/mapper/ProgressDeductionLogMapper.java b/backend/src/main/java/com/designhub/mapper/ProgressDeductionLogMapper.java new file mode 100644 index 0000000..502441b --- /dev/null +++ b/backend/src/main/java/com/designhub/mapper/ProgressDeductionLogMapper.java @@ -0,0 +1,34 @@ +package com.designhub.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.designhub.entity.ProgressDeductionLog; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; + +import java.math.BigDecimal; + +/** + * 进度未更新扣分记录Mapper + */ +@Mapper +public interface ProgressDeductionLogMapper extends BaseMapper { + + /** + * 查询用户指定月份的扣分总额 + */ + @Select("SELECT COALESCE(SUM(deduction_score), 0) FROM progress_deduction_log " + + "WHERE user_id = #{userId} " + + "AND YEAR(deduction_date) = #{year} " + + "AND MONTH(deduction_date) = #{month}") + BigDecimal sumMonthlyDeduction(@Param("userId") Long userId, + @Param("year") int year, + @Param("month") int month); + + /** + * 查询用户总扣分 + */ + @Select("SELECT COALESCE(SUM(deduction_score), 0) FROM progress_deduction_log WHERE user_id = #{userId}") + BigDecimal sumTotalDeduction(@Param("userId") Long userId); +} + diff --git a/backend/src/main/java/com/designhub/service/AnalyticsService.java b/backend/src/main/java/com/designhub/service/AnalyticsService.java index cde3159..be0b22d 100644 --- a/backend/src/main/java/com/designhub/service/AnalyticsService.java +++ b/backend/src/main/java/com/designhub/service/AnalyticsService.java @@ -38,6 +38,7 @@ public class AnalyticsService { private final RoleService roleService; private final DesignerTargetScoreMapper designerTargetScoreMapper; private final DemandTypeMapper demandTypeMapper; + private final ProgressDeductionLogMapper progressDeductionLogMapper; /** * 获取需求统计数据(根据角色返回不同维度的数据) @@ -749,6 +750,25 @@ public class AnalyticsService { } } + // 添加进度未更新扣分 + try { + if (progressDeductionLogMapper != null) { + // 解析月份 + String[] monthParts = currentMonth.split("-"); + if (monthParts.length == 2) { + int year = Integer.parseInt(monthParts[0]); + int month = Integer.parseInt(monthParts[1]); + BigDecimal progressDeduction = progressDeductionLogMapper.sumMonthlyDeduction(designer.getId(), year, month); + if (progressDeduction != null && progressDeduction.compareTo(BigDecimal.ZERO) > 0) { + deductionScore = deductionScore.add(progressDeduction); + log.debug("设计师 {} 本月进度未更新扣分: {}", designer.getRealName(), progressDeduction); + } + } + } + } catch (Exception e) { + log.warn("获取进度未更新扣分失败: {}", e.getMessage()); + } + // 净积分 = 总积分 - 扣分 BigDecimal netScore = totalScore.subtract(deductionScore); if (netScore.compareTo(BigDecimal.ZERO) < 0) { diff --git a/backend/src/main/java/com/designhub/service/ProgressCheckService.java b/backend/src/main/java/com/designhub/service/ProgressCheckService.java new file mode 100644 index 0000000..1b78125 --- /dev/null +++ b/backend/src/main/java/com/designhub/service/ProgressCheckService.java @@ -0,0 +1,215 @@ +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.getDingtalkUserId() == null) { + continue; + } + + StringBuilder content = new StringBuilder(); + content.append("⚠️ 进度更新提醒\n\n"); + content.append("您有 ").append(tasks.size()).append(" 个进行中的任务今日未更新进度:\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.sendPersonalMessage(user.getDingtalkUserId(), content.toString()); + log.info("已向用户 {} 发送进度提醒通知", user.getRealName()); + + } catch (Exception e) { + log.error("向用户 {} 发送钉钉通知失败", userId, e); + } + } + } + + /** + * 手动触发进度检查(用于测试) + */ + @Transactional(rollbackFor = Exception.class) + public void manualCheckProgress() { + log.info("手动触发进度检查"); + checkDailyProgressUpdate(); + } +} + diff --git a/backend/src/main/java/com/designhub/service/SystemConfigService.java b/backend/src/main/java/com/designhub/service/SystemConfigService.java index a48fea0..d1e1d1f 100644 --- a/backend/src/main/java/com/designhub/service/SystemConfigService.java +++ b/backend/src/main/java/com/designhub/service/SystemConfigService.java @@ -211,6 +211,12 @@ public class SystemConfigService { createConfigIfNotExists("score.delay_penalty_rate", "10", "score", "延误扣分比例(%)", "任务延误时的扣分比例", "number", 1); createConfigIfNotExists("score.delay_penalty_per_hour", "0.5", "score", "延误每小时扣分", "任务延误完结时每小时扣除的分数", "number", 1); createConfigIfNotExists("score.monthly_reset_enabled", "true", "score", "月度积分清零", "接单部门积分是否按月清零重新统计", "boolean", 1); + + // 每日进度更新检查配置 + createConfigIfNotExists("progress.daily_check_enabled", "true", "progress", "启用每日进度检查", "是否启用每天18点检查设计师进度更新", "boolean", 1); + createConfigIfNotExists("progress.daily_check_hour", "18", "progress", "每日检查时间(小时)", "每天检查进度的时间,24小时制", "number", 1); + createConfigIfNotExists("progress.no_update_deduction", "1", "progress", "未更新进度扣分", "设计师当日未更新进度时扣除的分数", "number", 1); + createConfigIfNotExists("progress.notify_on_deduction", "true", "progress", "扣分时发送钉钉通知", "未更新进度扣分时是否发送钉钉提醒", "boolean", 1); // 通知相关配置 createConfigIfNotExists("notification.dingtalk_enabled", "false", "notification", "启用钉钉通知", "是否启用钉钉消息推送", "boolean", 1); diff --git a/frontend/src/views/DesignerHall.vue b/frontend/src/views/DesignerHall.vue index f4a17c3..74ad5cd 100644 --- a/frontend/src/views/DesignerHall.vue +++ b/frontend/src/views/DesignerHall.vue @@ -44,39 +44,46 @@
-
+
- -
- - + +
+ - {{ month.label }} - - + + {{ month.label }} + + +
+ + + {{ leaderboardExpanded ? '收起' : '展开' }} + +
-
+
([]) const leaderboardData = ref([]) const activeLeaderboard = ref('score') const selectedMonth = ref('') // 选中的月份 +const leaderboardExpanded = ref(false) // 排行榜默认折叠 // 排行榜Tab配置 const leaderboardTabs = [