From e4ec8a9fc40595767a3b16e427dcb54e13913e36 Mon Sep 17 00:00:00 2001 From: Your Name Date: Fri, 16 Jan 2026 10:23:13 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E5=85=81=E8=AE=B8=E5=9C=A8=E5=AE=A1?= =?UTF-8?q?=E6=A0=B8=E6=9C=AA=E9=80=9A=E8=BF=87=E4=B9=8B=E5=89=8D=E7=BC=96?= =?UTF-8?q?=E8=BE=91=E4=BB=BB=E5=8A=A1=EF=BC=88=E5=BE=85=E5=AE=A1=E6=A0=B8?= =?UTF-8?q?=E3=80=81=E8=A2=AB=E9=A9=B3=E5=9B=9E=E7=8A=B6=E6=80=81=E9=83=BD?= =?UTF-8?q?=E5=8F=AF=E4=BB=A5=E7=BC=96=E8=BE=91=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/designhub/service/TaskService.java | 56 ++- 扣分逻辑说明.md | 427 ++++++++++++++---- 2 files changed, 365 insertions(+), 118 deletions(-) diff --git a/backend/src/main/java/com/designhub/service/TaskService.java b/backend/src/main/java/com/designhub/service/TaskService.java index c0adc03..833b9d3 100644 --- a/backend/src/main/java/com/designhub/service/TaskService.java +++ b/backend/src/main/java/com/designhub/service/TaskService.java @@ -1282,24 +1282,34 @@ public class TaskService { throw new BusinessException(ResultCode.FORBIDDEN, "只有任务发布人可以更新任务"); } - // 允许更新被驳回的任务(approvalStatus包含rejected或status为draft) - boolean isRejected = (existTask.getApprovalStatus() != null && - (existTask.getApprovalStatus().contains("rejected") || - "draft".equals(existTask.getApprovalStatus()))) || - "draft".equals(existTask.getStatus()); + // 允许更新的任务状态:草稿、待审核(需求/接单部门审核)、被驳回 + // 不允许更新的状态:进行中(in_progress)、已完成(completed)、已取消(cancelled) + String status = existTask.getStatus(); + String approvalStatus = existTask.getApprovalStatus(); + + boolean canUpdate = "draft".equals(status) || + "pending".equals(status) || + (approvalStatus != null && ( + "draft".equals(approvalStatus) || + "pending_demand_approval".equals(approvalStatus) || + "pending_receiving_approval".equals(approvalStatus) || + approvalStatus.contains("rejected") + )); + + if (!canUpdate) { + throw new BusinessException(ResultCode.PARAM_ERROR, "任务已在进行中或已完成,无法编辑"); + } task.setId(id); - // 如果是被驳回的任务,更新后重置审核状态为draft,以便重新提交审核 - if (isRejected) { - task.setApprovalStatus("draft"); - task.setStatus("draft"); - task.setRejectReason(null); // 清空驳回原因 - } + // 更新后重置审核状态为draft,以便重新提交审核 + task.setApprovalStatus("draft"); + task.setStatus("draft"); + task.setRejectReason(null); // 清空驳回原因 taskMapper.updateById(task); - log.info("任务更新成功: taskId={}, userId={}, isRejected={}", id, userId, isRejected); + log.info("任务更新成功: taskId={}, userId={}, canUpdate={}", id, userId, canUpdate); } /** @@ -1320,14 +1330,22 @@ public class TaskService { throw new BusinessException(ResultCode.PARAM_ERROR, "该任务不是流水线任务"); } - // 允许更新被驳回的任务(approvalStatus包含rejected或status为draft) - boolean isRejected = (existTask.getApprovalStatus() != null && - (existTask.getApprovalStatus().contains("rejected") || - "draft".equals(existTask.getApprovalStatus()))) || - "draft".equals(existTask.getStatus()); + // 允许更新的任务状态:草稿、待审核(需求/接单部门审核)、被驳回 + // 不允许更新的状态:进行中(in_progress)、已完成(completed)、已取消(cancelled) + String status = existTask.getStatus(); + String approvalStatus = existTask.getApprovalStatus(); - if (!isRejected) { - throw new BusinessException(ResultCode.PARAM_ERROR, "只能更新被驳回或草稿状态的任务"); + boolean canUpdate = "draft".equals(status) || + "pending".equals(status) || // 待接单状态也允许编辑 + (approvalStatus != null && ( + "draft".equals(approvalStatus) || + "pending_demand_approval".equals(approvalStatus) || + "pending_receiving_approval".equals(approvalStatus) || + approvalStatus.contains("rejected") + )); + + if (!canUpdate) { + throw new BusinessException(ResultCode.PARAM_ERROR, "任务已在进行中或已完成,无法编辑"); } // 验证流水线模式 diff --git a/扣分逻辑说明.md b/扣分逻辑说明.md index f5e9c99..ee55c35 100644 --- a/扣分逻辑说明.md +++ b/扣分逻辑说明.md @@ -1,140 +1,369 @@ # 设计师绩效排行榜扣分逻辑说明 -## 一、当前实现逻辑(数据分析页面) +## 一、当前系统扣分机制概述 -数据分析页面的设计师绩效排行榜使用的是 `AnalyticsService.getDesignerPerformance` 方法,扣分逻辑如下: +系统采用**按比例扣分**的机制,扣分主要包含两部分: +1. **延误扣分** - 任务超时完成或进行中任务已超时 +2. **进度未更新扣分** - 设计师未按时更新任务进度 -### 1. 延误扣分计算公式 +--- + +## 二、延误扣分计算规则 + +### 1. 扣分公式 ``` -扣分 = 任务积分 × 延误小时数 × 扣分比例(默认1%) +延误扣分 = 任务积分 × 延误工作小时数 × 扣分比例(%) ``` -**限制条件:** -- 扣分不能超过任务积分 -- 如果计算结果超过任务积分,则扣分 = 任务积分 +**重要说明:** +- **延误工作小时数**:只计算工作时间内的延误,非工作时间不计入(工作时间默认为 9:00-18:00,可在系统设置中配置) +- **扣分比例**:默认为 1%(即每延误1个工作小时,扣任务积分的1%) +- **扣分上限**:单个任务的扣分不能超过该任务的积分 -**扣分比例:** -- 默认值:1%(即 `BigDecimal.ONE`) -- 可以从部门配置中获取(`DesignerTargetScore.deductionRate`) -- 三级部门继承二级部门的配置 +### 2. 扣分比例配置 -### 2. 进度未更新扣分 +| 配置级别 | 说明 | +|---------|------| +| 默认值 | 1%(每延误1小时扣任务积分的1%)| +| 部门配置 | 可在部门目标积分设置中配置 `deductionRate` 字段 | +| 继承规则 | 三级部门继承二级部门的配置,如果二级部门未配置则使用默认值 | -除了延误扣分,还会加上进度未更新扣分: -- 从 `progress_deduction_log` 表中查询当月的进度未更新扣分总额 -- 进度未更新扣分 + 延误扣分 = 总扣分 +### 3. 扣分场景 -### 3. 净积分计算 +#### 场景A:已完成任务延误扣分 + +适用条件: +- 任务状态为 `completed`(已完成) +- 完成时间晚于截止时间 + +计算方式: +``` +延误小时数 = 工作时间计算器.计算延误小时(截止时间, 实际完成时间) +扣分 = 任务积分 × 延误小时数 × 扣分比例 / 100 +``` + +#### 场景B:进行中任务延误扣分 + +适用条件: +- 任务状态为 `in_progress`(进行中) +- 当前时间已超过截止时间 + +计算方式: +``` +延误小时数 = 工作时间计算器.计算延误小时(截止时间, 当前时间) +扣分 = 任务积分 × 延误小时数 × 扣分比例 / 100 +``` + +**注意**:进行中任务的延误扣分是**实时计算**的,会随着时间推移而增加。 + +### 4. 主任务与子任务扣分 + +系统同时计算主任务和子任务的延误扣分: + +| 任务类型 | 截止时间来源 | 完成时间来源 | +|---------|-------------|-------------| +| 主任务 | `task.deadline` 字段 | `task.actualEndTime` 字段 | +| 子任务 | 根据 `createTime + estimatedHours` 计算 | `taskItem.completeTime` 字段 | + +### 5. 扣分计算示例 + +**示例1:任务按时完成** +- 任务积分:10分 +- 截止时间:2026-01-15 18:00 +- 完成时间:2026-01-15 17:30 +- 延误扣分:0分(未延误) + +**示例2:任务延误2小时** +- 任务积分:10分 +- 截止时间:2026-01-15 16:00 +- 完成时间:2026-01-15 18:00 +- 延误小时数:2小时(工作时间内) +- 扣分比例:1% +- 延误扣分:10 × 2 × 1% = 0.20分 + +**示例3:任务延误跨天** +- 任务积分:20分 +- 截止时间:2026-01-15 17:00 +- 完成时间:2026-01-16 11:00 +- 延误工作小时数:1小时(15日17:00-18:00)+ 2小时(16日9:00-11:00)= 3小时 +- 扣分比例:1% +- 延误扣分:20 × 3 × 1% = 0.60分 + +**示例4:延误扣分达到上限** +- 任务积分:5分 +- 延误小时数:1000小时 +- 理论扣分:5 × 1000 × 1% = 50分 +- 实际扣分:5分(不能超过任务积分) + +--- + +## 三、进度未更新扣分 + +### 1. 触发条件 + +- 设计师每日 17:30 后首次登录需要更新进行中任务的进度 +- 如果未更新进度,系统将记录扣分 + +### 2. 扣分记录 + +- 存储在 `progress_deduction_log` 表中 +- 按月汇总计入设计师的总扣分 + +### 3. 查询方式 + +```sql +SELECT SUM(deduction_amount) +FROM progress_deduction_log +WHERE user_id = ? + AND YEAR(deduction_date) = ? + AND MONTH(deduction_date) = ? +``` + +--- + +## 四、净积分计算 + +### 计算公式 ``` +总扣分 = 延误扣分 + 进度未更新扣分 净积分 = 获得积分 - 总扣分 如果净积分 < 0,则净积分 = 0 ``` -## 二、需求文档中的扣分规则(DelayPenaltyService) +### 数据展示 -需求文档中定义的延误扣分规则(在 `DelayPenaltyService` 中实现,但**当前数据分析页面未使用**): +在数据分析页面的设计师绩效排行榜中: +- **获得积分**:当月完成的所有任务(主任务+子任务)的积分总和 +- **延误扣分**:当月所有任务的延误扣分总和 +- **净积分**:获得积分 - 延误扣分(最小为0) -| 延误程度 | 延误时长 | 扣分规则 | 示例 | -|---------|---------|---------|------| -| 轻微延误 | < 2小时 | 每小时扣2分 | 延误1.5小时 → 扣3分 | -| 一般延误 | 2-8小时 | 每小时扣3分 | 延误5小时 → 扣15分 | -| 严重延误 | 8-24小时 | 每小时扣5分 | 延误10小时 → 扣50分 | -| 重大延误 | > 24小时 | 基础50分+每天10分 | 延误2天 → 扣70分 | +--- -## 三、"若兰"被扣10分的情况分析 +## 五、任务截止时间计算 -从图片数据看: -- **获得积分**:10分 -- **延误扣分**:10分 -- **净积分**:0分 +### 1. 计算公式 -### 可能的原因: +``` +截止时间 = 接单时间 + 预估工时(只计算工作时间段) +``` -1. **情况A:延误扣分达到上限** - - 如果任务积分是10分,延误时间较长 - - 按照公式:`10分 × 延误小时数 × 1% = 10分` - - 或者计算结果超过10分,被限制为10分 +**核心原则**:截止时间的计算只累加工作时间段内的小时数,非工作时间(下班后、周末)自动跳过。 -2. **情况B:延误扣分 + 进度未更新扣分 = 10分** - - 延误扣分 + 进度未更新扣分 = 10分 - - 例如:延误扣分5分 + 进度未更新扣分5分 = 10分 +### 2. 计算逻辑详解 -## 四、代码位置 +#### 步骤1:确定起始点 -### 当前使用的扣分逻辑: -- 文件:`backend/src/main/java/com/designhub/service/AnalyticsService.java` -- 方法:`getDesignerPerformance`(第535行) -- 扣分计算:第680-698行(延误扣分)+ 第701-718行(进度未更新扣分) -- 扣分比例获取:`getDeductionRate`(第1146行) +| 接单时间 | 起始点处理 | +|---------|-----------| +| 在工作时间内(9:00-18:00) | 从接单时间开始计算 | +| 在上班前(如 8:00) | 从当天 9:00 开始计算 | +| 在下班后(如 19:00) | 从次日 9:00 开始计算 | -### 需求文档中的扣分逻辑(未在数据分析页面使用): -- 文件:`backend/src/main/java/com/designhub/service/DelayPenaltyService.java` -- 方法:`calculateDelayPenalty`(第36行) -- 规则:按延误等级和固定扣分规则 +#### 步骤2:累加工时 -## 五、建议复核的问题 +从起始点开始,逐步累加工作小时数: +1. 计算当天剩余工作时间 +2. 如果当天工作时间足够,直接加上剩余工时 +3. 如果当天工作时间不够,移动到下一个工作日继续累加 -1. **数据分析页面应该使用哪套扣分规则?** - - 当前实现:按百分比扣分(任务积分 × 延误小时数 × 扣分比例) - - 需求文档:按延误等级固定扣分(轻微/一般/严重/重大) +### 3. 计算示例 -2. **扣分比例默认值是否合理?** - - 当前默认:1% - - 如果任务积分100分,延误10小时,扣分 = 100 × 10 × 1% = 10分 - - 如果任务积分10分,延误100小时,扣分 = min(10 × 100 × 1%, 10) = 10分 +#### 示例1:当天可完成 -3. **进度未更新扣分是否应该计入?** - - 当前实现:计入总扣分 - - 是否需要单独显示? +``` +接单时间:2026-01-15 10:00(周三) +预估工时:5小时 +当天剩余工作时间:18:00 - 10:00 = 8小时 ✓ 足够 -4. **是否需要使用 DelayPenaltyRecord 表的数据?** - - 当前实现:直接从任务表计算,不使用 DelayPenaltyRecord - - 需求文档中有 DelayPenaltyRecord 表的设计,但数据分析页面未使用 +截止时间 = 10:00 + 5小时 = 2026-01-15 15:00 +``` -## 六、相关代码片段 +#### 示例2:需要跨天 + +``` +接单时间:2026-01-15 16:00(周三) +预估工时:5小时 +当天剩余工作时间:18:00 - 16:00 = 2小时 ✗ 不够 + +计算过程: +- 第1天(周三):消耗 2小时,剩余 3小时 +- 第2天(周四):从 9:00 开始,消耗 3小时 + +截止时间 = 2026-01-16 12:00 +``` + +#### 示例3:下班后接单 + +``` +接单时间:2026-01-15 19:00(周三,下班后) +预估工时:3小时 + +计算过程: +- 起始点移动到次日 9:00 +- 从 2026-01-16 09:00 开始计算 + +截止时间 = 2026-01-16 12:00 +``` + +#### 示例4:跨多天 + +``` +接单时间:2026-01-15 16:00(周三) +预估工时:12小时 +当天剩余工作时间:18:00 - 16:00 = 2小时 ✗ 不够 + +计算过程: +- 第1天(周三):消耗 2小时,剩余 10小时 +- 第2天(周四):消耗 9小时(9:00-18:00),剩余 1小时 +- 第3天(周五):从 9:00 开始,消耗 1小时 + +截止时间 = 2026-01-17 10:00(周五) +``` + +**注意**:当前系统按每天 9:00-18:00 计算工作时间,**周六周日也按正常工作日计算**,暂未配置节假日跳过功能。 + +### 4. 主任务与子任务的区别 + +| 任务类型 | 截止时间设置方式 | 说明 | +|---------|-----------------|------| +| 主任务 | 发布时直接设置 `deadline` | 由任务发布者手动设置 | +| 子任务 | 系统自动计算 | 接单时间 + 预估工时 | + +#### 子任务截止时间触发时机 + +子任务的截止时间在以下场景自动计算: +1. **接单部门审核通过时** - 子任务状态变为 `in_progress` +2. **设计师接单时** - 从任务大厅抢单 + +### 5. 代码实现 + +**核心方法**:`WorkHoursCalculator.calculateDeadline()` -### 当前扣分计算逻辑(AnalyticsService.java 第680-698行): ```java -// 计算延误扣分(如果超时完成) -if (task.getDeadline() != null && task.getActualEndTime() != null - && task.getActualEndTime().isAfter(task.getDeadline())) { - // 计算延误小时数 - long overdueHours = Duration.between(task.getDeadline(), task.getActualEndTime()).toHours(); - if (overdueHours > 0 && taskScore.compareTo(BigDecimal.ZERO) > 0) { - // 扣分 = 任务积分 * 延误小时数 * 扣分比例 - BigDecimal taskDeduction = taskScore - .multiply(BigDecimal.valueOf(overdueHours)) - .multiply(deductionRate) - .divide(BigDecimal.valueOf(100), 2, RoundingMode.HALF_UP); - // 扣分不能超过任务积分 - if (taskDeduction.compareTo(taskScore) > 0) { - taskDeduction = taskScore; - } - deductionScore = deductionScore.add(taskDeduction); - } +/** + * 计算截止时间(只计算工作时间段) + * @param startTime 开始时间(接单时间) + * @param workHours 需要的工作小时数 + * @return 截止时间 + */ +public LocalDateTime calculateDeadline(LocalDateTime startTime, BigDecimal workHours) { + // 1. 获取工作时间配置(默认 9:00-18:00) + // 2. 如果开始时间不在工作时间内,移动到下一个工作时段 + // 3. 循环累加工时,只在工作时间段内计算 + // 4. 返回最终截止时间 } ``` -### 需求文档中的扣分规则(DelayPenaltyService.java 第56-72行): -```java -if (delayHours < 2) { - // 轻微延误:每小时扣2分 - delayLevel = "light"; - penaltyScore = (int) Math.ceil(delayHours * 2.0); -} else if (delayHours < 8) { - // 一般延误:每小时扣3分 - delayLevel = "general"; - penaltyScore = (int) Math.ceil(delayHours * 3.0); -} else if (delayHours < 24) { - // 严重延误:每小时扣5分 - delayLevel = "severe"; - penaltyScore = (int) Math.ceil(delayHours * 5.0); -} else { - // 重大延误:基础50分+每天10分 - delayLevel = "major"; - penaltyScore = 50 + (int) delayDays * 10; -} +**调用位置**: +- `TaskService.updatePipelineSubTasksForAssigned()` - 审核通过时设置 +- `TaskItemService.approveTaskItemGroup()` - 指派设计师时设置 + +--- + +## 六、工作时间配置 + +### 1. 配置位置 + +系统设置 → 工作时间配置 + +### 2. 配置项 + +| 配置键 | 说明 | 默认值 | +|-------|------|--------| +| `work_hours.start_time` | 上班开始时间 | 09:00 | +| `work_hours.end_time` | 下班结束时间 | 18:00 | + +### 3. 影响范围 + +- 任务截止时间计算 +- 延误小时数计算(只计算工作时间内的延误) +- 延误扣分计算 + +--- + +## 七、代码位置参考 + +### 核心扣分计算 + +- **文件**:`backend/src/main/java/com/designhub/service/AnalyticsService.java` +- **方法**:`getDesignerPerformance()` +- **扣分比例获取**:`getDeductionRate()` 方法(第1325行) + +### 工作时间计算器 + +- **文件**:`backend/src/main/java/com/designhub/util/WorkHoursCalculator.java` +- **截止时间计算**:`calculateDeadline()` 方法 +- **延误小时计算**:`calculateDelayHours()` 方法 + +### 进度未更新扣分 + +- **记录表**:`progress_deduction_log` +- **汇总查询**:`ProgressDeductionLogMapper.sumMonthlyDeduction()` + +--- + +## 八、扣分规则对比(历史参考) + +### 当前实现(按比例扣分) + +``` +扣分 = 任务积分 × 延误工作小时数 × 扣分比例(1%) +扣分上限 = 任务积分 ``` +**优点**: +- 与任务价值挂钩,公平合理 +- 扣分上限明确,不会出现扣分超过任务价值的情况 +- 只计算工作时间,更加人性化 + +### 历史设计(按等级固定扣分,已废弃) + +| 延误程度 | 延误时长 | 扣分规则 | +|---------|---------|---------| +| 轻微延误 | < 2小时 | 每小时扣2分 | +| 一般延误 | 2-8小时 | 每小时扣3分 | +| 严重延误 | 8-24小时 | 每小时扣5分 | +| 重大延误 | > 24小时 | 基础50分+每天10分 | + +**注意**:此规则定义在 `DelayPenaltyService.java` 中,但**当前数据分析页面未使用**。 + +--- + +## 九、常见问题 + +### Q1:为什么扣分等于获得积分? + +**原因**:扣分达到上限(任务积分) + +**示例**: +- 任务积分:10分 +- 延误100小时 +- 理论扣分:10 × 100 × 1% = 10分 +- 实际扣分:10分(达到上限) + +### Q2:非工作时间(下班后)延误怎么计算? + +**原因**:系统只计算工作时间内的延误(9:00-18:00) + +**示例**: +- 截止时间:周三 17:00 +- 完成时间:周四 11:00 +- 延误工作小时:1小时(周三 17:00-18:00)+ 2小时(周四 9:00-11:00)= 3小时 +- 晚上 18:00 到次日 9:00 的时间不计入延误 + +**注意**:当前系统周六周日也按正常工作日计算,暂未配置周末跳过功能。 + +### Q3:子任务没有设置截止时间怎么计算? + +**方式**:根据子任务的创建时间 + 预估工时自动计算截止时间 + +``` +子任务截止时间 = 创建时间 + 预估工时(按工作时间计算) +``` + +--- + +*文档更新时间:2026-01-15* +*文档版本:v2.1*