feat: 添加每日进度更新检查功能
- 设计师大厅排行榜默认折叠,点击展开 - 系统设置添加每日未更新扣分配置项 - 每天18点检查设计师是否更新进度,未更新则扣分并发钉钉通知 - 扣分同步到数据分析页面设计师绩效的延误扣分
This commit is contained in:
parent
ca4ccecc8b
commit
dceff515b7
@ -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;
|
||||
}
|
||||
|
||||
@ -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<ProgressDeductionLog> {
|
||||
|
||||
/**
|
||||
* 查询用户指定月份的扣分总额
|
||||
*/
|
||||
@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);
|
||||
}
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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<Task> inProgressTasks = taskMapper.selectList(
|
||||
new LambdaQueryWrapper<Task>()
|
||||
.eq(Task::getStatus, "in_progress")
|
||||
.eq(Task::getDeleted, 0)
|
||||
.isNotNull(Task::getReceiverId)
|
||||
);
|
||||
|
||||
Map<Long, List<String>> 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<ProgressDeductionLog>()
|
||||
.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<TaskItem> inProgressItems = taskItemMapper.selectList(
|
||||
new LambdaQueryWrapper<TaskItem>()
|
||||
.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<ProgressDeductionLog>()
|
||||
.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<Long, List<String>> userMissedTasks, BigDecimal deductionScore) {
|
||||
for (Map.Entry<Long, List<String>> entry : userMissedTasks.entrySet()) {
|
||||
Long userId = entry.getKey();
|
||||
List<String> 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();
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -44,39 +44,46 @@
|
||||
|
||||
<!-- 排行榜Tab -->
|
||||
<div class="leaderboard-tabs chart-card">
|
||||
<div class="tabs-header-row">
|
||||
<div class="tabs-header-row" @click="leaderboardExpanded = !leaderboardExpanded" style="cursor: pointer;">
|
||||
<div class="tabs-header">
|
||||
<button
|
||||
v-for="tab in leaderboardTabs"
|
||||
:key="tab.key"
|
||||
:class="['tab-button', { active: activeLeaderboard === tab.key }]"
|
||||
@click="switchLeaderboard(tab.key)"
|
||||
@click.stop="switchLeaderboard(tab.key)"
|
||||
>
|
||||
<Icon :name="tab.iconName" size="sm" color="#31f08b" style="margin-right: 4px; vertical-align: middle;" />
|
||||
{{ tab.label }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 月份选择器 -->
|
||||
<div class="month-selector">
|
||||
<a-select
|
||||
v-model:value="selectedMonth"
|
||||
placeholder="选择月份"
|
||||
style="width: 150px"
|
||||
@change="handleMonthChange"
|
||||
>
|
||||
<a-select-option
|
||||
v-for="month in availableMonths"
|
||||
:key="month.value"
|
||||
:value="month.value"
|
||||
<div style="display: flex; align-items: center; gap: 12px;">
|
||||
<!-- 月份选择器 -->
|
||||
<div class="month-selector" @click.stop>
|
||||
<a-select
|
||||
v-model:value="selectedMonth"
|
||||
placeholder="选择月份"
|
||||
style="width: 150px"
|
||||
@change="handleMonthChange"
|
||||
>
|
||||
{{ month.label }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
<a-select-option
|
||||
v-for="month in availableMonths"
|
||||
:key="month.value"
|
||||
:value="month.value"
|
||||
>
|
||||
{{ month.label }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</div>
|
||||
<!-- 展开/折叠按钮 -->
|
||||
<a-button type="link" size="small" style="color: #31f08b;">
|
||||
{{ leaderboardExpanded ? '收起' : '展开' }}
|
||||
<Icon :name="leaderboardExpanded ? 'chevronUp' : 'chevronDown'" size="sm" style="margin-left: 4px;" />
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="leaderboard-content">
|
||||
<div v-show="leaderboardExpanded" class="leaderboard-content">
|
||||
<div
|
||||
v-for="(designer, index) in leaderboardData"
|
||||
:key="designer.userId"
|
||||
@ -274,6 +281,7 @@ const designersList = ref<DesignerStats[]>([])
|
||||
const leaderboardData = ref<DesignerStats[]>([])
|
||||
const activeLeaderboard = ref('score')
|
||||
const selectedMonth = ref('') // 选中的月份
|
||||
const leaderboardExpanded = ref(false) // 排行榜默认折叠
|
||||
|
||||
// 排行榜Tab配置
|
||||
const leaderboardTabs = [
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user