feat: 实施工作时间配置系统
- 添加工作时间配置数据库迁移(V12__add_work_hours_config.sql) - 创建WorkHoursCalculator工具类用于工作时间计算 - 添加工作时间配置API接口(SystemConfigController) - 修改后端服务使用动态工作时间(TaskService、TaskItemService、TaskApprovalService、AnalyticsService) - 在Settings.vue中添加工作时间配置UI - 修改前端时效格式化函数使用动态工作时长(TaskHall.vue、CreateTask.vue) - 所有时间计算(截止时间、延误扣分、时效格式化)现在使用配置的工作时间,替换硬编码的8小时
This commit is contained in:
parent
51264aef06
commit
124e18ab78
@ -3,11 +3,15 @@ package com.designhub.controller;
|
||||
import com.designhub.common.Result;
|
||||
import com.designhub.entity.SystemConfig;
|
||||
import com.designhub.service.SystemConfigService;
|
||||
import com.designhub.util.WorkHoursCalculator;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.Data;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.time.LocalTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@ -21,6 +25,64 @@ import java.util.Map;
|
||||
public class SystemConfigController {
|
||||
|
||||
private final SystemConfigService systemConfigService;
|
||||
private final WorkHoursCalculator workHoursCalculator;
|
||||
|
||||
/**
|
||||
* 工作时间配置DTO
|
||||
*/
|
||||
@Data
|
||||
public static class WorkHoursConfigDTO {
|
||||
private String startTime; // 上班开始时间,格式:HH:mm
|
||||
private String endTime; // 上班结束时间,格式:HH:mm
|
||||
private int workHoursPerDay; // 每天工作小时数
|
||||
}
|
||||
|
||||
/**
|
||||
* 工作时间配置更新DTO
|
||||
*/
|
||||
@Data
|
||||
public static class WorkHoursConfigUpdateDTO {
|
||||
private String startTime; // 上班开始时间,格式:HH:mm
|
||||
private String endTime; // 上班结束时间,格式:HH:mm
|
||||
}
|
||||
|
||||
@Operation(summary = "获取工作时间配置")
|
||||
@GetMapping("/work-hours")
|
||||
public Result<WorkHoursConfigDTO> getWorkHoursConfig() {
|
||||
WorkHoursCalculator.WorkHoursConfig config = workHoursCalculator.getWorkHoursConfig();
|
||||
WorkHoursConfigDTO dto = new WorkHoursConfigDTO();
|
||||
dto.setStartTime(config.getStartTime().format(DateTimeFormatter.ofPattern("HH:mm")));
|
||||
dto.setEndTime(config.getEndTime().format(DateTimeFormatter.ofPattern("HH:mm")));
|
||||
dto.setWorkHoursPerDay(config.getWorkHoursPerDay());
|
||||
return Result.success(dto);
|
||||
}
|
||||
|
||||
@Operation(summary = "更新工作时间配置")
|
||||
@PutMapping("/work-hours")
|
||||
public Result<Boolean> updateWorkHoursConfig(@RequestBody WorkHoursConfigUpdateDTO updateDTO) {
|
||||
// 验证时间格式
|
||||
try {
|
||||
LocalTime.parse(updateDTO.getStartTime(), DateTimeFormatter.ofPattern("HH:mm"));
|
||||
LocalTime.parse(updateDTO.getEndTime(), DateTimeFormatter.ofPattern("HH:mm"));
|
||||
} catch (Exception e) {
|
||||
return Result.error("时间格式错误,应为 HH:mm 格式(如 09:00)");
|
||||
}
|
||||
|
||||
// 验证开始时间不能晚于结束时间
|
||||
LocalTime startTime = LocalTime.parse(updateDTO.getStartTime(), DateTimeFormatter.ofPattern("HH:mm"));
|
||||
LocalTime endTime = LocalTime.parse(updateDTO.getEndTime(), DateTimeFormatter.ofPattern("HH:mm"));
|
||||
if (!startTime.isBefore(endTime)) {
|
||||
return Result.error("上班开始时间必须早于结束时间");
|
||||
}
|
||||
|
||||
// 更新配置
|
||||
Map<String, String> configMap = new java.util.HashMap<>();
|
||||
configMap.put("work_hours.start_time", updateDTO.getStartTime());
|
||||
configMap.put("work_hours.end_time", updateDTO.getEndTime());
|
||||
systemConfigService.batchUpdateValues(configMap);
|
||||
|
||||
return Result.success(true);
|
||||
}
|
||||
|
||||
@Operation(summary = "获取所有配置列表")
|
||||
@GetMapping
|
||||
|
||||
@ -41,6 +41,7 @@ public class AnalyticsService {
|
||||
private final DesignerTargetScoreMapper designerTargetScoreMapper;
|
||||
private final DemandTypeMapper demandTypeMapper;
|
||||
private final ProgressDeductionLogMapper progressDeductionLogMapper;
|
||||
private final com.designhub.util.WorkHoursCalculator workHoursCalculator;
|
||||
|
||||
/**
|
||||
* 获取需求统计数据(根据角色返回不同维度的数据)
|
||||
@ -81,13 +82,13 @@ public class AnalyticsService {
|
||||
.le(Task::getCreateTime, endTime);
|
||||
|
||||
// 如果指定了部门ID,则按部门过滤
|
||||
if (deptId != null) {
|
||||
queryWrapper.eq(Task::getPublisherDeptId, deptId);
|
||||
if (deptId != null) {
|
||||
queryWrapper.eq(Task::getPublisherDeptId, deptId);
|
||||
log.info("按部门过滤: deptId={}", deptId);
|
||||
}
|
||||
// 如果指定了用户ID,则按用户过滤
|
||||
if (userId != null) {
|
||||
queryWrapper.eq(Task::getPublisherId, userId);
|
||||
queryWrapper.eq(Task::getPublisherId, userId);
|
||||
log.info("按用户过滤: userId={}", userId);
|
||||
}
|
||||
|
||||
@ -690,12 +691,12 @@ public class AnalyticsService {
|
||||
// 计算延误扣分(如果超时完成)
|
||||
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 overdueHours = workHoursCalculator.calculateDelayHours(task.getDeadline(), task.getActualEndTime());
|
||||
if (overdueHours.compareTo(BigDecimal.ZERO) > 0 && taskScore.compareTo(BigDecimal.ZERO) > 0) {
|
||||
// 扣分 = 任务积分 * 延误小时数 * 扣分比例
|
||||
BigDecimal taskDeduction = taskScore
|
||||
.multiply(BigDecimal.valueOf(overdueHours))
|
||||
.multiply(overdueHours)
|
||||
.multiply(deductionRate)
|
||||
.divide(BigDecimal.valueOf(100), 2, RoundingMode.HALF_UP);
|
||||
// 扣分不能超过任务积分
|
||||
@ -711,12 +712,12 @@ public class AnalyticsService {
|
||||
&& task.getDeadline().isBefore(endTime) // deadline在查询月份范围内
|
||||
&& now.isAfter(task.getDeadline()) // 当前时间已超过deadline
|
||||
&& taskScore.compareTo(BigDecimal.ZERO) > 0) {
|
||||
// 计算延误小时数(当前时间 - deadline)
|
||||
long overdueHours = Duration.between(task.getDeadline(), now).toHours();
|
||||
if (overdueHours > 0) {
|
||||
// 计算延误小时数(只计算工作时间段内的延误)
|
||||
BigDecimal overdueHours = workHoursCalculator.calculateDelayHours(task.getDeadline(), now);
|
||||
if (overdueHours.compareTo(BigDecimal.ZERO) > 0) {
|
||||
// 扣分 = 任务积分 * 延误小时数 * 扣分比例
|
||||
BigDecimal taskDeduction = taskScore
|
||||
.multiply(BigDecimal.valueOf(overdueHours))
|
||||
.multiply(overdueHours)
|
||||
.multiply(deductionRate)
|
||||
.divide(BigDecimal.valueOf(100), 2, RoundingMode.HALF_UP);
|
||||
// 扣分不能超过任务积分
|
||||
@ -754,22 +755,21 @@ public class AnalyticsService {
|
||||
if (item.getEstimatedHours() != null && item.getEstimatedHours().compareTo(BigDecimal.ZERO) > 0
|
||||
&& item.getCreateTime() != null && item.getCompleteTime() != null) {
|
||||
|
||||
// 计算子任务的预期deadline:createTime + estimatedHours
|
||||
long estimatedMinutes = item.getEstimatedHours().multiply(BigDecimal.valueOf(60)).longValue();
|
||||
LocalDateTime expectedDeadline = item.getCreateTime().plusMinutes(estimatedMinutes);
|
||||
// 计算子任务的预期deadline:createTime + estimatedHours(使用工作时间计算)
|
||||
LocalDateTime expectedDeadline = workHoursCalculator.calculateDeadline(item.getCreateTime(), item.getEstimatedHours());
|
||||
|
||||
// 如果完成时间超过预期deadline,计算延误扣分
|
||||
if (item.getCompleteTime().isAfter(expectedDeadline)) {
|
||||
if (expectedDeadline != null && item.getCompleteTime().isAfter(expectedDeadline)) {
|
||||
BigDecimal itemScore = item.getTotalScore() != null ? BigDecimal.valueOf(item.getTotalScore()) : BigDecimal.ZERO;
|
||||
|
||||
if (itemScore.compareTo(BigDecimal.ZERO) > 0) {
|
||||
// 计算延误小时数
|
||||
long overdueHours = Duration.between(expectedDeadline, item.getCompleteTime()).toHours();
|
||||
// 计算延误小时数(只计算工作时间段内的延误)
|
||||
BigDecimal overdueHours = workHoursCalculator.calculateDelayHours(expectedDeadline, item.getCompleteTime());
|
||||
|
||||
if (overdueHours > 0) {
|
||||
if (overdueHours.compareTo(BigDecimal.ZERO) > 0) {
|
||||
// 计算扣分
|
||||
BigDecimal itemDeduction = itemScore
|
||||
.multiply(BigDecimal.valueOf(overdueHours))
|
||||
.multiply(overdueHours)
|
||||
.multiply(deductionRate)
|
||||
.divide(BigDecimal.valueOf(100), 2, RoundingMode.HALF_UP);
|
||||
|
||||
@ -790,22 +790,21 @@ public class AnalyticsService {
|
||||
if (item.getEstimatedHours() != null && item.getEstimatedHours().compareTo(BigDecimal.ZERO) > 0
|
||||
&& item.getCreateTime() != null) {
|
||||
|
||||
// 计算子任务的预期deadline:createTime + estimatedHours
|
||||
long estimatedMinutes = item.getEstimatedHours().multiply(BigDecimal.valueOf(60)).longValue();
|
||||
LocalDateTime expectedDeadline = item.getCreateTime().plusMinutes(estimatedMinutes);
|
||||
// 计算子任务的预期deadline:createTime + estimatedHours(使用工作时间计算)
|
||||
LocalDateTime expectedDeadline = workHoursCalculator.calculateDeadline(item.getCreateTime(), item.getEstimatedHours());
|
||||
|
||||
// 如果当前时间超过预期deadline,计算延误扣分
|
||||
if (now.isAfter(expectedDeadline)) {
|
||||
if (expectedDeadline != null && now.isAfter(expectedDeadline)) {
|
||||
BigDecimal itemScore = item.getTotalScore() != null ? BigDecimal.valueOf(item.getTotalScore()) : BigDecimal.ZERO;
|
||||
|
||||
if (itemScore.compareTo(BigDecimal.ZERO) > 0) {
|
||||
// 计算延误小时数(当前时间 - 预期deadline)
|
||||
long overdueHours = Duration.between(expectedDeadline, now).toHours();
|
||||
// 计算延误小时数(只计算工作时间段内的延误)
|
||||
BigDecimal overdueHours = workHoursCalculator.calculateDelayHours(expectedDeadline, now);
|
||||
|
||||
if (overdueHours > 0) {
|
||||
if (overdueHours.compareTo(BigDecimal.ZERO) > 0) {
|
||||
// 计算扣分
|
||||
BigDecimal itemDeduction = itemScore
|
||||
.multiply(BigDecimal.valueOf(overdueHours))
|
||||
.multiply(overdueHours)
|
||||
.multiply(deductionRate)
|
||||
.divide(BigDecimal.valueOf(100), 2, RoundingMode.HALF_UP);
|
||||
|
||||
@ -1497,13 +1496,13 @@ public class AnalyticsService {
|
||||
}
|
||||
|
||||
if (taskScore.compareTo(BigDecimal.ZERO) > 0) {
|
||||
// 计算延误小时数
|
||||
long overdueHours = Duration.between(task.getDeadline(), task.getActualEndTime()).toHours();
|
||||
// 计算延误小时数(只计算工作时间段内的延误)
|
||||
BigDecimal overdueHours = workHoursCalculator.calculateDelayHours(task.getDeadline(), task.getActualEndTime());
|
||||
|
||||
if (overdueHours > 0) {
|
||||
if (overdueHours.compareTo(BigDecimal.ZERO) > 0) {
|
||||
// 计算扣分
|
||||
BigDecimal taskDeduction = taskScore
|
||||
.multiply(BigDecimal.valueOf(overdueHours))
|
||||
.multiply(overdueHours)
|
||||
.multiply(deductionRate)
|
||||
.divide(BigDecimal.valueOf(100), 2, RoundingMode.HALF_UP);
|
||||
|
||||
@ -1514,8 +1513,9 @@ public class AnalyticsService {
|
||||
|
||||
totalDelayDeduction = totalDelayDeduction.add(taskDeduction);
|
||||
|
||||
long overdueHoursLong = overdueHours.longValue();
|
||||
log.info("发现已完成延误任务: taskNo={}, 任务积分={}, 延误小时={}, 扣分={}",
|
||||
task.getTaskNo(), taskScore, overdueHours, taskDeduction);
|
||||
task.getTaskNo(), taskScore, overdueHoursLong, taskDeduction);
|
||||
|
||||
delayDeductions.add(DeductionDetailVO.DelayDeductionItem.builder()
|
||||
.taskId(task.getId())
|
||||
@ -1524,9 +1524,9 @@ public class AnalyticsService {
|
||||
.taskScore(taskScore)
|
||||
.deadline(task.getDeadline())
|
||||
.actualEndTime(task.getActualEndTime())
|
||||
.overdueHours(overdueHours)
|
||||
.overdueHours(overdueHoursLong)
|
||||
.deductionScore(taskDeduction)
|
||||
.deductionReason("任务延误 " + overdueHours + " 小时")
|
||||
.deductionReason("任务延误 " + overdueHoursLong + " 小时")
|
||||
.build());
|
||||
}
|
||||
}
|
||||
@ -1550,16 +1550,16 @@ public class AnalyticsService {
|
||||
log.info("进行中任务积分计算: taskNo={}, taskScore={}", task.getTaskNo(), taskScore);
|
||||
|
||||
if (taskScore.compareTo(BigDecimal.ZERO) > 0) {
|
||||
// 计算延误小时数(当前时间 - deadline)
|
||||
long overdueHours = Duration.between(task.getDeadline(), now).toHours();
|
||||
// 计算延误小时数(只计算工作时间段内的延误)
|
||||
BigDecimal overdueHours = workHoursCalculator.calculateDelayHours(task.getDeadline(), now);
|
||||
|
||||
log.info("进行中任务延误计算: taskNo={}, deadline={}, now={}, overdueHours={}",
|
||||
task.getTaskNo(), task.getDeadline(), now, overdueHours);
|
||||
|
||||
if (overdueHours > 0) {
|
||||
if (overdueHours.compareTo(BigDecimal.ZERO) > 0) {
|
||||
// 计算扣分
|
||||
BigDecimal taskDeduction = taskScore
|
||||
.multiply(BigDecimal.valueOf(overdueHours))
|
||||
.multiply(overdueHours)
|
||||
.multiply(deductionRate)
|
||||
.divide(BigDecimal.valueOf(100), 2, RoundingMode.HALF_UP);
|
||||
|
||||
@ -1570,8 +1570,9 @@ public class AnalyticsService {
|
||||
|
||||
totalDelayDeduction = totalDelayDeduction.add(taskDeduction);
|
||||
|
||||
long overdueHoursLong = overdueHours.longValue();
|
||||
log.info("发现进行中延误任务: taskNo={}, 任务积分={}, 延误小时={}, 扣分={}",
|
||||
task.getTaskNo(), taskScore, overdueHours, taskDeduction);
|
||||
task.getTaskNo(), taskScore, overdueHoursLong, taskDeduction);
|
||||
|
||||
delayDeductions.add(DeductionDetailVO.DelayDeductionItem.builder()
|
||||
.taskId(task.getId())
|
||||
@ -1580,9 +1581,9 @@ public class AnalyticsService {
|
||||
.taskScore(taskScore)
|
||||
.deadline(task.getDeadline())
|
||||
.actualEndTime(null) // 进行中任务没有完成时间
|
||||
.overdueHours(overdueHours)
|
||||
.overdueHours(overdueHoursLong)
|
||||
.deductionScore(taskDeduction)
|
||||
.deductionReason("任务进行中,已延误 " + overdueHours + " 小时")
|
||||
.deductionReason("任务进行中,已延误 " + overdueHoursLong + " 小时")
|
||||
.build());
|
||||
}
|
||||
}
|
||||
@ -1616,22 +1617,21 @@ public class AnalyticsService {
|
||||
if (item.getEstimatedHours() != null && item.getEstimatedHours().compareTo(BigDecimal.ZERO) > 0
|
||||
&& item.getCreateTime() != null && item.getCompleteTime() != null) {
|
||||
|
||||
// 计算子任务的预期deadline:createTime + estimatedHours
|
||||
long estimatedMinutes = item.getEstimatedHours().multiply(BigDecimal.valueOf(60)).longValue();
|
||||
LocalDateTime expectedDeadline = item.getCreateTime().plusMinutes(estimatedMinutes);
|
||||
// 计算子任务的预期deadline:createTime + estimatedHours(使用工作时间计算)
|
||||
LocalDateTime expectedDeadline = workHoursCalculator.calculateDeadline(item.getCreateTime(), item.getEstimatedHours());
|
||||
|
||||
// 如果完成时间超过预期deadline,计算延误扣分
|
||||
if (item.getCompleteTime().isAfter(expectedDeadline)) {
|
||||
if (expectedDeadline != null && item.getCompleteTime().isAfter(expectedDeadline)) {
|
||||
BigDecimal itemScore = item.getTotalScore() != null ? BigDecimal.valueOf(item.getTotalScore()) : BigDecimal.ZERO;
|
||||
|
||||
if (itemScore.compareTo(BigDecimal.ZERO) > 0) {
|
||||
// 计算延误小时数
|
||||
long overdueHours = Duration.between(expectedDeadline, item.getCompleteTime()).toHours();
|
||||
// 计算延误小时数(只计算工作时间段内的延误)
|
||||
BigDecimal overdueHours = workHoursCalculator.calculateDelayHours(expectedDeadline, item.getCompleteTime());
|
||||
|
||||
if (overdueHours > 0) {
|
||||
if (overdueHours.compareTo(BigDecimal.ZERO) > 0) {
|
||||
// 计算扣分
|
||||
BigDecimal itemDeduction = itemScore
|
||||
.multiply(BigDecimal.valueOf(overdueHours))
|
||||
.multiply(overdueHours)
|
||||
.multiply(deductionRate)
|
||||
.divide(BigDecimal.valueOf(100), 2, RoundingMode.HALF_UP);
|
||||
|
||||
@ -1647,8 +1647,9 @@ public class AnalyticsService {
|
||||
String taskNo = item.getItemNo() != null ? item.getItemNo() : "";
|
||||
String taskTitle = mainTask != null ? mainTask.getTitle() : "子任务";
|
||||
|
||||
long overdueHoursLong = overdueHours.longValue();
|
||||
log.info("发现已完成延误子任务: itemNo={}, 任务积分={}, 延误小时={}, 扣分={}",
|
||||
taskNo, itemScore, overdueHours, itemDeduction);
|
||||
taskNo, itemScore, overdueHoursLong, itemDeduction);
|
||||
|
||||
delayDeductions.add(DeductionDetailVO.DelayDeductionItem.builder()
|
||||
.taskId(item.getTaskId()) // 使用主任务ID
|
||||
@ -1657,9 +1658,9 @@ public class AnalyticsService {
|
||||
.taskScore(itemScore)
|
||||
.deadline(expectedDeadline)
|
||||
.actualEndTime(item.getCompleteTime())
|
||||
.overdueHours(overdueHours)
|
||||
.overdueHours(overdueHoursLong)
|
||||
.deductionScore(itemDeduction)
|
||||
.deductionReason("子任务延误 " + overdueHours + " 小时")
|
||||
.deductionReason("子任务延误 " + overdueHoursLong + " 小时")
|
||||
.build());
|
||||
}
|
||||
}
|
||||
@ -1672,22 +1673,21 @@ public class AnalyticsService {
|
||||
if (item.getEstimatedHours() != null && item.getEstimatedHours().compareTo(BigDecimal.ZERO) > 0
|
||||
&& item.getCreateTime() != null) {
|
||||
|
||||
// 计算子任务的预期deadline:createTime + estimatedHours
|
||||
long estimatedMinutes = item.getEstimatedHours().multiply(BigDecimal.valueOf(60)).longValue();
|
||||
LocalDateTime expectedDeadline = item.getCreateTime().plusMinutes(estimatedMinutes);
|
||||
// 计算子任务的预期deadline:createTime + estimatedHours(使用工作时间计算)
|
||||
LocalDateTime expectedDeadline = workHoursCalculator.calculateDeadline(item.getCreateTime(), item.getEstimatedHours());
|
||||
|
||||
// 如果当前时间超过预期deadline,计算延误扣分
|
||||
if (now.isAfter(expectedDeadline)) {
|
||||
if (expectedDeadline != null && now.isAfter(expectedDeadline)) {
|
||||
BigDecimal itemScore = item.getTotalScore() != null ? BigDecimal.valueOf(item.getTotalScore()) : BigDecimal.ZERO;
|
||||
|
||||
if (itemScore.compareTo(BigDecimal.ZERO) > 0) {
|
||||
// 计算延误小时数(当前时间 - 预期deadline)
|
||||
long overdueHours = Duration.between(expectedDeadline, now).toHours();
|
||||
// 计算延误小时数(只计算工作时间段内的延误)
|
||||
BigDecimal overdueHours = workHoursCalculator.calculateDelayHours(expectedDeadline, now);
|
||||
|
||||
if (overdueHours > 0) {
|
||||
if (overdueHours.compareTo(BigDecimal.ZERO) > 0) {
|
||||
// 计算扣分
|
||||
BigDecimal itemDeduction = itemScore
|
||||
.multiply(BigDecimal.valueOf(overdueHours))
|
||||
.multiply(overdueHours)
|
||||
.multiply(deductionRate)
|
||||
.divide(BigDecimal.valueOf(100), 2, RoundingMode.HALF_UP);
|
||||
|
||||
@ -1703,8 +1703,9 @@ public class AnalyticsService {
|
||||
String taskNo = item.getItemNo() != null ? item.getItemNo() : "";
|
||||
String taskTitle = mainTask != null ? mainTask.getTitle() : "子任务";
|
||||
|
||||
long overdueHoursLong = overdueHours.longValue();
|
||||
log.info("发现进行中延误子任务: itemNo={}, 任务积分={}, 延误小时={}, 扣分={}",
|
||||
taskNo, itemScore, overdueHours, itemDeduction);
|
||||
taskNo, itemScore, overdueHoursLong, itemDeduction);
|
||||
|
||||
delayDeductions.add(DeductionDetailVO.DelayDeductionItem.builder()
|
||||
.taskId(item.getTaskId()) // 使用主任务ID
|
||||
@ -1713,9 +1714,9 @@ public class AnalyticsService {
|
||||
.taskScore(itemScore)
|
||||
.deadline(expectedDeadline)
|
||||
.actualEndTime(null) // 进行中子任务没有完成时间
|
||||
.overdueHours(overdueHours)
|
||||
.overdueHours(overdueHoursLong)
|
||||
.deductionScore(itemDeduction)
|
||||
.deductionReason("子任务进行中,已延误 " + overdueHours + " 小时")
|
||||
.deductionReason("子任务进行中,已延误 " + overdueHoursLong + " 小时")
|
||||
.build());
|
||||
}
|
||||
}
|
||||
|
||||
@ -43,6 +43,7 @@ public class TaskApprovalService {
|
||||
private final ConfigService configService;
|
||||
private final ObjectMapper objectMapper;
|
||||
private final DingTalkService dingTalkService;
|
||||
private final com.designhub.util.WorkHoursCalculator workHoursCalculator;
|
||||
|
||||
/**
|
||||
* 需求部门负责人审核通过
|
||||
@ -227,14 +228,11 @@ public class TaskApprovalService {
|
||||
task.setStartTime(now);
|
||||
task.setActualStartTime(now);
|
||||
|
||||
// 重新计算deadline:从接单时间开始计算
|
||||
if (task.getEstimatedHours() != null) {
|
||||
int hours = task.getEstimatedHours().intValue();
|
||||
if (hours > 0) {
|
||||
task.setDeadline(now.plusHours(hours));
|
||||
log.info("任务deadline已重新计算:从接单时间 {} 开始,预估 {} 小时,截止时间 {}",
|
||||
now, hours, task.getDeadline());
|
||||
}
|
||||
// 重新计算deadline:从接单时间开始计算(使用工作时间计算)
|
||||
if (task.getEstimatedHours() != null && task.getEstimatedHours().compareTo(java.math.BigDecimal.ZERO) > 0) {
|
||||
task.setDeadline(workHoursCalculator.calculateDeadline(now, task.getEstimatedHours()));
|
||||
log.info("任务deadline已重新计算:从接单时间 {} 开始,预估 {} 小时,截止时间 {}",
|
||||
now, task.getEstimatedHours(), task.getDeadline());
|
||||
}
|
||||
} else {
|
||||
task.setStatus("pending"); // 未指定则发布到大厅
|
||||
@ -855,12 +853,9 @@ public class TaskApprovalService {
|
||||
task.setStartTime(now);
|
||||
task.setActualStartTime(now);
|
||||
|
||||
// 重新计算deadline:从接单时间开始计算
|
||||
if (task.getEstimatedHours() != null) {
|
||||
int hours = task.getEstimatedHours().intValue();
|
||||
if (hours > 0) {
|
||||
task.setDeadline(now.plusHours(hours));
|
||||
}
|
||||
// 重新计算deadline:从接单时间开始计算(使用工作时间计算)
|
||||
if (task.getEstimatedHours() != null && task.getEstimatedHours().compareTo(java.math.BigDecimal.ZERO) > 0) {
|
||||
task.setDeadline(workHoursCalculator.calculateDeadline(now, task.getEstimatedHours()));
|
||||
}
|
||||
|
||||
// 更新子任务状态为in_progress(第一组或全部,取决于流水线模式)
|
||||
@ -901,24 +896,14 @@ public class TaskApprovalService {
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化预估工时显示
|
||||
* 将小时数转换为 "X天X小时" 格式(8小时=1天)
|
||||
* 格式化预估工时显示(基于工作时间配置)
|
||||
*/
|
||||
private String formatEstimatedHours(java.math.BigDecimal hours) {
|
||||
if (hours == null) {
|
||||
return "未设置";
|
||||
}
|
||||
int totalHours = hours.intValue();
|
||||
int days = totalHours / 8;
|
||||
int remainingHours = totalHours % 8;
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
if (days > 0) {
|
||||
sb.append(days).append("天");
|
||||
}
|
||||
if (remainingHours > 0 || days == 0) {
|
||||
sb.append(remainingHours).append("小时");
|
||||
}
|
||||
return sb.toString();
|
||||
String formatted = workHoursCalculator.formatWorkHours(hours);
|
||||
// WorkHoursCalculator 返回 "--" 表示无效,这里保持 "未设置" 的语义
|
||||
return "--".equals(formatted) ? "未设置" : formatted;
|
||||
}
|
||||
}
|
||||
|
||||
@ -44,6 +44,7 @@ public class TaskItemService {
|
||||
private final DemandTypeMapper demandTypeMapper;
|
||||
private final com.designhub.mapper.UserMapper userMapper;
|
||||
private final ObjectMapper objectMapper;
|
||||
private final com.designhub.util.WorkHoursCalculator workHoursCalculator;
|
||||
|
||||
/**
|
||||
* 获取可接单的子任务列表
|
||||
@ -1347,23 +1348,14 @@ public class TaskItemService {
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化工时显示(与TaskApprovalService一致,8小时=1天)
|
||||
* 格式化工时显示(基于工作时间配置)
|
||||
*/
|
||||
private String formatEstimatedHours(java.math.BigDecimal hours) {
|
||||
if (hours == null) {
|
||||
return "未设置";
|
||||
}
|
||||
int totalHours = hours.intValue();
|
||||
int days = totalHours / 8;
|
||||
int remainingHours = totalHours % 8;
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
if (days > 0) {
|
||||
sb.append(days).append("天");
|
||||
}
|
||||
if (remainingHours > 0 || days == 0) {
|
||||
sb.append(remainingHours).append("小时");
|
||||
}
|
||||
return sb.toString();
|
||||
String formatted = workHoursCalculator.formatWorkHours(hours);
|
||||
// WorkHoursCalculator 返回 "--" 表示无效,这里保持 "未设置" 的语义
|
||||
return "--".equals(formatted) ? "未设置" : formatted;
|
||||
}
|
||||
}
|
||||
|
||||
@ -60,6 +60,7 @@ public class TaskService {
|
||||
private final DeptMonthlyScoreService deptMonthlyScoreService;
|
||||
private final SystemConfigService systemConfigService;
|
||||
private final com.designhub.mapper.DepartmentMapper departmentMapper;
|
||||
private final com.designhub.util.WorkHoursCalculator workHoursCalculator;
|
||||
|
||||
/**
|
||||
* 创建任务
|
||||
@ -1501,15 +1502,12 @@ public class TaskService {
|
||||
}
|
||||
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
// 重新计算deadline:从接单时间开始计算,无论之前是否有deadline
|
||||
// 重新计算deadline:从接单时间开始计算,无论之前是否有deadline(使用工作时间计算)
|
||||
LocalDateTime deadline = null;
|
||||
if (task.getEstimatedHours() != null) {
|
||||
int hours = task.getEstimatedHours().intValue();
|
||||
if (hours > 0) {
|
||||
deadline = now.plusHours(hours);
|
||||
log.info("任务deadline已重新计算:从接单时间 {} 开始,预估 {} 小时,截止时间 {}",
|
||||
now, hours, deadline);
|
||||
}
|
||||
if (task.getEstimatedHours() != null && task.getEstimatedHours().compareTo(BigDecimal.ZERO) > 0) {
|
||||
deadline = workHoursCalculator.calculateDeadline(now, task.getEstimatedHours());
|
||||
log.info("任务deadline已重新计算:从接单时间 {} 开始,预估 {} 小时,截止时间 {}",
|
||||
now, task.getEstimatedHours(), deadline);
|
||||
}
|
||||
|
||||
int updated = taskMapper.grabTask(taskId, userId, deptId, now, deadline);
|
||||
@ -2599,19 +2597,20 @@ public class TaskService {
|
||||
BigDecimal deductedScore = BigDecimal.ZERO;
|
||||
|
||||
if (task.getStartTime() != null && task.getEstimatedHours() != null && task.getEstimatedHours().compareTo(BigDecimal.ZERO) > 0) {
|
||||
// 计算预计完成时间(接单时间 + 预估工时)
|
||||
long estimatedMinutes = task.getEstimatedHours().multiply(BigDecimal.valueOf(60)).longValue();
|
||||
LocalDateTime expectedCompleteTime = task.getStartTime().plusMinutes(estimatedMinutes);
|
||||
// 计算预计完成时间(接单时间 + 预估工时,使用工作时间计算)
|
||||
LocalDateTime expectedCompleteTime = workHoursCalculator.calculateDeadline(task.getStartTime(), task.getEstimatedHours());
|
||||
|
||||
// 判断是否延误
|
||||
if (completeTime.isAfter(expectedCompleteTime)) {
|
||||
if (expectedCompleteTime != null && completeTime.isAfter(expectedCompleteTime)) {
|
||||
isDelayed = true;
|
||||
task.setIsDelayed(1);
|
||||
|
||||
// 计算延误小时数
|
||||
long delayMinutes = java.time.Duration.between(expectedCompleteTime, completeTime).toMinutes();
|
||||
delayHours = BigDecimal.valueOf(delayMinutes).divide(BigDecimal.valueOf(60), 2, java.math.RoundingMode.HALF_UP);
|
||||
task.setDelayDays(delayHours.divide(BigDecimal.valueOf(24), 0, java.math.RoundingMode.CEILING).intValue());
|
||||
// 计算延误小时数(只计算工作时间段内的延误)
|
||||
delayHours = workHoursCalculator.calculateDelayHours(expectedCompleteTime, completeTime);
|
||||
// 计算延误天数(基于每天工作小时数)
|
||||
com.designhub.util.WorkHoursCalculator.WorkHoursConfig config = workHoursCalculator.getWorkHoursConfig();
|
||||
int workHoursPerDay = config.getWorkHoursPerDay();
|
||||
task.setDelayDays(delayHours.divide(BigDecimal.valueOf(workHoursPerDay), 0, java.math.RoundingMode.CEILING).intValue());
|
||||
|
||||
// 从系统配置获取延误每小时扣分数
|
||||
String penaltyPerHourStr = systemConfigService.getConfigValue("score.delay_penalty_per_hour");
|
||||
@ -2878,20 +2877,10 @@ public class TaskService {
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化工时(小时转天+小时,8小时=1天)
|
||||
* 格式化工时(小时转天+小时,基于工作时间配置)
|
||||
*/
|
||||
private String formatHours(BigDecimal hours) {
|
||||
if (hours == null) return "--";
|
||||
int totalHours = hours.intValue();
|
||||
int days = totalHours / 8;
|
||||
int remainingHours = totalHours % 8;
|
||||
if (days > 0 && remainingHours > 0) {
|
||||
return days + "天" + remainingHours + "小时";
|
||||
} else if (days > 0) {
|
||||
return days + "天";
|
||||
} else {
|
||||
return totalHours + "小时";
|
||||
}
|
||||
return workHoursCalculator.formatWorkHours(hours);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -0,0 +1,218 @@
|
||||
package com.designhub.util;
|
||||
|
||||
import com.designhub.service.SystemConfigService;
|
||||
import lombok.Data;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.time.Duration;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.LocalTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.format.DateTimeParseException;
|
||||
|
||||
/**
|
||||
* 工作时间计算工具类
|
||||
* 用于计算截止时间、延误时间等,只考虑工作时间段内的计算
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class WorkHoursCalculator {
|
||||
|
||||
private final SystemConfigService systemConfigService;
|
||||
|
||||
private static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern("HH:mm");
|
||||
|
||||
/**
|
||||
* 工作时间配置
|
||||
*/
|
||||
@Data
|
||||
public static class WorkHoursConfig {
|
||||
private LocalTime startTime; // 上班开始时间
|
||||
private LocalTime endTime; // 上班结束时间
|
||||
private int workHoursPerDay; // 每天工作小时数
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取工作时间配置
|
||||
*/
|
||||
public WorkHoursConfig getWorkHoursConfig() {
|
||||
WorkHoursConfig config = new WorkHoursConfig();
|
||||
|
||||
// 从系统配置获取开始时间(默认09:00)
|
||||
String startTimeStr = systemConfigService.getConfigValue("work_hours.start_time", "09:00");
|
||||
// 从系统配置获取结束时间(默认18:00)
|
||||
String endTimeStr = systemConfigService.getConfigValue("work_hours.end_time", "18:00");
|
||||
|
||||
try {
|
||||
config.setStartTime(LocalTime.parse(startTimeStr, TIME_FORMATTER));
|
||||
config.setEndTime(LocalTime.parse(endTimeStr, TIME_FORMATTER));
|
||||
} catch (DateTimeParseException e) {
|
||||
log.warn("工作时间配置格式错误,使用默认值: startTime={}, endTime={}", startTimeStr, endTimeStr);
|
||||
config.setStartTime(LocalTime.of(9, 0));
|
||||
config.setEndTime(LocalTime.of(18, 0));
|
||||
}
|
||||
|
||||
// 计算每天工作小时数
|
||||
if (config.getStartTime() != null && config.getEndTime() != null) {
|
||||
long minutes = Duration.between(config.getStartTime(), config.getEndTime()).toMinutes();
|
||||
config.setWorkHoursPerDay((int) (minutes / 60));
|
||||
} else {
|
||||
config.setWorkHoursPerDay(9); // 默认9小时
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算截止时间(只计算工作时间段)
|
||||
* @param startTime 开始时间(接单时间)
|
||||
* @param workHours 需要的工作小时数
|
||||
* @return 截止时间
|
||||
*/
|
||||
public LocalDateTime calculateDeadline(LocalDateTime startTime, BigDecimal workHours) {
|
||||
if (startTime == null || workHours == null || workHours.compareTo(BigDecimal.ZERO) <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
WorkHoursConfig config = getWorkHoursConfig();
|
||||
LocalTime workStart = config.getStartTime();
|
||||
LocalTime workEnd = config.getEndTime();
|
||||
|
||||
// 如果开始时间不在工作时间内,移动到下一个工作日的开始时间
|
||||
LocalDateTime current = startTime;
|
||||
LocalTime currentTime = current.toLocalTime();
|
||||
|
||||
// 如果当前时间在工作时间之前,移动到当天的开始时间
|
||||
if (currentTime.isBefore(workStart)) {
|
||||
current = current.toLocalDate().atTime(workStart);
|
||||
}
|
||||
// 如果当前时间在工作时间之后,移动到下一个工作日的开始时间
|
||||
else if (currentTime.isAfter(workEnd) || currentTime.equals(workEnd)) {
|
||||
current = current.toLocalDate().plusDays(1).atTime(workStart);
|
||||
}
|
||||
|
||||
// 计算剩余工时
|
||||
BigDecimal remainingHours = workHours;
|
||||
|
||||
// 循环累加工时,只在工作时间段内计算
|
||||
while (remainingHours.compareTo(BigDecimal.ZERO) > 0) {
|
||||
LocalTime currentWorkTime = current.toLocalTime();
|
||||
LocalDate currentDate = current.toLocalDate();
|
||||
|
||||
// 计算当天剩余的工作时间
|
||||
long remainingWorkMinutesToday = Duration.between(currentWorkTime, workEnd).toMinutes();
|
||||
BigDecimal remainingWorkHoursToday = BigDecimal.valueOf(remainingWorkMinutesToday).divide(BigDecimal.valueOf(60), 2, RoundingMode.HALF_UP);
|
||||
|
||||
if (remainingWorkHoursToday.compareTo(remainingHours) >= 0) {
|
||||
// 当天可以完成,直接加上剩余工时
|
||||
current = current.plusMinutes(remainingHours.multiply(BigDecimal.valueOf(60)).longValue());
|
||||
break;
|
||||
} else {
|
||||
// 当天不够,移动到下一个工作日的开始时间
|
||||
remainingHours = remainingHours.subtract(remainingWorkHoursToday);
|
||||
current = currentDate.plusDays(1).atTime(workStart);
|
||||
}
|
||||
}
|
||||
|
||||
return current;
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算延误时间(只计算工作时间段内的延误)
|
||||
* @param deadline 截止时间
|
||||
* @param actualTime 实际完成时间
|
||||
* @return 延误小时数(只计算工作时间段内)
|
||||
*/
|
||||
public BigDecimal calculateDelayHours(LocalDateTime deadline, LocalDateTime actualTime) {
|
||||
if (deadline == null || actualTime == null) {
|
||||
return BigDecimal.ZERO;
|
||||
}
|
||||
|
||||
// 如果实际时间早于或等于截止时间,未延误
|
||||
if (actualTime.isBefore(deadline) || actualTime.isEqual(deadline)) {
|
||||
return BigDecimal.ZERO;
|
||||
}
|
||||
|
||||
WorkHoursConfig config = getWorkHoursConfig();
|
||||
LocalTime workStart = config.getStartTime();
|
||||
LocalTime workEnd = config.getEndTime();
|
||||
|
||||
// 计算延误时间(只计算工作时间段内)
|
||||
BigDecimal delayHours = BigDecimal.ZERO;
|
||||
LocalDateTime current = deadline;
|
||||
int maxDays = 1000; // 防止无限循环
|
||||
int dayCount = 0;
|
||||
|
||||
while (current.isBefore(actualTime) && dayCount < maxDays) {
|
||||
LocalTime currentTime = current.toLocalTime();
|
||||
LocalDate currentDate = current.toLocalDate();
|
||||
|
||||
// 如果当前时间在工作时间内
|
||||
if (!currentTime.isBefore(workStart) && currentTime.isBefore(workEnd)) {
|
||||
// 计算到当天结束或实际时间的时间差(取较小值)
|
||||
LocalDateTime endOfDay = currentDate.atTime(workEnd);
|
||||
LocalDateTime nextPoint = actualTime.isBefore(endOfDay) ? actualTime : endOfDay;
|
||||
|
||||
long minutes = Duration.between(current, nextPoint).toMinutes();
|
||||
if (minutes > 0) {
|
||||
delayHours = delayHours.add(BigDecimal.valueOf(minutes).divide(BigDecimal.valueOf(60), 2, RoundingMode.HALF_UP));
|
||||
}
|
||||
|
||||
current = nextPoint;
|
||||
|
||||
// 如果已经到达实际时间,退出循环
|
||||
if (current.isAfter(actualTime) || current.isEqual(actualTime)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 移动到下一个工作日的开始时间
|
||||
if (currentTime.isAfter(workEnd) || currentTime.equals(workEnd) || currentTime.isBefore(workStart)) {
|
||||
current = currentDate.plusDays(1).atTime(workStart);
|
||||
dayCount++;
|
||||
} else {
|
||||
// 如果当前时间在工作时间内但还没到达实际时间,移动到当天结束
|
||||
current = currentDate.atTime(workEnd);
|
||||
}
|
||||
}
|
||||
|
||||
if (dayCount >= maxDays) {
|
||||
log.warn("计算延误时间超过{}天,可能存在异常: deadline={}, actualTime={}", maxDays, deadline, actualTime);
|
||||
}
|
||||
|
||||
return delayHours;
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化工时显示(X天X小时,基于工作时间段)
|
||||
* @param hours 小时数
|
||||
* @return 格式化后的字符串
|
||||
*/
|
||||
public String formatWorkHours(BigDecimal hours) {
|
||||
if (hours == null || hours.compareTo(BigDecimal.ZERO) <= 0) {
|
||||
return "--";
|
||||
}
|
||||
|
||||
WorkHoursConfig config = getWorkHoursConfig();
|
||||
int workHoursPerDay = config.getWorkHoursPerDay();
|
||||
|
||||
int totalHours = hours.intValue();
|
||||
int days = totalHours / workHoursPerDay;
|
||||
int remainingHours = totalHours % workHoursPerDay;
|
||||
|
||||
if (days > 0 && remainingHours > 0) {
|
||||
return days + "天" + remainingHours + "小时";
|
||||
} else if (days > 0) {
|
||||
return days + "天";
|
||||
} else {
|
||||
return totalHours + "小时";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,10 @@
|
||||
-- 添加工作时间配置
|
||||
-- V12: 工作时间配置(用于截止时间计算和延误扣分计算)
|
||||
|
||||
-- 添加上班时间配置
|
||||
INSERT INTO `sys_config` (`config_key`, `config_value`, `config_group`, `config_name`, `description`, `value_type`, `built_in`, `sort_order`, `create_time`, `update_time`, `deleted`)
|
||||
VALUES
|
||||
('work_hours.start_time', '09:00', 'system', '上班开始时间', '工作日上班开始时间,格式:HH:mm(24小时制),例如:09:00', 'string', 1, 20, NOW(), NOW(), 0),
|
||||
('work_hours.end_time', '18:00', 'system', '上班结束时间', '工作日上班结束时间,格式:HH:mm(24小时制),例如:18:00', 'string', 1, 21, NOW(), NOW(), 0)
|
||||
ON DUPLICATE KEY UPDATE `update_time` = NOW();
|
||||
|
||||
@ -143,3 +143,30 @@ export const getReceivingDepartments = () => {
|
||||
return request.get<any>('/designer-target-score/receiving-departments')
|
||||
}
|
||||
|
||||
// ==================== 工作时间配置 ====================
|
||||
|
||||
export interface WorkHoursConfig {
|
||||
startTime: string // 上班开始时间,格式:HH:mm
|
||||
endTime: string // 上班结束时间,格式:HH:mm
|
||||
workHoursPerDay: number // 每天工作小时数
|
||||
}
|
||||
|
||||
export interface WorkHoursConfigUpdate {
|
||||
startTime: string // 上班开始时间,格式:HH:mm
|
||||
endTime: string // 上班结束时间,格式:HH:mm
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取工作时间配置
|
||||
*/
|
||||
export const getWorkHoursConfig = () => {
|
||||
return request.get<WorkHoursConfig>('/system-config/work-hours')
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新工作时间配置
|
||||
*/
|
||||
export const updateWorkHoursConfig = (config: WorkHoursConfigUpdate) => {
|
||||
return request.put<boolean>('/system-config/work-hours', config)
|
||||
}
|
||||
|
||||
|
||||
@ -341,7 +341,10 @@ const getSubTaskStatusClass = (status: string) => {
|
||||
}
|
||||
|
||||
// 计算子任务完成截止日期(从接单开始计算+预计完成需要的时间)
|
||||
// 只有已接单或进行中的子任务才显示截止时间
|
||||
const getSubTaskDeadline = (subTask: TaskItem) => {
|
||||
// 只有进行中或已完成的子任务才显示截止时间
|
||||
if (subTask.status !== 'in_progress' && subTask.status !== 'completed') return null
|
||||
if (!subTask.createTime || !subTask.estimatedHours) return null
|
||||
// 从创建时间(接单时间)开始计算
|
||||
const startTime = dayjs(subTask.createTime)
|
||||
|
||||
@ -28,7 +28,7 @@
|
||||
</div>
|
||||
<div class="estimate-tip">
|
||||
<Icon name="info" size="xs" style="margin-right: 4px; opacity: 0.6;" />
|
||||
<span>按8小时/工作日计算,超过8小时按整天计算</span>
|
||||
<span>按{{ workHoursPerDay }}小时/工作日计算</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -547,7 +547,7 @@ import * as taskApi from '@/api/task'
|
||||
import * as configApi from '@/api/config'
|
||||
import * as uploadApi from '@/api/upload'
|
||||
import * as departmentApi from '@/api/department'
|
||||
import { getSystemConfigsMapByGroup } from '@/api/system'
|
||||
import { getSystemConfigsMapByGroup, getWorkHoursConfig } from '@/api/system'
|
||||
import { uploadFilesToS3 } from '@/api/file'
|
||||
import type { FormInstance } from 'ant-design-vue'
|
||||
import type { DemandType, Brand, Platform, Category, TaskSubType } from '@/api/config'
|
||||
@ -754,6 +754,9 @@ const brands = computed(() => configStore.brands)
|
||||
const platforms = computed(() => configStore.platforms)
|
||||
const categories = computed(() => configStore.categories)
|
||||
|
||||
// 工作时间配置(用于时效格式化)
|
||||
const workHoursPerDay = ref(9) // 默认9小时
|
||||
|
||||
// 层级任务类型数据
|
||||
const hierarchicalTaskTypes = ref<any[]>([])
|
||||
|
||||
@ -1003,29 +1006,19 @@ const formattedEstimatedTime = computed(() => {
|
||||
}
|
||||
})
|
||||
|
||||
// 计算预计完成时间(考虑8小时工作制)
|
||||
// 计算预计完成时间(基于工作时间配置)
|
||||
const estimatedDeadline = computed(() => {
|
||||
const totalHours = estimatedTotalHours.value
|
||||
if (totalHours === 0) return '--'
|
||||
|
||||
const now = new Date()
|
||||
const WORK_HOURS_PER_DAY = 8 // 每天工作8小时
|
||||
const workHours = workHoursPerDay.value || 9 // 默认9小时
|
||||
|
||||
// 计算需要多少个工作日
|
||||
let workDays = Math.floor(totalHours / WORK_HOURS_PER_DAY)
|
||||
let remainingHours = totalHours % WORK_HOURS_PER_DAY
|
||||
|
||||
// 如果超过8小时,按24小时(1天)计算
|
||||
if (totalHours > WORK_HOURS_PER_DAY) {
|
||||
// 超过8小时的部分,每8小时算1个工作日,但日历上是24小时
|
||||
const totalDays = Math.ceil(totalHours / WORK_HOURS_PER_DAY)
|
||||
const deadline = new Date(now.getTime() + totalDays * 24 * 60 * 60 * 1000)
|
||||
return formatDeadline(deadline)
|
||||
} else {
|
||||
// 8小时内,当天完成
|
||||
const deadline = new Date(now.getTime() + totalHours * 60 * 60 * 1000)
|
||||
return formatDeadline(deadline)
|
||||
}
|
||||
const totalDays = Math.ceil(totalHours / workHours)
|
||||
// 按自然日计算(每工作日对应1个自然日)
|
||||
const deadline = new Date(now.getTime() + totalDays * 24 * 60 * 60 * 1000)
|
||||
return formatDeadline(deadline)
|
||||
})
|
||||
|
||||
// 格式化截止时间
|
||||
@ -1206,14 +1199,14 @@ const totalPipelineHours = computed(() => {
|
||||
return pipelineGroups.value.reduce((sum, group) => sum + group.totalHours, 0).toFixed(1)
|
||||
})
|
||||
|
||||
// 格式化工时显示(8小时=1个工作日)
|
||||
// 格式化工时显示(基于工作时间配置)
|
||||
const formatWorkHours = (hours: number): string => {
|
||||
if (hours === 0) return '0小时'
|
||||
|
||||
const WORK_HOURS_PER_DAY = 8
|
||||
const days = Math.floor(hours / WORK_HOURS_PER_DAY)
|
||||
const workHours = workHoursPerDay.value || 9 // 默认9小时
|
||||
const days = Math.floor(hours / workHours)
|
||||
// 处理浮点数精度问题,保留两位小数
|
||||
let remainingHours = Math.round((hours % WORK_HOURS_PER_DAY) * 100) / 100
|
||||
let remainingHours = Math.round((hours % workHours) * 100) / 100
|
||||
|
||||
// 格式化小时显示
|
||||
const formatHours = (h: number): string => {
|
||||
@ -1756,6 +1749,16 @@ const loadReleaseTypeConfig = async () => {
|
||||
}
|
||||
}
|
||||
}
|
||||
// 加载工作时间配置
|
||||
try {
|
||||
const workHoursResponse = await getWorkHoursConfig()
|
||||
if (workHoursResponse.data && workHoursResponse.data.workHoursPerDay) {
|
||||
workHoursPerDay.value = workHoursResponse.data.workHoursPerDay
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载工作时间配置失败:', error)
|
||||
// 使用默认值
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载发布类型配置失败:', error)
|
||||
}
|
||||
|
||||
@ -74,6 +74,44 @@
|
||||
</div>
|
||||
</a-form-item>
|
||||
|
||||
<a-divider orientation="left">工作时间设置</a-divider>
|
||||
<a-form-item label="上班开始时间">
|
||||
<a-time-picker
|
||||
v-model:value="workHoursSettings.startTime"
|
||||
format="HH:mm"
|
||||
value-format="HH:mm"
|
||||
placeholder="请选择上班开始时间"
|
||||
size="large"
|
||||
style="width: 100%;"
|
||||
/>
|
||||
<div style="margin-top: 8px; font-size: 12px; color: #888;">
|
||||
<Icon name="info" size="sm" color="#888" style="margin-right: 4px;" />
|
||||
用于计算任务截止时间和延误扣分
|
||||
</div>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="上班结束时间">
|
||||
<a-time-picker
|
||||
v-model:value="workHoursSettings.endTime"
|
||||
format="HH:mm"
|
||||
value-format="HH:mm"
|
||||
placeholder="请选择上班结束时间"
|
||||
size="large"
|
||||
style="width: 100%;"
|
||||
/>
|
||||
<div style="margin-top: 8px; font-size: 12px; color: #888;">
|
||||
<Icon name="info" size="sm" color="#888" style="margin-right: 4px;" />
|
||||
用于计算任务截止时间和延误扣分
|
||||
</div>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item v-if="workHoursSettings.workHoursPerDay > 0">
|
||||
<div style="padding: 8px 12px; background: #f5f5f5; border-radius: 4px;">
|
||||
<span style="font-size: 14px; color: #666;">每日工作小时数:</span>
|
||||
<span style="font-size: 14px; font-weight: 500; color: #333;">{{ workHoursSettings.workHoursPerDay }} 小时</span>
|
||||
</div>
|
||||
</a-form-item>
|
||||
|
||||
<a-divider orientation="left">功能开关</a-divider>
|
||||
<a-form-item label="注册功能">
|
||||
<a-switch
|
||||
@ -2367,8 +2405,11 @@ import {
|
||||
saveDesignerTargetScore,
|
||||
deleteDesignerTargetScore,
|
||||
getReceivingDepartments,
|
||||
getWorkHoursConfig,
|
||||
updateWorkHoursConfig,
|
||||
type SystemConfig,
|
||||
type DesignerTargetScore
|
||||
type DesignerTargetScore,
|
||||
type WorkHoursConfig
|
||||
} from '@/api/system'
|
||||
import {
|
||||
getAllUsers,
|
||||
@ -2439,6 +2480,16 @@ const siteSettings = reactive({
|
||||
registerEnabledConfigId: null as number | null
|
||||
})
|
||||
|
||||
// 工作时间设置
|
||||
const workHoursSettings = reactive({
|
||||
startTime: '09:00' as string | null,
|
||||
endTime: '18:00' as string | null,
|
||||
workHoursPerDay: 0
|
||||
})
|
||||
|
||||
const savingWorkHours = ref(false)
|
||||
const loadingWorkHours = ref(false)
|
||||
|
||||
const loading = reactive({
|
||||
demandTypes: false,
|
||||
taskSubTypes: false,
|
||||
@ -4733,10 +4784,71 @@ const updateSiteInfo = () => {
|
||||
siteSettingsStore.updateSiteSettings(siteSettings.siteName, siteSettings.logoUrl)
|
||||
}
|
||||
|
||||
// 加载工作时间配置
|
||||
const loadWorkHours = async () => {
|
||||
loadingWorkHours.value = true
|
||||
try {
|
||||
const response = await getWorkHoursConfig()
|
||||
if (response.data) {
|
||||
workHoursSettings.startTime = response.data.startTime
|
||||
workHoursSettings.endTime = response.data.endTime
|
||||
workHoursSettings.workHoursPerDay = response.data.workHoursPerDay
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载工作时间配置失败:', error)
|
||||
// 使用默认值
|
||||
workHoursSettings.startTime = '09:00'
|
||||
workHoursSettings.endTime = '18:00'
|
||||
workHoursSettings.workHoursPerDay = 9
|
||||
} finally {
|
||||
loadingWorkHours.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 保存工作时间配置
|
||||
const handleSaveWorkHours = async () => {
|
||||
if (!workHoursSettings.startTime || !workHoursSettings.endTime) {
|
||||
message.warning('请设置上班开始时间和结束时间')
|
||||
return
|
||||
}
|
||||
|
||||
// 验证时间格式和逻辑
|
||||
const startTime = dayjs(workHoursSettings.startTime, 'HH:mm')
|
||||
const endTime = dayjs(workHoursSettings.endTime, 'HH:mm')
|
||||
|
||||
if (!startTime.isValid() || !endTime.isValid()) {
|
||||
message.error('时间格式错误')
|
||||
return
|
||||
}
|
||||
|
||||
if (!endTime.isAfter(startTime)) {
|
||||
message.error('上班结束时间必须晚于开始时间')
|
||||
return
|
||||
}
|
||||
|
||||
savingWorkHours.value = true
|
||||
try {
|
||||
await updateWorkHoursConfig({
|
||||
startTime: workHoursSettings.startTime,
|
||||
endTime: workHoursSettings.endTime
|
||||
})
|
||||
// 重新加载配置以获取计算后的 workHoursPerDay
|
||||
await loadWorkHours()
|
||||
message.success('工作时间配置保存成功')
|
||||
} catch (error: any) {
|
||||
console.error('保存工作时间配置失败:', error)
|
||||
message.error(error.response?.data?.message || '保存失败')
|
||||
} finally {
|
||||
savingWorkHours.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadAllData()
|
||||
// 加载网站设置
|
||||
loadSiteSettings()
|
||||
// 加载工作时间配置
|
||||
loadWorkHours()
|
||||
// 加载钉钉配置
|
||||
loadDingTalkConfig()
|
||||
// 加载任务发布类型配置
|
||||
|
||||
@ -2435,6 +2435,7 @@ import * as taskApi from '@/api/task'
|
||||
import * as userApi from '@/api/user'
|
||||
import * as configApi from '@/api/config'
|
||||
import * as authApi from '@/api/auth'
|
||||
import { getWorkHoursConfig } from '@/api/system'
|
||||
import type { Task, TaskApprovalDTO, TaskProduct } from '@/api/task'
|
||||
import type { DesignerWithWorkload } from '@/api/user'
|
||||
import { useConfigStore } from '@/stores/config'
|
||||
@ -2657,6 +2658,9 @@ const brands = computed(() => configStore.brands)
|
||||
const platforms = computed(() => configStore.platforms)
|
||||
const categories = computed(() => configStore.categories)
|
||||
|
||||
// 工作时间配置(用于时效格式化)
|
||||
const workHoursPerDay = ref(9) // 默认9小时
|
||||
|
||||
// 层级任务类型数据(用于级联选择器)
|
||||
const hierarchicalTaskTypes = ref<any[]>([])
|
||||
|
||||
@ -2863,12 +2867,12 @@ const formatDeadline = (deadline: string | undefined) => {
|
||||
}
|
||||
}
|
||||
|
||||
// 格式化时效要求(工时转换为天+小时,8小时=1天)
|
||||
// 格式化时效要求(工时转换为天+小时,基于工作时间配置)
|
||||
const formatEstimatedTime = (hours: number | undefined) => {
|
||||
if (!hours) return '--'
|
||||
const WORK_HOURS_PER_DAY = 8
|
||||
const days = Math.floor(hours / WORK_HOURS_PER_DAY)
|
||||
const remainingHours = Math.round((hours % WORK_HOURS_PER_DAY) * 100) / 100
|
||||
const workHours = workHoursPerDay.value || 9 // 默认9小时
|
||||
const days = Math.floor(hours / workHours)
|
||||
const remainingHours = Math.round((hours % workHours) * 100) / 100
|
||||
|
||||
if (days === 0) {
|
||||
return Number.isInteger(remainingHours) ? `${remainingHours}小时` : `${remainingHours.toFixed(1)}小时`
|
||||
@ -2910,13 +2914,16 @@ const getSubTaskStatusText = (status: string | undefined) => {
|
||||
}
|
||||
|
||||
// 计算子任务完成截止日期(从接单开始计算+预计完成需要的时间)
|
||||
// 只有已接单或进行中的子任务才显示截止时间
|
||||
const getSubTaskDeadline = (subTask: any) => {
|
||||
// 只有进行中或已完成的子任务才显示截止时间
|
||||
if (subTask.status !== 'in_progress' && subTask.status !== 'completed') return null
|
||||
if (!subTask.createTime || !subTask.estimatedHours) return null
|
||||
// 从创建时间(接单时间)开始计算
|
||||
const startTime = dayjs(subTask.createTime)
|
||||
// 计算截止时间:创建时间 + 预计工时
|
||||
const deadline = startTime.add(subTask.estimatedHours, 'hour')
|
||||
// 格式化为"XX月XX日XX时"
|
||||
// 格式化为"XX月XX日HH时"
|
||||
return deadline.format('MM月DD日HH时')
|
||||
}
|
||||
|
||||
|
||||
331
工作时间系统实施计划.md
Normal file
331
工作时间系统实施计划.md
Normal file
@ -0,0 +1,331 @@
|
||||
# 工作时间系统实施计划
|
||||
|
||||
## 一、需求概述
|
||||
|
||||
### 1.1 核心需求
|
||||
1. **工作时间配置**:在系统设置中增加上班时间设置(开始时间、结束时间)
|
||||
2. **动态计算**:一天的工作时间根据设置的上班时间段作为自然日(替换硬编码的8小时)
|
||||
3. **截止时间计算**:计算截止时间时只计算到上班时间段内
|
||||
4. **延误扣分限制**:延误扣分时间也需要限制在上班时间段内才扣分,超过上班时间不扣分
|
||||
|
||||
### 1.2 涉及场景
|
||||
- 截止时间计算(任务创建、子任务分配时)
|
||||
- 延误扣分计算(任务完成时、进行中任务检查时)
|
||||
- 时效格式化显示(前端显示、钉钉通知)
|
||||
- 子任务截止日期显示(前端任务卡片)
|
||||
|
||||
---
|
||||
|
||||
## 二、数据库设计
|
||||
|
||||
### 2.1 使用系统配置表(sys_config)
|
||||
**方案选择**:使用现有的 `sys_config` 表存储工作时间配置(参考 `site_url` 配置的实现方式)
|
||||
|
||||
**配置项设计**:
|
||||
- `config_key`: `work_hours.start_time`(上班开始时间,格式:HH:mm,如 "09:00")
|
||||
- `config_key`: `work_hours.end_time`(上班结束时间,格式:HH:mm,如 "18:00")
|
||||
- `config_group`: `system`
|
||||
- `value_type`: `string`
|
||||
- `built_in`: `1`(系统内置,不可删除)
|
||||
|
||||
**默认值**:
|
||||
- `work_hours.start_time`: `"09:00"`
|
||||
- `work_hours.end_time`: `"18:00"`
|
||||
- 工作时间长度:9小时(9:00-18:00,不含午休)
|
||||
|
||||
### 2.2 数据库迁移文件
|
||||
创建 `V12__add_work_hours_config.sql`:
|
||||
```sql
|
||||
-- 添加上班时间配置
|
||||
INSERT INTO `sys_config` (`config_key`, `config_value`, `config_group`, `config_name`, `description`, `value_type`, `built_in`, `sort_order`, `create_time`, `update_time`, `deleted`)
|
||||
VALUES
|
||||
('work_hours.start_time', '09:00', 'system', '上班开始时间', '工作日上班开始时间,格式:HH:mm(24小时制),例如:09:00', 'string', 1, 20, NOW(), NOW(), 0),
|
||||
('work_hours.end_time', '18:00', 'system', '上班结束时间', '工作日上班结束时间,格式:HH:mm(24小时制),例如:18:00', 'string', 1, 21, NOW(), NOW(), 0)
|
||||
ON DUPLICATE KEY UPDATE `update_time` = NOW();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 三、后端实现
|
||||
|
||||
### 3.1 工作时间计算工具类
|
||||
**文件路径**:`backend/src/main/java/com/designhub/util/WorkHoursCalculator.java`
|
||||
|
||||
**核心方法**:
|
||||
1. `getWorkHoursConfig()`:从系统配置获取工作时间(开始时间、结束时间、工作时长)
|
||||
2. `calculateDeadline(LocalDateTime startTime, BigDecimal workHours)`:计算截止时间(只计算工作时间段)
|
||||
3. `calculateDelayHours(LocalDateTime deadline, LocalDateTime actualTime)`:计算延误时间(只计算工作时间段内的延误)
|
||||
4. `formatWorkHours(BigDecimal hours)`:格式化工时显示(X天X小时,基于工作时间段)
|
||||
|
||||
**计算逻辑**:
|
||||
- **截止时间计算**:
|
||||
- 从开始时间(接单时间)开始,累加工时
|
||||
- 只在工作时间段内累加(跳过非工作时间)
|
||||
- 如果当前时间不在工作时间内,先移动到下一个工作日的开始时间
|
||||
- 如果工时跨越多天,跳过周末(可选,根据需求)
|
||||
|
||||
- **延误时间计算**:
|
||||
- 从截止时间到实际完成时间
|
||||
- 只统计工作时间段内的小时数
|
||||
- 跳过非工作时间(下班后、周末)
|
||||
|
||||
### 3.2 系统配置 Service 扩展
|
||||
**文件路径**:`backend/src/main/java/com/designhub/service/SystemConfigService.java`
|
||||
|
||||
**新增方法**:
|
||||
- `getWorkHoursConfig()`:获取工作时间配置(返回 WorkHoursConfig 对象)
|
||||
|
||||
**新增内部类**:
|
||||
```java
|
||||
public static class WorkHoursConfig {
|
||||
private LocalTime startTime; // 上班开始时间
|
||||
private LocalTime endTime; // 上班结束时间
|
||||
private int workHoursPerDay; // 每天工作小时数
|
||||
}
|
||||
```
|
||||
|
||||
### 3.3 需要修改的后端文件清单
|
||||
|
||||
#### 3.3.1 时效格式化(替换硬编码的8小时)
|
||||
- `TaskService.java`:`formatHours()` 方法
|
||||
- `TaskItemService.java`:`formatEstimatedHours()` 方法
|
||||
- `TaskApprovalService.java`:`formatEstimatedHours()` 方法
|
||||
|
||||
**修改方式**:
|
||||
- 从 `WorkHoursCalculator.formatWorkHours()` 获取格式化结果
|
||||
- 使用动态的工作时长(从配置读取)替换硬编码的8小时
|
||||
|
||||
#### 3.3.2 截止时间计算
|
||||
- `TaskService.java`:任务创建时的截止时间计算
|
||||
- `TaskItemService.java`:子任务创建时的截止时间计算
|
||||
- `TaskApprovalService.java`:任务审批通过后的截止时间计算
|
||||
|
||||
**修改方式**:
|
||||
- 使用 `WorkHoursCalculator.calculateDeadline()` 计算截止时间
|
||||
|
||||
#### 3.3.3 延误扣分计算
|
||||
- `AnalyticsService.java`:
|
||||
- `getDesignerPerformance()`:设计师绩效统计中的延误扣分
|
||||
- `getDesignerDeductionDetails()`:设计师扣分明细中的延误扣分
|
||||
- `DelayPenaltyService.java`:`calculateDelayPenalty()` 方法(如果有使用)
|
||||
- `TaskService.java`:任务完成时的延误扣分计算(`completeTask()` 方法)
|
||||
|
||||
**修改方式**:
|
||||
- 使用 `WorkHoursCalculator.calculateDelayHours()` 计算延误时间
|
||||
- 只计算工作时间段内的延误
|
||||
|
||||
---
|
||||
|
||||
## 四、前端实现
|
||||
|
||||
### 4.1 工作时间配置 UI
|
||||
**文件路径**:`frontend/src/views/Settings.vue`
|
||||
|
||||
**新增标签页**:在"网站设置"标签页中添加"工作时间设置"区域
|
||||
|
||||
**UI 组件**:
|
||||
- 开始时间选择器(TimePicker,格式:HH:mm)
|
||||
- 结束时间选择器(TimePicker,格式:HH:mm)
|
||||
- 工作时间长度显示(自动计算,只读)
|
||||
- 保存按钮
|
||||
|
||||
**配置项**:
|
||||
- `work_hours.start_time`:上班开始时间
|
||||
- `work_hours.end_time`:上班结束时间
|
||||
|
||||
### 4.2 需要修改的前端文件清单
|
||||
|
||||
#### 4.2.1 截止时间计算
|
||||
- `frontend/src/components/TaskCard.vue`:`getSubTaskDeadline()` 方法
|
||||
- `frontend/src/views/TaskHall.vue`:`getSubTaskDeadline()` 方法
|
||||
- `frontend/src/views/CreateTask.vue`:`estimatedDeadline` 计算属性
|
||||
|
||||
**修改方式**:
|
||||
- 从系统配置获取工作时间
|
||||
- 使用工作时间计算截止时间(调用后端API或前端计算)
|
||||
|
||||
#### 4.2.2 时效格式化显示
|
||||
- `frontend/src/views/TaskHall.vue`:`formatEstimatedTime()` 函数
|
||||
- `frontend/src/views/CreateTask.vue`:`formatWorkHours()` 函数
|
||||
|
||||
**修改方式**:
|
||||
- 从系统配置获取工作时长
|
||||
- 使用动态工作时长替换硬编码的8小时
|
||||
|
||||
---
|
||||
|
||||
## 五、API 设计
|
||||
|
||||
### 5.1 获取工作时间配置
|
||||
**接口**:`GET /api/system-config/work-hours`
|
||||
|
||||
**响应**:
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"data": {
|
||||
"startTime": "09:00",
|
||||
"endTime": "18:00",
|
||||
"workHoursPerDay": 9
|
||||
},
|
||||
"message": "操作成功"
|
||||
}
|
||||
```
|
||||
|
||||
### 5.2 更新工作时间配置
|
||||
**接口**:`PUT /api/system-config/work-hours`
|
||||
|
||||
**请求体**:
|
||||
```json
|
||||
{
|
||||
"startTime": "09:00",
|
||||
"endTime": "18:00"
|
||||
}
|
||||
```
|
||||
|
||||
**响应**:
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"data": true,
|
||||
"message": "操作成功"
|
||||
}
|
||||
```
|
||||
|
||||
### 5.3 计算截止时间(可选,如果需要前端计算)
|
||||
**接口**:`POST /api/system-config/calculate-deadline`
|
||||
|
||||
**请求体**:
|
||||
```json
|
||||
{
|
||||
"startTime": "2026-01-14T09:00:00",
|
||||
"workHours": 16
|
||||
}
|
||||
```
|
||||
|
||||
**响应**:
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"data": "2026-01-16T10:00:00",
|
||||
"message": "操作成功"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 六、实施步骤
|
||||
|
||||
### 阶段一:数据库和基础工具类
|
||||
1. ✅ 创建数据库迁移文件 `V12__add_work_hours_config.sql`
|
||||
2. ✅ 创建 `WorkHoursCalculator` 工具类
|
||||
3. ✅ 在 `SystemConfigService` 中添加 `getWorkHoursConfig()` 方法
|
||||
4. ✅ 创建工作时间配置 API(`SystemConfigController` 或新 Controller)
|
||||
|
||||
### 阶段二:后端核心逻辑修改
|
||||
5. ✅ 修改 `TaskService.formatHours()` 使用动态工作时长
|
||||
6. ✅ 修改 `TaskItemService.formatEstimatedHours()` 使用动态工作时长
|
||||
7. ✅ 修改 `TaskApprovalService.formatEstimatedHours()` 使用动态工作时长
|
||||
8. ✅ 修改任务创建时的截止时间计算逻辑
|
||||
9. ✅ 修改子任务创建时的截止时间计算逻辑
|
||||
10. ✅ 修改 `AnalyticsService` 中的延误扣分计算
|
||||
11. ✅ 修改 `TaskService.completeTask()` 中的延误扣分计算
|
||||
|
||||
### 阶段三:前端实现
|
||||
12. ✅ 在 `Settings.vue` 中添加工作时间配置 UI
|
||||
13. ✅ 创建前端 API 调用(`api/system.ts`)
|
||||
14. ✅ 修改 `TaskCard.vue` 中的截止时间计算
|
||||
15. ✅ 修改 `TaskHall.vue` 中的截止时间计算
|
||||
16. ✅ 修改 `CreateTask.vue` 中的截止时间计算
|
||||
17. ✅ 修改前端时效格式化函数
|
||||
|
||||
### 阶段四:测试和优化
|
||||
18. ✅ 测试工作时间配置的保存和读取
|
||||
19. ✅ 测试截止时间计算(跨天、跨周末等场景)
|
||||
20. ✅ 测试延误扣分计算(工作时间段内、外)
|
||||
21. ✅ 测试时效格式化显示
|
||||
22. ✅ 测试钉钉通知中的时效显示
|
||||
23. ✅ 修复发现的 bug
|
||||
|
||||
---
|
||||
|
||||
## 七、技术难点和注意事项
|
||||
|
||||
### 7.1 工作时间计算复杂性
|
||||
- **跨天计算**:工时可能跨越多天,需要跳过非工作时间
|
||||
- **跨周末**:是否需要跳过周末?建议第一版先不考虑,只处理工作日的工作时间段
|
||||
- **时区问题**:确保使用系统时区
|
||||
|
||||
### 7.2 性能考虑
|
||||
- 工作时间配置需要频繁读取,建议缓存(可以使用 Spring Cache)
|
||||
- 截止时间计算可能涉及循环,需要优化算法
|
||||
|
||||
### 7.3 向后兼容
|
||||
- 如果配置不存在,使用默认值(9:00-18:00,9小时)
|
||||
- 确保旧数据不会因为配置缺失而出错
|
||||
|
||||
### 7.4 数据迁移
|
||||
- 现有任务的截止时间是否需要重新计算?
|
||||
- **建议**:不重新计算,只影响新创建的任务和新的延误扣分
|
||||
- 现有延误扣分记录是否需要重新计算?
|
||||
- **建议**:不重新计算,只影响新产生的延误扣分
|
||||
|
||||
---
|
||||
|
||||
## 八、测试用例
|
||||
|
||||
### 8.1 工作时间配置
|
||||
- 测试保存和读取工作时间配置
|
||||
- 测试配置格式验证(HH:mm 格式)
|
||||
- 测试开始时间不能晚于结束时间
|
||||
- 测试配置不存在时使用默认值
|
||||
|
||||
### 8.2 截止时间计算
|
||||
- 测试同一工作日内完成的任务
|
||||
- 测试跨天的任务(例如:16小时工时,从上午开始)
|
||||
- 测试从非工作时间开始的任务(例如:晚上接单,从第二天早上开始计算)
|
||||
- 测试跨周末的任务(如果实现)
|
||||
|
||||
### 8.3 延误扣分计算
|
||||
- 测试在工作时间内延误的扣分
|
||||
- 测试跨越下班时间的延误(只计算工作时间内的延误)
|
||||
- 测试跨越周末的延误(只计算工作日的延误)
|
||||
- 测试提前完成的任务(不扣分)
|
||||
|
||||
### 8.4 时效格式化
|
||||
- 测试小时数少于1天的情况
|
||||
- 测试小时数等于1天的情况
|
||||
- 测试小时数超过1天的情况
|
||||
- 测试小数点小时数(如 1.5 小时)
|
||||
|
||||
---
|
||||
|
||||
## 九、预计工作量
|
||||
|
||||
- **数据库设计**:0.5 天
|
||||
- **后端工具类开发**:2 天
|
||||
- **后端逻辑修改**:3 天
|
||||
- **前端 UI 开发**:1 天
|
||||
- **前端逻辑修改**:1.5 天
|
||||
- **测试和修复**:2 天
|
||||
|
||||
**总计**:约 10 个工作日
|
||||
|
||||
---
|
||||
|
||||
## 十、风险点
|
||||
|
||||
1. **计算逻辑复杂性**:工作时间计算涉及复杂的日期时间处理,容易出现边界情况
|
||||
2. **性能问题**:如果计算逻辑复杂,可能影响性能
|
||||
3. **向后兼容性**:确保现有功能不受影响
|
||||
4. **数据一致性**:确保前后端计算逻辑一致
|
||||
|
||||
---
|
||||
|
||||
## 十一、后续优化建议
|
||||
|
||||
1. **支持多时间段**:支持上午、下午两个时间段(考虑午休)
|
||||
2. **支持周末配置**:允许配置周末是否工作
|
||||
3. **支持节假日配置**:支持配置节假日,跳过计算
|
||||
4. **历史数据重算**:提供工具重新计算历史任务的截止时间和延误扣分
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user