diff --git a/backend/src/main/java/com/designhub/controller/DeptMonthlyScoreController.java b/backend/src/main/java/com/designhub/controller/DeptMonthlyScoreController.java new file mode 100644 index 0000000..74b2b8f --- /dev/null +++ b/backend/src/main/java/com/designhub/controller/DeptMonthlyScoreController.java @@ -0,0 +1,67 @@ +package com.designhub.controller; + +import com.designhub.common.Result; +import com.designhub.entity.DeptMonthlyScore; +import com.designhub.service.DeptMonthlyScoreService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; + +import java.time.LocalDateTime; +import java.util.List; + +/** + * 部门月度积分控制器 + */ +@Tag(name = "部门月度积分") +@RestController +@RequestMapping("/api/dept-monthly-score") +@RequiredArgsConstructor +public class DeptMonthlyScoreController { + + private final DeptMonthlyScoreService deptMonthlyScoreService; + + @Operation(summary = "获取部门当月积分") + @GetMapping("/current/{deptId}") + public Result getCurrentMonthScore(@PathVariable Long deptId) { + DeptMonthlyScore score = deptMonthlyScoreService.getCurrentMonthScore(deptId); + return Result.success(score); + } + + @Operation(summary = "获取部门某年某月积分") + @GetMapping("/{deptId}/{year}/{month}") + public Result getMonthScore( + @PathVariable Long deptId, + @PathVariable Integer year, + @PathVariable Integer month) { + DeptMonthlyScore score = deptMonthlyScoreService.getMonthScore(deptId, year, month); + return Result.success(score); + } + + @Operation(summary = "获取部门某年的所有月度积分") + @GetMapping("/yearly/{deptId}/{year}") + public Result> getYearlyScores( + @PathVariable Long deptId, + @PathVariable Integer year) { + List scores = deptMonthlyScoreService.getYearlyScores(deptId, year); + return Result.success(scores); + } + + @Operation(summary = "获取某月所有部门的积分排名") + @GetMapping("/ranking/{year}/{month}") + public Result> getMonthlyRanking( + @PathVariable Integer year, + @PathVariable Integer month) { + List ranking = deptMonthlyScoreService.getMonthlyRanking(year, month); + return Result.success(ranking); + } + + @Operation(summary = "获取当月所有部门的积分排名") + @GetMapping("/ranking/current") + public Result> getCurrentMonthRanking() { + List ranking = deptMonthlyScoreService.getCurrentMonthRanking(); + return Result.success(ranking); + } +} + diff --git a/backend/src/main/java/com/designhub/entity/DeptMonthlyScore.java b/backend/src/main/java/com/designhub/entity/DeptMonthlyScore.java new file mode 100644 index 0000000..b1c1fd0 --- /dev/null +++ b/backend/src/main/java/com/designhub/entity/DeptMonthlyScore.java @@ -0,0 +1,87 @@ +package com.designhub.entity; + +import com.baomidou.mybatisplus.annotation.*; +import lombok.Data; +import java.io.Serializable; +import java.math.BigDecimal; +import java.time.LocalDateTime; + +/** + * 部门月度积分统计实体 + * 记录接单部门每月获得的积分 + */ +@Data +@TableName("dept_monthly_score") +public class DeptMonthlyScore implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * 主键ID + */ + @TableId(value = "id", type = IdType.AUTO) + private Long id; + + /** + * 部门ID + */ + private Long deptId; + + /** + * 部门名称(冗余字段) + */ + private String deptName; + + /** + * 年份 + */ + private Integer year; + + /** + * 月份(1-12) + */ + private Integer month; + + /** + * 本月获得积分 + */ + private BigDecimal earnedScore; + + /** + * 本月扣除积分(延误等) + */ + private BigDecimal deductedScore; + + /** + * 本月净积分(earned - deducted) + */ + private BigDecimal netScore; + + /** + * 本月完成任务数 + */ + private Integer completedTaskCount; + + /** + * 本月延误任务数 + */ + private Integer delayedTaskCount; + + /** + * 本月延误总小时数 + */ + private BigDecimal totalDelayHours; + + /** + * 创建时间 + */ + @TableField(fill = FieldFill.INSERT) + private LocalDateTime createTime; + + /** + * 更新时间 + */ + @TableField(fill = FieldFill.INSERT_UPDATE) + private LocalDateTime updateTime; +} + diff --git a/backend/src/main/java/com/designhub/mapper/DeptMonthlyScoreMapper.java b/backend/src/main/java/com/designhub/mapper/DeptMonthlyScoreMapper.java new file mode 100644 index 0000000..b799644 --- /dev/null +++ b/backend/src/main/java/com/designhub/mapper/DeptMonthlyScoreMapper.java @@ -0,0 +1,35 @@ +package com.designhub.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.designhub.entity.DeptMonthlyScore; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; + +import java.util.List; + +/** + * 部门月度积分Mapper接口 + */ +@Mapper +public interface DeptMonthlyScoreMapper extends BaseMapper { + + /** + * 获取部门某年某月的积分记录 + */ + @Select("SELECT * FROM dept_monthly_score WHERE dept_id = #{deptId} AND year = #{year} AND month = #{month}") + DeptMonthlyScore getByDeptAndMonth(@Param("deptId") Long deptId, @Param("year") Integer year, @Param("month") Integer month); + + /** + * 获取部门某年的所有月度积分 + */ + @Select("SELECT * FROM dept_monthly_score WHERE dept_id = #{deptId} AND year = #{year} ORDER BY month") + List getByDeptAndYear(@Param("deptId") Long deptId, @Param("year") Integer year); + + /** + * 获取某年某月所有部门的积分排名 + */ + @Select("SELECT * FROM dept_monthly_score WHERE year = #{year} AND month = #{month} ORDER BY net_score DESC") + List getRankingByMonth(@Param("year") Integer year, @Param("month") Integer month); +} + diff --git a/backend/src/main/java/com/designhub/service/DeptMonthlyScoreService.java b/backend/src/main/java/com/designhub/service/DeptMonthlyScoreService.java new file mode 100644 index 0000000..5ac3647 --- /dev/null +++ b/backend/src/main/java/com/designhub/service/DeptMonthlyScoreService.java @@ -0,0 +1,142 @@ +package com.designhub.service; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.designhub.entity.Department; +import com.designhub.entity.DeptMonthlyScore; +import com.designhub.mapper.DepartmentMapper; +import com.designhub.mapper.DeptMonthlyScoreMapper; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.List; + +/** + * 部门月度积分服务 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class DeptMonthlyScoreService { + + private final DeptMonthlyScoreMapper deptMonthlyScoreMapper; + private final DepartmentMapper departmentMapper; + + /** + * 获取或创建当月积分记录 + */ + @Transactional(rollbackFor = Exception.class) + public DeptMonthlyScore getOrCreateCurrentMonth(Long deptId) { + LocalDateTime now = LocalDateTime.now(); + int year = now.getYear(); + int month = now.getMonthValue(); + + DeptMonthlyScore record = deptMonthlyScoreMapper.getByDeptAndMonth(deptId, year, month); + if (record == null) { + record = new DeptMonthlyScore(); + record.setDeptId(deptId); + + // 获取部门名称 + Department dept = departmentMapper.selectById(deptId); + if (dept != null) { + record.setDeptName(dept.getName()); + } + + record.setYear(year); + record.setMonth(month); + record.setEarnedScore(BigDecimal.ZERO); + record.setDeductedScore(BigDecimal.ZERO); + record.setNetScore(BigDecimal.ZERO); + record.setCompletedTaskCount(0); + record.setDelayedTaskCount(0); + record.setTotalDelayHours(BigDecimal.ZERO); + record.setCreateTime(now); + record.setUpdateTime(now); + + deptMonthlyScoreMapper.insert(record); + } + return record; + } + + /** + * 增加部门获得积分(任务完成时调用) + */ + @Transactional(rollbackFor = Exception.class) + public void addEarnedScore(Long deptId, BigDecimal score, boolean isDelayed, BigDecimal delayHours) { + DeptMonthlyScore record = getOrCreateCurrentMonth(deptId); + + // 增加获得积分 + record.setEarnedScore(record.getEarnedScore().add(score)); + record.setCompletedTaskCount(record.getCompletedTaskCount() + 1); + + // 如果延误,记录延误信息 + if (isDelayed) { + record.setDelayedTaskCount(record.getDelayedTaskCount() + 1); + if (delayHours != null) { + record.setTotalDelayHours(record.getTotalDelayHours().add(delayHours)); + } + } + + // 重新计算净积分 + record.setNetScore(record.getEarnedScore().subtract(record.getDeductedScore())); + record.setUpdateTime(LocalDateTime.now()); + + deptMonthlyScoreMapper.updateById(record); + } + + /** + * 增加部门扣除积分(延误扣分时调用) + */ + @Transactional(rollbackFor = Exception.class) + public void addDeductedScore(Long deptId, BigDecimal deductedScore) { + DeptMonthlyScore record = getOrCreateCurrentMonth(deptId); + + record.setDeductedScore(record.getDeductedScore().add(deductedScore)); + // 重新计算净积分 + record.setNetScore(record.getEarnedScore().subtract(record.getDeductedScore())); + record.setUpdateTime(LocalDateTime.now()); + + deptMonthlyScoreMapper.updateById(record); + } + + /** + * 获取部门当月积分 + */ + public DeptMonthlyScore getCurrentMonthScore(Long deptId) { + LocalDateTime now = LocalDateTime.now(); + return deptMonthlyScoreMapper.getByDeptAndMonth(deptId, now.getYear(), now.getMonthValue()); + } + + /** + * 获取部门某年某月积分 + */ + public DeptMonthlyScore getMonthScore(Long deptId, Integer year, Integer month) { + return deptMonthlyScoreMapper.getByDeptAndMonth(deptId, year, month); + } + + /** + * 获取部门某年的所有月度积分 + */ + public List getYearlyScores(Long deptId, Integer year) { + return deptMonthlyScoreMapper.getByDeptAndYear(deptId, year); + } + + /** + * 获取某月所有部门的积分排名 + */ + public List getMonthlyRanking(Integer year, Integer month) { + return deptMonthlyScoreMapper.getRankingByMonth(year, month); + } + + /** + * 获取当月所有部门的积分排名 + */ + public List getCurrentMonthRanking() { + LocalDateTime now = LocalDateTime.now(); + return getMonthlyRanking(now.getYear(), now.getMonthValue()); + } +} + diff --git a/backend/src/main/java/com/designhub/service/SystemConfigService.java b/backend/src/main/java/com/designhub/service/SystemConfigService.java index b7bc601..a48fea0 100644 --- a/backend/src/main/java/com/designhub/service/SystemConfigService.java +++ b/backend/src/main/java/com/designhub/service/SystemConfigService.java @@ -209,6 +209,8 @@ public class SystemConfigService { // 积分相关配置 createConfigIfNotExists("score.low_balance_alert", "100", "score", "积分余额预警阈值", "部门积分低于此值时发出预警", "number", 1); 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("notification.dingtalk_enabled", "false", "notification", "启用钉钉通知", "是否启用钉钉消息推送", "boolean", 1); diff --git a/backend/src/main/java/com/designhub/service/TaskService.java b/backend/src/main/java/com/designhub/service/TaskService.java index 89b294e..de5c4a3 100644 --- a/backend/src/main/java/com/designhub/service/TaskService.java +++ b/backend/src/main/java/com/designhub/service/TaskService.java @@ -56,6 +56,8 @@ public class TaskService { private final RoleService roleService; private final NotificationService notificationService; private final com.designhub.mapper.TaskItemMapper taskItemMapper; + private final DeptMonthlyScoreService deptMonthlyScoreService; + private final SystemConfigService systemConfigService; /** * 创建任务 @@ -1856,25 +1858,97 @@ public class TaskService { throw new BusinessException("只有待审核的任务才能审核"); } + LocalDateTime completeTime = LocalDateTime.now(); + // 更新任务状态为已完成 task.setStatus("completed"); - task.setCompleteTime(LocalDateTime.now()); - task.setUpdateTime(LocalDateTime.now()); + task.setCompleteTime(completeTime); + task.setUpdateTime(completeTime); task.setRating(5); // 默认5分 - // 计算积分奖励 + // 计算是否延误以及延误扣分 + boolean isDelayed = false; + BigDecimal delayHours = BigDecimal.ZERO; + 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); + + // 判断是否延误 + if (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()); + + // 从系统配置获取延误每小时扣分数 + String penaltyPerHourStr = systemConfigService.getConfigValue("score.delay_penalty_per_hour"); + BigDecimal penaltyPerHour = penaltyPerHourStr != null ? new BigDecimal(penaltyPerHourStr) : BigDecimal.valueOf(0.5); + + // 计算扣分 + deductedScore = delayHours.multiply(penaltyPerHour).setScale(2, java.math.RoundingMode.HALF_UP); + + log.info("任务延误: taskId={}, 延误小时数={}, 扣分={}", taskId, delayHours, deductedScore); + } else { + task.setIsDelayed(0); + task.setDelayDays(0); + } + } + + // 计算实际积分奖励(原积分 - 延误扣分) + BigDecimal earnedScore = BigDecimal.ZERO; if (task.getScore() != null) { - task.setScoreReward(BigDecimal.valueOf(task.getScore())); + earnedScore = BigDecimal.valueOf(task.getScore()); + BigDecimal actualReward = earnedScore.subtract(deductedScore); + if (actualReward.compareTo(BigDecimal.ZERO) < 0) { + actualReward = BigDecimal.ZERO; // 最少为0,不会变成负数 + } + task.setScoreReward(actualReward); } taskMapper.updateById(task); + // 更新接单部门的月度积分统计 + if (task.getReceiverDeptId() != null) { + try { + deptMonthlyScoreService.addEarnedScore( + task.getReceiverDeptId(), + task.getScoreReward() != null ? task.getScoreReward() : BigDecimal.ZERO, + isDelayed, + delayHours + ); + + // 如果有延误扣分,单独记录扣除 + if (deductedScore.compareTo(BigDecimal.ZERO) > 0) { + deptMonthlyScoreService.addDeductedScore(task.getReceiverDeptId(), deductedScore); + } + + log.info("更新部门月度积分: deptId={}, earned={}, deducted={}", + task.getReceiverDeptId(), task.getScoreReward(), deductedScore); + } catch (Exception e) { + log.error("更新部门月度积分失败", e); + } + } + // 发送通知给接单人 try { Notification notification = new Notification(); notification.setReceiverId(task.getReceiverId()); notification.setTitle("任务审核通过"); - notification.setContent("任务【" + task.getTitle() + "】审核通过,积分已到账"); + + String content = "任务【" + task.getTitle() + "】审核通过"; + if (isDelayed && deductedScore.compareTo(BigDecimal.ZERO) > 0) { + content += ",但因延误" + delayHours + "小时,扣除" + deductedScore + "分,实得" + task.getScoreReward() + "分"; + } else { + content += ",积分已到账"; + } + + notification.setContent(content); notification.setNotificationType("TASK_COMPLETED"); notification.setRelatedType("task"); notification.setRelatedId(taskId); @@ -1884,7 +1958,7 @@ public class TaskService { log.error("发送完成通知失败", e); } - log.info("任务审核通过: taskId={}", taskId); + log.info("任务审核通过: taskId={}, isDelayed={}, deductedScore={}", taskId, isDelayed, deductedScore); } /** diff --git a/frontend/src/views/Settings.vue b/frontend/src/views/Settings.vue index 5e198e9..bf786c1 100644 --- a/frontend/src/views/Settings.vue +++ b/frontend/src/views/Settings.vue @@ -1047,6 +1047,69 @@ + + +
+

延误扣分配置

+

设置任务延误时的扣分规则

+
+ + + + + 扣分规则 + + +
+
+
+ + 每小时扣分 +
+
任务延误完结时每延误1小时扣除的分数
+ + + +
+ +
+
+ + 月度积分清零 +
+
接单部门积分是否按月清零重新统计
+ +
+
+ + + + + + + + 保存配置 + + + +
@@ -3412,6 +3475,48 @@ const taskReleaseConfig = ref({ const savingTaskReleaseConfig = ref(false) +// ==================== 延误扣分配置 ==================== +const delayPenaltyConfig = ref({ + perHour: 0.5, // 每小时扣分 + monthlyReset: true // 月度积分清零 +}) + +const savingDelayPenaltyConfig = ref(false) + +// 加载延误扣分配置 +const loadDelayPenaltyConfig = async () => { + try { + const response = await getSystemConfigsByGroup('score') + const configs = response.data || [] + + const perHourConfig = configs.find((c: any) => c.configKey === 'score.delay_penalty_per_hour') + const monthlyResetConfig = configs.find((c: any) => c.configKey === 'score.monthly_reset_enabled') + + delayPenaltyConfig.value = { + perHour: perHourConfig ? parseFloat(perHourConfig.configValue) : 0.5, + monthlyReset: monthlyResetConfig?.configValue !== 'false' + } + } catch (error) { + console.error('加载延误扣分配置失败:', error) + } +} + +// 保存延误扣分配置 +const handleSaveDelayPenaltyConfig = async () => { + savingDelayPenaltyConfig.value = true + try { + await batchUpdateSystemConfigs({ + 'score.delay_penalty_per_hour': delayPenaltyConfig.value.perHour.toString(), + 'score.monthly_reset_enabled': delayPenaltyConfig.value.monthlyReset.toString() + }) + message.success('延误扣分配置保存成功') + } catch (error: any) { + message.error(error.message || '保存失败') + } finally { + savingDelayPenaltyConfig.value = false + } +} + // 加载任务发布类型配置 const loadTaskReleaseConfig = async () => { try { @@ -3641,6 +3746,8 @@ onMounted(() => { loadDingTalkConfig() // 加载任务发布类型配置 loadTaskReleaseConfig() + // 加载延误扣分配置 + loadDelayPenaltyConfig() }) // S3配置相关方法