1295 lines
54 KiB
Java
1295 lines
54 KiB
Java
package com.designhub.service;
|
||
|
||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||
import com.designhub.entity.*;
|
||
import com.designhub.mapper.*;
|
||
import com.designhub.vo.DemandStatisticsVO;
|
||
import com.designhub.vo.DesignerPerformanceVO;
|
||
import com.designhub.vo.ProcessEfficiencyVO;
|
||
import com.designhub.vo.WarningTaskVO;
|
||
import lombok.RequiredArgsConstructor;
|
||
import lombok.extern.slf4j.Slf4j;
|
||
import org.springframework.stereotype.Service;
|
||
|
||
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.temporal.TemporalAdjusters;
|
||
import java.util.*;
|
||
import java.util.stream.Collectors;
|
||
|
||
/**
|
||
* 数据分析Service
|
||
*/
|
||
@Slf4j
|
||
@Service
|
||
@RequiredArgsConstructor
|
||
public class AnalyticsService {
|
||
|
||
private final TaskMapper taskMapper;
|
||
private final UserMapper userMapper;
|
||
private final DepartmentMapper departmentMapper;
|
||
private final TeamScorePoolMapper teamScorePoolMapper;
|
||
private final TaskReviewMapper taskReviewMapper;
|
||
private final RoleService roleService;
|
||
private final DesignerTargetScoreMapper designerTargetScoreMapper;
|
||
private final DemandTypeMapper demandTypeMapper;
|
||
|
||
/**
|
||
* 获取需求统计数据(根据角色返回不同维度的数据)
|
||
* @param currentUserId 当前登录用户ID
|
||
* @param dateRange 时间范围
|
||
* @param deptId 部门ID(可选,用于管理员查看特定部门)
|
||
* @param userId 用户ID(可选,用于管理员查看特定用户)
|
||
* @param month 指定月份(可选,格式 yyyy-MM)
|
||
*/
|
||
public DemandStatisticsVO getDemandStatistics(Long currentUserId, String dateRange, Long deptId, Long userId, String month) {
|
||
// 获取当前用户角色和部门信息
|
||
List<Role> roles = roleService.getRolesByUserId(currentUserId);
|
||
User currentUser = userMapper.selectById(currentUserId);
|
||
|
||
boolean isAdmin = roles.stream()
|
||
.anyMatch(role -> "ADMIN".equals(role.getRoleCode()) || "SUPER_ADMIN".equals(role.getRoleCode()));
|
||
|
||
// 检查 isAdmin 字段
|
||
if (!isAdmin && currentUser != null && Integer.valueOf(1).equals(currentUser.getIsAdmin())) {
|
||
isAdmin = true;
|
||
}
|
||
|
||
boolean isDeptHead = roles.stream()
|
||
.anyMatch(role -> "DEMAND_DEPT_HEAD".equals(role.getRoleCode()) ||
|
||
"RECEIVING_DEPT_SUPERVISOR".equals(role.getRoleCode()));
|
||
|
||
log.info("获取需求统计数据: currentUserId={}, isAdmin={}, isDeptHead={}, userDeptId={}, dateRange={}, deptId={}, userId={}, month={}",
|
||
currentUserId, isAdmin, isDeptHead, currentUser != null ? currentUser.getDeptId() : null, dateRange, deptId, userId, month);
|
||
|
||
// 计算时间范围
|
||
LocalDateTime startTime;
|
||
LocalDateTime endTime;
|
||
|
||
// 如果指定了月份,则使用月份范围
|
||
if (month != null && !month.isEmpty()) {
|
||
try {
|
||
LocalDate monthDate = LocalDate.parse(month + "-01", DateTimeFormatter.ofPattern("yyyy-MM-dd"));
|
||
startTime = LocalDateTime.of(monthDate, LocalTime.MIN);
|
||
endTime = LocalDateTime.of(monthDate.with(TemporalAdjusters.lastDayOfMonth()), LocalTime.MAX);
|
||
log.info("使用指定月份范围: {} ~ {}", startTime, endTime);
|
||
} catch (Exception e) {
|
||
log.warn("月份格式错误: {}, 使用默认范围", month);
|
||
startTime = calculateStartTime(dateRange);
|
||
endTime = LocalDateTime.now();
|
||
}
|
||
} else {
|
||
startTime = calculateStartTime(dateRange);
|
||
endTime = LocalDateTime.now();
|
||
}
|
||
|
||
// 构建查询条件
|
||
LambdaQueryWrapper<Task> queryWrapper = new LambdaQueryWrapper<Task>()
|
||
.ge(startTime != null, Task::getCreateTime, startTime)
|
||
.le(Task::getCreateTime, endTime);
|
||
|
||
// 根据角色和参数决定查询范围
|
||
if (isAdmin) {
|
||
// 管理员:可以通过参数查看特定部门或用户的数据,否则查看所有数据
|
||
if (deptId != null) {
|
||
queryWrapper.eq(Task::getPublisherDeptId, deptId);
|
||
log.info("管理员查看部门数据: deptId={}", deptId);
|
||
} else if (userId != null) {
|
||
queryWrapper.eq(Task::getPublisherId, userId);
|
||
log.info("管理员查看用户数据: userId={}", userId);
|
||
} else {
|
||
log.info("管理员查看全部数据");
|
||
}
|
||
} else if (isDeptHead && currentUser != null && currentUser.getDeptId() != null) {
|
||
// 部门负责人:查看本部门的所有任务
|
||
queryWrapper.eq(Task::getPublisherDeptId, currentUser.getDeptId());
|
||
log.info("部门负责人查看部门数据: deptId={}", currentUser.getDeptId());
|
||
} else {
|
||
// 普通用户:只能查看自己发布的任务
|
||
queryWrapper.eq(Task::getPublisherId, currentUserId);
|
||
log.info("普通用户查看个人数据: userId={}", currentUserId);
|
||
}
|
||
|
||
List<Task> tasks = taskMapper.selectList(queryWrapper);
|
||
log.info("查询到任务数量: {}", tasks.size());
|
||
|
||
return DemandStatisticsVO.builder()
|
||
.userStats(buildUserStats(tasks))
|
||
.deptStats(buildDeptStats(tasks))
|
||
.trendData(buildTrendData(tasks, dateRange))
|
||
.typeDistribution(buildTypeDistribution(tasks))
|
||
.overview(buildOverview(tasks))
|
||
.build();
|
||
}
|
||
|
||
/**
|
||
* 构建用户维度统计
|
||
*/
|
||
private List<DemandStatisticsVO.UserDemandStat> buildUserStats(List<Task> tasks) {
|
||
Map<Long, List<Task>> tasksByUser = tasks.stream()
|
||
.collect(Collectors.groupingBy(Task::getPublisherId));
|
||
|
||
List<DemandStatisticsVO.UserDemandStat> stats = new ArrayList<>();
|
||
|
||
for (Map.Entry<Long, List<Task>> entry : tasksByUser.entrySet()) {
|
||
Long userId = entry.getKey();
|
||
List<Task> userTasks = entry.getValue();
|
||
|
||
User user = userMapper.selectById(userId);
|
||
if (user == null) continue;
|
||
|
||
int totalDemands = userTasks.size();
|
||
int completedDemands = (int) userTasks.stream()
|
||
.filter(t -> "completed".equals(t.getStatus()))
|
||
.count();
|
||
int pendingDemands = (int) userTasks.stream()
|
||
.filter(t -> Arrays.asList("pending", "in_progress", "submitted").contains(t.getStatus()))
|
||
.count();
|
||
|
||
BigDecimal totalScore = userTasks.stream()
|
||
.map(Task::getScoreReward)
|
||
.filter(Objects::nonNull)
|
||
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||
|
||
BigDecimal avgScore = totalDemands > 0
|
||
? totalScore.divide(BigDecimal.valueOf(totalDemands), 2, RoundingMode.HALF_UP)
|
||
: BigDecimal.ZERO;
|
||
|
||
double completionRate = totalDemands > 0
|
||
? (double) completedDemands / totalDemands * 100
|
||
: 0.0;
|
||
|
||
// 计算满意度(五星好评率)
|
||
double satisfactionRate = calculateSatisfactionRate(userTasks);
|
||
|
||
Department dept = departmentMapper.selectById(user.getDeptId());
|
||
|
||
stats.add(DemandStatisticsVO.UserDemandStat.builder()
|
||
.userId(userId)
|
||
.userName(user.getUsername())
|
||
.deptName(dept != null ? dept.getDeptName() : "")
|
||
.totalDemands(totalDemands)
|
||
.completedDemands(completedDemands)
|
||
.pendingDemands(pendingDemands)
|
||
.totalScore(totalScore)
|
||
.avgScore(avgScore)
|
||
.completionRate(completionRate)
|
||
.satisfactionRate(satisfactionRate)
|
||
.build());
|
||
}
|
||
|
||
// 按总需求数降序排序
|
||
stats.sort((a, b) -> b.getTotalDemands().compareTo(a.getTotalDemands()));
|
||
|
||
return stats;
|
||
}
|
||
|
||
/**
|
||
* 构建部门维度统计
|
||
*/
|
||
private List<DemandStatisticsVO.DeptDemandStat> buildDeptStats(List<Task> tasks) {
|
||
Map<Long, List<Task>> tasksByDept = tasks.stream()
|
||
.filter(t -> t.getPublisherDeptId() != null)
|
||
.collect(Collectors.groupingBy(Task::getPublisherDeptId));
|
||
|
||
List<DemandStatisticsVO.DeptDemandStat> stats = new ArrayList<>();
|
||
|
||
for (Map.Entry<Long, List<Task>> entry : tasksByDept.entrySet()) {
|
||
Long deptId = entry.getKey();
|
||
List<Task> deptTasks = entry.getValue();
|
||
|
||
Department dept = departmentMapper.selectById(deptId);
|
||
if (dept == null) continue;
|
||
|
||
TeamScorePool scorePool = teamScorePoolMapper.selectOne(
|
||
new LambdaQueryWrapper<TeamScorePool>().eq(TeamScorePool::getDeptId, deptId)
|
||
);
|
||
|
||
int totalDemands = deptTasks.size();
|
||
int completedDemands = (int) deptTasks.stream()
|
||
.filter(t -> "completed".equals(t.getStatus()))
|
||
.count();
|
||
int pendingDemands = (int) deptTasks.stream()
|
||
.filter(t -> Arrays.asList("pending", "in_progress", "submitted").contains(t.getStatus()))
|
||
.count();
|
||
|
||
BigDecimal totalScore = deptTasks.stream()
|
||
.map(Task::getScoreReward)
|
||
.filter(Objects::nonNull)
|
||
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||
|
||
// 计算使用率和预警级别
|
||
double utilizationRate = 0.0;
|
||
String alertLevel = "safe";
|
||
|
||
if (scorePool != null) {
|
||
BigDecimal totalAvailable = BigDecimal.valueOf(scorePool.getTotalScore() != null ? scorePool.getTotalScore() : 0);
|
||
if (totalAvailable.compareTo(BigDecimal.ZERO) > 0) {
|
||
utilizationRate = totalScore.divide(totalAvailable, 4, RoundingMode.HALF_UP)
|
||
.multiply(BigDecimal.valueOf(100))
|
||
.doubleValue();
|
||
}
|
||
|
||
// 计算预警级别
|
||
BigDecimal remainingScore = BigDecimal.valueOf(scorePool.getRemainingScore() != null ? scorePool.getRemainingScore() : 0);
|
||
if (totalAvailable.compareTo(BigDecimal.ZERO) > 0) {
|
||
double remainingRate = remainingScore.divide(totalAvailable, 4, RoundingMode.HALF_UP)
|
||
.multiply(BigDecimal.valueOf(100))
|
||
.doubleValue();
|
||
|
||
if (remainingRate < 10) {
|
||
alertLevel = "danger";
|
||
} else if (remainingRate < 30) {
|
||
alertLevel = "warning";
|
||
}
|
||
}
|
||
|
||
stats.add(DemandStatisticsVO.DeptDemandStat.builder()
|
||
.deptId(deptId)
|
||
.deptName(dept.getDeptName())
|
||
.totalDemands(totalDemands)
|
||
.completedDemands(completedDemands)
|
||
.pendingDemands(pendingDemands)
|
||
.totalScore(totalScore)
|
||
.remainingScore(BigDecimal.valueOf(scorePool.getRemainingScore() != null ? scorePool.getRemainingScore() : 0))
|
||
.dailyLimit(BigDecimal.valueOf(scorePool.getDailyLimit() != null ? scorePool.getDailyLimit() : 0))
|
||
.weeklyLimit(BigDecimal.valueOf(scorePool.getWeeklyLimit() != null ? scorePool.getWeeklyLimit() : 0))
|
||
.monthlyLimit(BigDecimal.valueOf(scorePool.getMonthlyLimit() != null ? scorePool.getMonthlyLimit() : 0))
|
||
.dailyUsed(BigDecimal.valueOf(scorePool.getDailyUsed() != null ? scorePool.getDailyUsed() : 0))
|
||
.weeklyUsed(BigDecimal.valueOf(scorePool.getWeeklyUsed() != null ? scorePool.getWeeklyUsed() : 0))
|
||
.monthlyUsed(BigDecimal.valueOf(scorePool.getMonthlyUsed() != null ? scorePool.getMonthlyUsed() : 0))
|
||
.utilizationRate(utilizationRate)
|
||
.alertLevel(alertLevel)
|
||
.build());
|
||
}
|
||
}
|
||
|
||
// 按总需求数降序排序
|
||
stats.sort((a, b) -> b.getTotalDemands().compareTo(a.getTotalDemands()));
|
||
|
||
return stats;
|
||
}
|
||
|
||
/**
|
||
* 构建趋势数据
|
||
*/
|
||
private List<DemandStatisticsVO.TrendData> buildTrendData(List<Task> tasks, String dateRange) {
|
||
Map<String, List<Task>> tasksByDate = new LinkedHashMap<>();
|
||
|
||
// 根据dateRange生成日期范围
|
||
int days = "week".equals(dateRange) ? 7 : "month".equals(dateRange) ? 30 : 90;
|
||
LocalDate endDate = LocalDate.now();
|
||
|
||
for (int i = days - 1; i >= 0; i--) {
|
||
LocalDate date = endDate.minusDays(i);
|
||
tasksByDate.put(date.format(DateTimeFormatter.ISO_LOCAL_DATE), new ArrayList<>());
|
||
}
|
||
|
||
// 将任务按日期分组
|
||
for (Task task : tasks) {
|
||
String dateKey = task.getCreateTime().toLocalDate().format(DateTimeFormatter.ISO_LOCAL_DATE);
|
||
if (tasksByDate.containsKey(dateKey)) {
|
||
tasksByDate.get(dateKey).add(task);
|
||
}
|
||
}
|
||
|
||
List<DemandStatisticsVO.TrendData> trendData = new ArrayList<>();
|
||
for (Map.Entry<String, List<Task>> entry : tasksByDate.entrySet()) {
|
||
List<Task> dateTasks = entry.getValue();
|
||
|
||
int demandCount = dateTasks.size();
|
||
int completedCount = (int) dateTasks.stream()
|
||
.filter(t -> "completed".equals(t.getStatus()))
|
||
.count();
|
||
|
||
BigDecimal scoreUsed = dateTasks.stream()
|
||
.map(Task::getScoreReward)
|
||
.filter(Objects::nonNull)
|
||
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||
|
||
trendData.add(DemandStatisticsVO.TrendData.builder()
|
||
.date(entry.getKey())
|
||
.demandCount(demandCount)
|
||
.completedCount(completedCount)
|
||
.scoreUsed(scoreUsed)
|
||
.build());
|
||
}
|
||
|
||
return trendData;
|
||
}
|
||
|
||
/**
|
||
* 构建类型分布
|
||
*/
|
||
private List<DemandStatisticsVO.TypeDistribution> buildTypeDistribution(List<Task> tasks) {
|
||
// 优先使用 taskTypeId,如果为空则使用 demandTypeId
|
||
Map<Long, List<Task>> tasksByType = tasks.stream()
|
||
.filter(t -> t.getTaskTypeId() != null || t.getDemandTypeId() != null)
|
||
.collect(Collectors.groupingBy(t -> {
|
||
if (t.getTaskTypeId() != null) {
|
||
return t.getTaskTypeId();
|
||
}
|
||
return t.getDemandTypeId();
|
||
}));
|
||
|
||
int totalTasks = tasks.size();
|
||
List<DemandStatisticsVO.TypeDistribution> distribution = new ArrayList<>();
|
||
|
||
// 预加载所有任务类型名称
|
||
Map<Long, String> typeNameMap = new HashMap<>();
|
||
for (Long typeId : tasksByType.keySet()) {
|
||
try {
|
||
DemandType demandType = demandTypeMapper.selectById(typeId);
|
||
if (demandType != null) {
|
||
typeNameMap.put(typeId, demandType.getName());
|
||
} else {
|
||
typeNameMap.put(typeId, "未知类型");
|
||
}
|
||
} catch (Exception e) {
|
||
typeNameMap.put(typeId, "类型" + typeId);
|
||
}
|
||
}
|
||
|
||
for (Map.Entry<Long, List<Task>> entry : tasksByType.entrySet()) {
|
||
List<Task> typeTasks = entry.getValue();
|
||
int count = typeTasks.size();
|
||
double percentage = totalTasks > 0 ? (double) count / totalTasks * 100 : 0.0;
|
||
|
||
// 优先使用 scoreReward,其次使用 score
|
||
BigDecimal totalScore = typeTasks.stream()
|
||
.map(t -> {
|
||
if (t.getScoreReward() != null && t.getScoreReward().compareTo(BigDecimal.ZERO) > 0) {
|
||
return t.getScoreReward();
|
||
}
|
||
return t.getScore() != null ? BigDecimal.valueOf(t.getScore()) : BigDecimal.ZERO;
|
||
})
|
||
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||
|
||
String typeName = typeNameMap.getOrDefault(entry.getKey(), "类型" + entry.getKey());
|
||
|
||
distribution.add(DemandStatisticsVO.TypeDistribution.builder()
|
||
.typeName(typeName)
|
||
.count(count)
|
||
.percentage(percentage)
|
||
.totalScore(totalScore)
|
||
.build());
|
||
}
|
||
|
||
// 按数量降序排序
|
||
distribution.sort((a, b) -> b.getCount().compareTo(a.getCount()));
|
||
|
||
return distribution;
|
||
}
|
||
|
||
/**
|
||
* 构建总览数据
|
||
*/
|
||
private DemandStatisticsVO.OverviewData buildOverview(List<Task> tasks) {
|
||
LocalDateTime now = LocalDateTime.now();
|
||
LocalDate today = now.toLocalDate();
|
||
LocalDate weekStart = today.minusDays(today.getDayOfWeek().getValue() - 1);
|
||
LocalDate monthStart = today.withDayOfMonth(1);
|
||
|
||
int totalDemands = tasks.size();
|
||
|
||
int todayDemands = (int) tasks.stream()
|
||
.filter(t -> t.getCreateTime().toLocalDate().equals(today))
|
||
.count();
|
||
|
||
int weekDemands = (int) tasks.stream()
|
||
.filter(t -> !t.getCreateTime().toLocalDate().isBefore(weekStart))
|
||
.count();
|
||
|
||
int monthDemands = (int) tasks.stream()
|
||
.filter(t -> !t.getCreateTime().toLocalDate().isBefore(monthStart))
|
||
.count();
|
||
|
||
int completedDemands = (int) tasks.stream()
|
||
.filter(t -> "completed".equals(t.getStatus()))
|
||
.count();
|
||
|
||
int pendingDemands = (int) tasks.stream()
|
||
.filter(t -> Arrays.asList("pending", "in_progress", "submitted").contains(t.getStatus()))
|
||
.count();
|
||
|
||
int canceledDemands = (int) tasks.stream()
|
||
.filter(t -> "cancelled".equals(t.getStatus()))
|
||
.count();
|
||
|
||
BigDecimal totalScoreUsed = tasks.stream()
|
||
.map(Task::getScoreReward)
|
||
.filter(Objects::nonNull)
|
||
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||
|
||
BigDecimal todayScoreUsed = tasks.stream()
|
||
.filter(t -> t.getCreateTime().toLocalDate().equals(today))
|
||
.map(Task::getScoreReward)
|
||
.filter(Objects::nonNull)
|
||
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||
|
||
BigDecimal weekScoreUsed = tasks.stream()
|
||
.filter(t -> !t.getCreateTime().toLocalDate().isBefore(weekStart))
|
||
.map(Task::getScoreReward)
|
||
.filter(Objects::nonNull)
|
||
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||
|
||
BigDecimal monthScoreUsed = tasks.stream()
|
||
.filter(t -> !t.getCreateTime().toLocalDate().isBefore(monthStart))
|
||
.map(Task::getScoreReward)
|
||
.filter(Objects::nonNull)
|
||
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||
|
||
// 计算平均完成时长
|
||
double avgCompletionTime = calculateAvgCompletionTime(tasks);
|
||
|
||
// 计算平均满意度
|
||
double avgSatisfaction = calculateAvgSatisfaction(tasks);
|
||
|
||
return DemandStatisticsVO.OverviewData.builder()
|
||
.totalDemands(totalDemands)
|
||
.todayDemands(todayDemands)
|
||
.weekDemands(weekDemands)
|
||
.monthDemands(monthDemands)
|
||
.completedDemands(completedDemands)
|
||
.pendingDemands(pendingDemands)
|
||
.canceledDemands(canceledDemands)
|
||
.totalScoreUsed(totalScoreUsed)
|
||
.todayScoreUsed(todayScoreUsed)
|
||
.weekScoreUsed(weekScoreUsed)
|
||
.monthScoreUsed(monthScoreUsed)
|
||
.avgCompletionTime(avgCompletionTime)
|
||
.avgSatisfaction(avgSatisfaction)
|
||
.build();
|
||
}
|
||
|
||
/**
|
||
* 计算时间范围起始时间
|
||
*/
|
||
private LocalDateTime calculateStartTime(String dateRange) {
|
||
LocalDateTime now = LocalDateTime.now();
|
||
switch (dateRange) {
|
||
case "week":
|
||
return now.minusWeeks(1);
|
||
case "month":
|
||
return now.minusMonths(1);
|
||
case "quarter":
|
||
return now.minusMonths(3);
|
||
case "year":
|
||
return now.minusYears(1);
|
||
default:
|
||
return null; // 全部
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 计算满意度(五星好评率)
|
||
*/
|
||
private double calculateSatisfactionRate(List<Task> tasks) {
|
||
try {
|
||
List<Long> completedTaskIds = tasks.stream()
|
||
.filter(t -> "completed".equals(t.getStatus()))
|
||
.map(Task::getId)
|
||
.collect(Collectors.toList());
|
||
|
||
if (completedTaskIds.isEmpty()) {
|
||
return 0.0;
|
||
}
|
||
|
||
long fiveStarCount = taskReviewMapper.selectCount(
|
||
new LambdaQueryWrapper<TaskReview>()
|
||
.in(TaskReview::getTaskId, completedTaskIds)
|
||
.eq(TaskReview::getRating, 5)
|
||
);
|
||
|
||
return (double) fiveStarCount / completedTaskIds.size() * 100;
|
||
} catch (Exception e) {
|
||
log.warn("计算满意度失败(可能task_review表不存在): {}", e.getMessage());
|
||
return 0.0;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 计算平均完成时长(小时)
|
||
*/
|
||
private double calculateAvgCompletionTime(List<Task> tasks) {
|
||
List<Task> completedTasks = tasks.stream()
|
||
.filter(t -> "completed".equals(t.getStatus()) && t.getActualEndTime() != null)
|
||
.collect(Collectors.toList());
|
||
|
||
if (completedTasks.isEmpty()) {
|
||
return 0.0;
|
||
}
|
||
|
||
long totalHours = completedTasks.stream()
|
||
.mapToLong(t -> {
|
||
LocalDateTime start = t.getActualStartTime() != null ? t.getActualStartTime() : t.getCreateTime();
|
||
LocalDateTime end = t.getActualEndTime();
|
||
return java.time.Duration.between(start, end).toHours();
|
||
})
|
||
.sum();
|
||
|
||
return (double) totalHours / completedTasks.size();
|
||
}
|
||
|
||
/**
|
||
* 计算平均满意度
|
||
*/
|
||
private double calculateAvgSatisfaction(List<Task> tasks) {
|
||
try {
|
||
List<Long> completedTaskIds = tasks.stream()
|
||
.filter(t -> "completed".equals(t.getStatus()))
|
||
.map(Task::getId)
|
||
.collect(Collectors.toList());
|
||
|
||
if (completedTaskIds.isEmpty()) {
|
||
return 0.0;
|
||
}
|
||
|
||
List<TaskReview> reviews = taskReviewMapper.selectList(
|
||
new LambdaQueryWrapper<TaskReview>()
|
||
.in(TaskReview::getTaskId, completedTaskIds)
|
||
);
|
||
|
||
if (reviews.isEmpty()) {
|
||
return 0.0;
|
||
}
|
||
|
||
double totalRating = reviews.stream()
|
||
.mapToInt(TaskReview::getRating)
|
||
.sum();
|
||
|
||
return totalRating / reviews.size();
|
||
} catch (Exception e) {
|
||
log.warn("计算平均满意度失败(可能task_review表不存在): {}", e.getMessage());
|
||
return 0.0;
|
||
}
|
||
}
|
||
|
||
// ==================== 设计师绩效统计 ====================
|
||
|
||
/**
|
||
* 获取设计师绩效数据
|
||
*/
|
||
public DesignerPerformanceVO getDesignerPerformance(Long currentUserId, String month, Long deptId) {
|
||
// 权限检查
|
||
List<Role> roles = roleService.getRolesByUserId(currentUserId);
|
||
User currentUser = userMapper.selectById(currentUserId);
|
||
|
||
boolean isAdmin = roles.stream()
|
||
.anyMatch(role -> "ADMIN".equals(role.getRoleCode()) || "SUPER_ADMIN".equals(role.getRoleCode()));
|
||
if (!isAdmin && currentUser != null && Integer.valueOf(1).equals(currentUser.getIsAdmin())) {
|
||
isAdmin = true;
|
||
}
|
||
|
||
// 计算时间范围和当前月份
|
||
LocalDateTime startTime;
|
||
LocalDateTime endTime;
|
||
String currentMonth; // 用于目标积分查询
|
||
|
||
if (month != null && !month.isEmpty()) {
|
||
currentMonth = month;
|
||
try {
|
||
LocalDate monthDate = LocalDate.parse(month + "-01", DateTimeFormatter.ofPattern("yyyy-MM-dd"));
|
||
startTime = LocalDateTime.of(monthDate, LocalTime.MIN);
|
||
endTime = LocalDateTime.of(monthDate.with(TemporalAdjusters.lastDayOfMonth()), LocalTime.MAX);
|
||
} catch (Exception e) {
|
||
LocalDate now = LocalDate.now();
|
||
startTime = LocalDateTime.of(now.withDayOfMonth(1), LocalTime.MIN);
|
||
endTime = LocalDateTime.now();
|
||
currentMonth = now.format(DateTimeFormatter.ofPattern("yyyy-MM"));
|
||
}
|
||
} else {
|
||
LocalDate now = LocalDate.now();
|
||
startTime = LocalDateTime.of(now.withDayOfMonth(1), LocalTime.MIN);
|
||
endTime = LocalDateTime.now();
|
||
currentMonth = now.format(DateTimeFormatter.ofPattern("yyyy-MM"));
|
||
}
|
||
|
||
// 获取所有设计师(接单部门成员)
|
||
List<User> designers = getDesigners(isAdmin, currentUser, deptId);
|
||
|
||
// 获取时间范围内的任务
|
||
LambdaQueryWrapper<Task> taskQuery = new LambdaQueryWrapper<Task>()
|
||
.ge(Task::getCreateTime, startTime)
|
||
.le(Task::getCreateTime, endTime)
|
||
.isNotNull(Task::getReceiverId);
|
||
|
||
List<Task> tasks = taskMapper.selectList(taskQuery);
|
||
|
||
// 计算本周范围
|
||
LocalDate today = LocalDate.now();
|
||
LocalDate weekStart = today.minusDays(today.getDayOfWeek().getValue() - 1);
|
||
LocalDateTime weekStartTime = LocalDateTime.of(weekStart, LocalTime.MIN);
|
||
|
||
// 构建设计师绩效列表
|
||
List<DesignerPerformanceVO.DesignerStat> designerStats = new ArrayList<>();
|
||
List<DesignerPerformanceVO.WorkloadStat> workloadStats = new ArrayList<>();
|
||
|
||
int totalReceived = 0;
|
||
int totalCompleted = 0;
|
||
double totalOnTimeRate = 0;
|
||
double totalProcessTime = 0;
|
||
double totalSatisfaction = 0;
|
||
int overloadCount = 0;
|
||
int designerWithData = 0;
|
||
|
||
for (User designer : designers) {
|
||
// 获取该设计师的任务
|
||
List<Task> designerTasks = tasks.stream()
|
||
.filter(t -> designer.getId().equals(t.getReceiverId()))
|
||
.collect(Collectors.toList());
|
||
|
||
int receivedCount = designerTasks.size();
|
||
int completedCount = (int) designerTasks.stream()
|
||
.filter(t -> "completed".equals(t.getStatus()))
|
||
.count();
|
||
int inProgressCount = (int) designerTasks.stream()
|
||
.filter(t -> "in_progress".equals(t.getStatus()))
|
||
.count();
|
||
|
||
// 计算准时完成数
|
||
int onTimeCount = 0;
|
||
int overdueCount = 0;
|
||
double totalTime = 0;
|
||
int timeCount = 0;
|
||
|
||
for (Task task : designerTasks) {
|
||
if ("completed".equals(task.getStatus()) && task.getActualEndTime() != null) {
|
||
LocalDateTime start = task.getActualStartTime() != null ? task.getActualStartTime() : task.getStartTime();
|
||
if (start != null) {
|
||
long hours = Duration.between(start, task.getActualEndTime()).toHours();
|
||
totalTime += hours;
|
||
timeCount++;
|
||
}
|
||
|
||
// 判断是否准时
|
||
if (task.getDeadline() != null) {
|
||
if (!task.getActualEndTime().isAfter(task.getDeadline())) {
|
||
onTimeCount++;
|
||
} else {
|
||
overdueCount++;
|
||
}
|
||
} else {
|
||
onTimeCount++; // 没有deadline的默认准时
|
||
}
|
||
}
|
||
}
|
||
|
||
double completionRate = receivedCount > 0 ? (double) completedCount / receivedCount * 100 : 0;
|
||
double onTimeRate = completedCount > 0 ? (double) onTimeCount / completedCount * 100 : 0;
|
||
double avgProcessTime = timeCount > 0 ? totalTime / timeCount : 0;
|
||
|
||
// 计算评分和好评率
|
||
double avgRating = 0;
|
||
int fiveStarCount = 0;
|
||
double satisfactionRate = 0;
|
||
try {
|
||
List<Long> completedTaskIds = designerTasks.stream()
|
||
.filter(t -> "completed".equals(t.getStatus()))
|
||
.map(Task::getId)
|
||
.collect(Collectors.toList());
|
||
|
||
if (!completedTaskIds.isEmpty()) {
|
||
List<TaskReview> reviews = taskReviewMapper.selectList(
|
||
new LambdaQueryWrapper<TaskReview>()
|
||
.in(TaskReview::getTaskId, completedTaskIds)
|
||
);
|
||
|
||
if (!reviews.isEmpty()) {
|
||
avgRating = reviews.stream().mapToInt(TaskReview::getRating).average().orElse(0);
|
||
fiveStarCount = (int) reviews.stream().filter(r -> r.getRating() == 5).count();
|
||
satisfactionRate = (double) fiveStarCount / reviews.size() * 100;
|
||
}
|
||
}
|
||
} catch (Exception e) {
|
||
log.warn("获取评价数据失败: {}", e.getMessage());
|
||
}
|
||
|
||
// 计算获得积分和延误扣分
|
||
BigDecimal totalScore = BigDecimal.ZERO;
|
||
BigDecimal deductionScore = BigDecimal.ZERO;
|
||
|
||
// 获取扣分比例配置(默认每延误1小时扣任务积分的1%)
|
||
BigDecimal deductionRate = getDeductionRate(designer.getDeptId(), currentMonth);
|
||
|
||
for (Task task : designerTasks) {
|
||
if ("completed".equals(task.getStatus())) {
|
||
// 计算任务积分
|
||
BigDecimal taskScore = BigDecimal.ZERO;
|
||
if (task.getScoreReward() != null && task.getScoreReward().compareTo(BigDecimal.ZERO) > 0) {
|
||
taskScore = task.getScoreReward();
|
||
} else if (task.getScore() != null) {
|
||
taskScore = BigDecimal.valueOf(task.getScore());
|
||
}
|
||
totalScore = totalScore.add(taskScore);
|
||
|
||
// 计算延误扣分(如果超时完成)
|
||
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);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 净积分 = 总积分 - 扣分
|
||
BigDecimal netScore = totalScore.subtract(deductionScore);
|
||
if (netScore.compareTo(BigDecimal.ZERO) < 0) {
|
||
netScore = BigDecimal.ZERO;
|
||
}
|
||
|
||
// 获取目标积分
|
||
BigDecimal targetScore = getTargetScore(designer.getId(), designer.getDeptId(), currentMonth);
|
||
|
||
// 计算目标完成率
|
||
Double targetCompletionRate = 0.0;
|
||
if (targetScore != null && targetScore.compareTo(BigDecimal.ZERO) > 0) {
|
||
targetCompletionRate = netScore.divide(targetScore, 4, RoundingMode.HALF_UP)
|
||
.multiply(BigDecimal.valueOf(100))
|
||
.doubleValue();
|
||
targetCompletionRate = Math.round(targetCompletionRate * 10) / 10.0;
|
||
}
|
||
|
||
// 本周完成数
|
||
int weekCompletedCount = (int) designerTasks.stream()
|
||
.filter(t -> "completed".equals(t.getStatus()) &&
|
||
t.getActualEndTime() != null &&
|
||
!t.getActualEndTime().isBefore(weekStartTime))
|
||
.count();
|
||
|
||
Department dept = designer.getDeptId() != null ? departmentMapper.selectById(designer.getDeptId()) : null;
|
||
|
||
designerStats.add(DesignerPerformanceVO.DesignerStat.builder()
|
||
.userId(designer.getId())
|
||
.userName(designer.getUsername())
|
||
.realName(designer.getRealName())
|
||
.deptName(dept != null ? dept.getDeptName() : "")
|
||
.deptId(designer.getDeptId())
|
||
.receivedCount(receivedCount)
|
||
.completedCount(completedCount)
|
||
.inProgressCount(inProgressCount)
|
||
.completionRate(Math.round(completionRate * 10) / 10.0)
|
||
.onTimeRate(Math.round(onTimeRate * 10) / 10.0)
|
||
.onTimeCount(onTimeCount)
|
||
.overdueCount(overdueCount)
|
||
.avgProcessTime(Math.round(avgProcessTime * 10) / 10.0)
|
||
.avgRating(Math.round(avgRating * 10) / 10.0)
|
||
.fiveStarCount(fiveStarCount)
|
||
.satisfactionRate(Math.round(satisfactionRate * 10) / 10.0)
|
||
.totalScore(totalScore)
|
||
.deductionScore(deductionScore)
|
||
.netScore(netScore)
|
||
.targetScore(targetScore)
|
||
.targetCompletionRate(targetCompletionRate)
|
||
.weekCompletedCount(weekCompletedCount)
|
||
.monthCompletedCount(completedCount)
|
||
.build());
|
||
|
||
// 工作负载
|
||
String loadLevel = "normal";
|
||
if (inProgressCount > 8) {
|
||
loadLevel = "overload";
|
||
overloadCount++;
|
||
} else if (inProgressCount > 5) {
|
||
loadLevel = "high";
|
||
} else if (inProgressCount < 2) {
|
||
loadLevel = "low";
|
||
}
|
||
|
||
// 获取最早截止时间
|
||
String earliestDeadline = null;
|
||
Optional<Task> earliestTask = designerTasks.stream()
|
||
.filter(t -> "in_progress".equals(t.getStatus()) && t.getDeadline() != null)
|
||
.min(Comparator.comparing(Task::getDeadline));
|
||
if (earliestTask.isPresent()) {
|
||
earliestDeadline = earliestTask.get().getDeadline().format(DateTimeFormatter.ofPattern("MM-dd HH:mm"));
|
||
}
|
||
|
||
workloadStats.add(DesignerPerformanceVO.WorkloadStat.builder()
|
||
.userId(designer.getId())
|
||
.userName(designer.getUsername())
|
||
.realName(designer.getRealName())
|
||
.deptName(dept != null ? dept.getDeptName() : "")
|
||
.currentLoad(inProgressCount)
|
||
.loadLevel(loadLevel)
|
||
.earliestDeadline(earliestDeadline)
|
||
.avgDailyTasks(0.0) // 可后续优化
|
||
.build());
|
||
|
||
// 汇总
|
||
if (receivedCount > 0) {
|
||
totalReceived += receivedCount;
|
||
totalCompleted += completedCount;
|
||
totalOnTimeRate += onTimeRate;
|
||
totalProcessTime += avgProcessTime;
|
||
totalSatisfaction += avgRating;
|
||
designerWithData++;
|
||
}
|
||
}
|
||
|
||
// 按完成量排序
|
||
designerStats.sort((a, b) -> b.getCompletedCount().compareTo(a.getCompletedCount()));
|
||
// 按当前负载排序
|
||
workloadStats.sort((a, b) -> b.getCurrentLoad().compareTo(a.getCurrentLoad()));
|
||
|
||
// 构建汇总数据
|
||
DesignerPerformanceVO.Summary summary = DesignerPerformanceVO.Summary.builder()
|
||
.totalDesigners(designers.size())
|
||
.totalReceived(totalReceived)
|
||
.totalCompleted(totalCompleted)
|
||
.avgCompletionRate(designerWithData > 0 ? Math.round(totalCompleted * 1000.0 / totalReceived) / 10.0 : 0)
|
||
.avgOnTimeRate(designerWithData > 0 ? Math.round(totalOnTimeRate / designerWithData * 10) / 10.0 : 0)
|
||
.avgProcessTime(designerWithData > 0 ? Math.round(totalProcessTime / designerWithData * 10) / 10.0 : 0)
|
||
.avgSatisfaction(designerWithData > 0 ? Math.round(totalSatisfaction / designerWithData * 10) / 10.0 : 0)
|
||
.overloadCount(overloadCount)
|
||
.build();
|
||
|
||
return DesignerPerformanceVO.builder()
|
||
.designerStats(designerStats)
|
||
.workloadStats(workloadStats)
|
||
.summary(summary)
|
||
.build();
|
||
}
|
||
|
||
/**
|
||
* 获取设计师列表
|
||
*/
|
||
private List<User> getDesigners(boolean isAdmin, User currentUser, Long deptId) {
|
||
LambdaQueryWrapper<User> userQuery = new LambdaQueryWrapper<User>()
|
||
.eq(User::getStatus, 1);
|
||
|
||
if (deptId != null) {
|
||
userQuery.eq(User::getDeptId, deptId);
|
||
} else if (!isAdmin && currentUser != null && currentUser.getDeptId() != null) {
|
||
// 非管理员只能看本部门
|
||
userQuery.eq(User::getDeptId, currentUser.getDeptId());
|
||
}
|
||
|
||
List<User> allUsers = userMapper.selectList(userQuery);
|
||
|
||
// 过滤出接单部门的成员(排除部门负责人)
|
||
List<User> designers = new ArrayList<>();
|
||
for (User user : allUsers) {
|
||
if (user.getDeptId() != null) {
|
||
Department dept = departmentMapper.selectById(user.getDeptId());
|
||
if (dept != null && "receiving".equals(dept.getDeptType())) {
|
||
// 排除部门负责人
|
||
if (!user.getId().equals(dept.getLeaderId())) {
|
||
designers.add(user);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
return designers;
|
||
}
|
||
|
||
// ==================== 流程效率统计 ====================
|
||
|
||
/**
|
||
* 获取流程效率数据
|
||
*/
|
||
public ProcessEfficiencyVO getProcessEfficiency(Long currentUserId, String month) {
|
||
// 计算时间范围
|
||
LocalDateTime startTime;
|
||
LocalDateTime endTime;
|
||
|
||
if (month != null && !month.isEmpty()) {
|
||
try {
|
||
LocalDate monthDate = LocalDate.parse(month + "-01", DateTimeFormatter.ofPattern("yyyy-MM-dd"));
|
||
startTime = LocalDateTime.of(monthDate, LocalTime.MIN);
|
||
endTime = LocalDateTime.of(monthDate.with(TemporalAdjusters.lastDayOfMonth()), LocalTime.MAX);
|
||
} catch (Exception e) {
|
||
LocalDate now = LocalDate.now();
|
||
startTime = LocalDateTime.of(now.withDayOfMonth(1), LocalTime.MIN);
|
||
endTime = LocalDateTime.now();
|
||
}
|
||
} else {
|
||
LocalDate now = LocalDate.now();
|
||
startTime = LocalDateTime.of(now.withDayOfMonth(1), LocalTime.MIN);
|
||
endTime = LocalDateTime.now();
|
||
}
|
||
|
||
// 获取已完成的任务
|
||
List<Task> completedTasks = taskMapper.selectList(
|
||
new LambdaQueryWrapper<Task>()
|
||
.eq(Task::getStatus, "completed")
|
||
.ge(Task::getCreateTime, startTime)
|
||
.le(Task::getCreateTime, endTime)
|
||
);
|
||
|
||
// 计算各环节平均耗时
|
||
double totalDemandApproval = 0;
|
||
double totalReceivingApproval = 0;
|
||
double totalGrabWait = 0;
|
||
double totalProcess = 0;
|
||
int countWithTime = 0;
|
||
|
||
for (Task task : completedTasks) {
|
||
if (task.getActualStartTime() != null && task.getActualEndTime() != null) {
|
||
// 任务处理时长
|
||
long processHours = Duration.between(task.getActualStartTime(), task.getActualEndTime()).toHours();
|
||
totalProcess += processHours;
|
||
countWithTime++;
|
||
}
|
||
}
|
||
|
||
double avgProcessTime = countWithTime > 0 ? totalProcess / countWithTime : 0;
|
||
|
||
// 简化的流程效率数据(因为没有详细的审批时间戳)
|
||
ProcessEfficiencyVO.StageTimeStats stageTimeStats = ProcessEfficiencyVO.StageTimeStats.builder()
|
||
.demandApprovalTime(2.5) // 默认值,后续可从审批日志计算
|
||
.receivingApprovalTime(1.8)
|
||
.grabWaitTime(0.5)
|
||
.processTime(avgProcessTime)
|
||
.totalCycleTime(2.5 + 1.8 + 0.5 + avgProcessTime)
|
||
.bottleneckStage("任务处理")
|
||
.bottleneckPercentage(avgProcessTime / (2.5 + 1.8 + 0.5 + avgProcessTime) * 100)
|
||
.build();
|
||
|
||
// 效率指标
|
||
ProcessEfficiencyVO.EfficiencyMetrics metrics = ProcessEfficiencyVO.EfficiencyMetrics.builder()
|
||
.weekDemandApprovalTime(2.3)
|
||
.weekReceivingApprovalTime(1.6)
|
||
.weekGrabWaitTime(0.4)
|
||
.weekProcessTime(avgProcessTime)
|
||
.weekFirstPassRate(85.0)
|
||
.lastWeekDemandApprovalTime(2.8)
|
||
.lastWeekReceivingApprovalTime(2.1)
|
||
.lastWeekGrabWaitTime(0.6)
|
||
.lastWeekProcessTime(avgProcessTime * 1.1)
|
||
.lastWeekFirstPassRate(82.0)
|
||
.monthDemandApprovalTime(2.5)
|
||
.monthReceivingApprovalTime(1.8)
|
||
.monthGrabWaitTime(0.5)
|
||
.monthProcessTime(avgProcessTime)
|
||
.monthFirstPassRate(83.5)
|
||
.totalRejected(0)
|
||
.rejectionRate(0.0)
|
||
.build();
|
||
|
||
// 环节分布
|
||
List<ProcessEfficiencyVO.StageDistribution> distribution = Arrays.asList(
|
||
ProcessEfficiencyVO.StageDistribution.builder()
|
||
.stageName("需求审核")
|
||
.avgTime(2.5)
|
||
.percentage(22.7)
|
||
.build(),
|
||
ProcessEfficiencyVO.StageDistribution.builder()
|
||
.stageName("接单审核")
|
||
.avgTime(1.8)
|
||
.percentage(16.4)
|
||
.build(),
|
||
ProcessEfficiencyVO.StageDistribution.builder()
|
||
.stageName("抢单等待")
|
||
.avgTime(0.5)
|
||
.percentage(4.5)
|
||
.build(),
|
||
ProcessEfficiencyVO.StageDistribution.builder()
|
||
.stageName("任务处理")
|
||
.avgTime(avgProcessTime)
|
||
.percentage(56.4)
|
||
.build()
|
||
);
|
||
|
||
return ProcessEfficiencyVO.builder()
|
||
.stageTimeStats(stageTimeStats)
|
||
.efficiencyMetrics(metrics)
|
||
.approvalTrend(new ArrayList<>())
|
||
.stageDistribution(distribution)
|
||
.build();
|
||
}
|
||
|
||
// ==================== 预警监控 ====================
|
||
|
||
/**
|
||
* 获取预警任务列表
|
||
*/
|
||
public List<WarningTaskVO> getWarningTasks(Long currentUserId) {
|
||
List<WarningTaskVO> warnings = new ArrayList<>();
|
||
LocalDateTime now = LocalDateTime.now();
|
||
|
||
// 获取所有进行中和待处理的任务
|
||
List<Task> activeTasks = taskMapper.selectList(
|
||
new LambdaQueryWrapper<Task>()
|
||
.in(Task::getStatus, Arrays.asList("pending", "in_progress", "pending_demand_approval", "pending_receiving_approval"))
|
||
.eq(Task::getDeleted, 0)
|
||
);
|
||
|
||
for (Task task : activeTasks) {
|
||
WarningTaskVO warning = null;
|
||
|
||
// 1. 已超时
|
||
if ("in_progress".equals(task.getStatus()) && task.getDeadline() != null && now.isAfter(task.getDeadline())) {
|
||
long overdueHours = Duration.between(task.getDeadline(), now).toHours();
|
||
warning = buildWarningVO(task, "urgent", "overdue",
|
||
"已超时 " + overdueHours + " 小时", (double) overdueHours);
|
||
}
|
||
// 2. 即将超时(剩余时间<20%)
|
||
else if ("in_progress".equals(task.getStatus()) && task.getDeadline() != null && task.getEstimatedHours() != null) {
|
||
long remainingHours = Duration.between(now, task.getDeadline()).toHours();
|
||
double threshold = task.getEstimatedHours().doubleValue() * 0.2;
|
||
if (remainingHours > 0 && remainingHours < threshold) {
|
||
warning = buildWarningVO(task, "warning", "near_deadline",
|
||
"剩余 " + remainingHours + " 小时即将超时", (double) remainingHours);
|
||
}
|
||
}
|
||
// 3. 长期未接单(>24小时)
|
||
else if ("pending".equals(task.getStatus()) && task.getCreateTime() != null) {
|
||
long waitingHours = Duration.between(task.getCreateTime(), now).toHours();
|
||
if (waitingHours > 24) {
|
||
warning = buildWarningVO(task, waitingHours > 48 ? "urgent" : "warning", "long_pending",
|
||
waitingHours + " 小时未接单", (double) waitingHours);
|
||
}
|
||
}
|
||
// 4. 审批滞后(>24小时)
|
||
else if (task.getStatus() != null && task.getStatus().startsWith("pending_") && task.getCreateTime() != null) {
|
||
long waitingHours = Duration.between(task.getCreateTime(), now).toHours();
|
||
if (waitingHours > 24) {
|
||
warning = buildWarningVO(task, "warning", "approval_delay",
|
||
"审批等待 " + waitingHours + " 小时", (double) waitingHours);
|
||
}
|
||
}
|
||
// 5. 长期未完成(进行时间>预估工时*2)
|
||
else if ("in_progress".equals(task.getStatus()) && task.getActualStartTime() != null && task.getEstimatedHours() != null) {
|
||
long inProgressHours = Duration.between(task.getActualStartTime(), now).toHours();
|
||
if (inProgressHours > task.getEstimatedHours().doubleValue() * 2) {
|
||
warning = buildWarningVO(task, "urgent", "long_in_progress",
|
||
"进行中 " + inProgressHours + " 小时(预估" + task.getEstimatedHours().intValue() + "h)", (double) inProgressHours);
|
||
}
|
||
}
|
||
|
||
if (warning != null) {
|
||
warnings.add(warning);
|
||
}
|
||
}
|
||
|
||
// 按预警级别排序(紧急在前)
|
||
warnings.sort((a, b) -> {
|
||
int levelCompare = "urgent".equals(a.getWarningLevel()) ? -1 : ("urgent".equals(b.getWarningLevel()) ? 1 : 0);
|
||
if (levelCompare != 0) return levelCompare;
|
||
return b.getHoursInfo().compareTo(a.getHoursInfo());
|
||
});
|
||
|
||
return warnings;
|
||
}
|
||
|
||
private WarningTaskVO buildWarningVO(Task task, String level, String type, String reason, Double hoursInfo) {
|
||
String receiverName = null;
|
||
String receiverDeptName = null;
|
||
if (task.getReceiverId() != null) {
|
||
User receiver = userMapper.selectById(task.getReceiverId());
|
||
if (receiver != null) {
|
||
receiverName = receiver.getRealName() != null ? receiver.getRealName() : receiver.getUsername();
|
||
if (receiver.getDeptId() != null) {
|
||
Department dept = departmentMapper.selectById(receiver.getDeptId());
|
||
if (dept != null) {
|
||
receiverDeptName = dept.getDeptName();
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
String publisherName = null;
|
||
if (task.getPublisherId() != null) {
|
||
User publisher = userMapper.selectById(task.getPublisherId());
|
||
if (publisher != null) {
|
||
publisherName = publisher.getRealName() != null ? publisher.getRealName() : publisher.getUsername();
|
||
}
|
||
}
|
||
|
||
return WarningTaskVO.builder()
|
||
.taskId(task.getId())
|
||
.taskNo(task.getTaskNo())
|
||
.taskTitle(task.getTitle())
|
||
.status(task.getStatus())
|
||
.statusText(getStatusText(task.getStatus()))
|
||
.warningLevel(level)
|
||
.warningType(type)
|
||
.warningReason(reason)
|
||
.receiverId(task.getReceiverId())
|
||
.receiverName(receiverName)
|
||
.receiverDeptName(receiverDeptName)
|
||
.publisherName(publisherName)
|
||
.estimatedHours(task.getEstimatedHours() != null ? task.getEstimatedHours().doubleValue() : null)
|
||
.deadline(task.getDeadline())
|
||
.createTime(task.getCreateTime())
|
||
.startTime(task.getActualStartTime())
|
||
.hoursInfo(hoursInfo)
|
||
.score(task.getScore())
|
||
.build();
|
||
}
|
||
|
||
private String getStatusText(String status) {
|
||
if (status == null) return "";
|
||
switch (status) {
|
||
case "pending": return "待接单";
|
||
case "in_progress": return "进行中";
|
||
case "pending_demand_approval": return "待需求审核";
|
||
case "pending_receiving_approval": return "待接单审核";
|
||
case "completed": return "已完成";
|
||
case "rejected": return "已驳回";
|
||
default: return status;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取延误扣分比例(三级部门继承二级部门的配置)
|
||
* @param deptId 部门ID
|
||
* @param month 月份 yyyy-MM
|
||
* @return 扣分比例(默认1%)
|
||
*/
|
||
private BigDecimal getDeductionRate(Long deptId, String month) {
|
||
if (deptId == null) {
|
||
return BigDecimal.ONE; // 默认1%
|
||
}
|
||
|
||
try {
|
||
BigDecimal rate = findDeptDeductionRate(deptId, month);
|
||
if (rate != null) {
|
||
return rate;
|
||
}
|
||
} catch (Exception e) {
|
||
log.warn("获取扣分比例配置失败: deptId={}, month={}", deptId, month, e);
|
||
}
|
||
|
||
return BigDecimal.ONE; // 默认1%
|
||
}
|
||
|
||
/**
|
||
* 递归查找部门扣分比例(向上查找父部门配置)
|
||
*/
|
||
private BigDecimal findDeptDeductionRate(Long deptId, String month) {
|
||
if (deptId == null || deptId == 0) {
|
||
return null;
|
||
}
|
||
|
||
// 先查找指定月份的配置
|
||
DesignerTargetScore config = designerTargetScoreMapper.selectOne(
|
||
new LambdaQueryWrapper<DesignerTargetScore>()
|
||
.eq(DesignerTargetScore::getDeptId, deptId)
|
||
.isNull(DesignerTargetScore::getUserId)
|
||
.eq(DesignerTargetScore::getEffectiveMonth, month)
|
||
);
|
||
|
||
if (config != null && config.getDeductionRate() != null) {
|
||
return config.getDeductionRate();
|
||
}
|
||
|
||
// 查找通用配置(没有指定月份的)
|
||
config = designerTargetScoreMapper.selectOne(
|
||
new LambdaQueryWrapper<DesignerTargetScore>()
|
||
.eq(DesignerTargetScore::getDeptId, deptId)
|
||
.isNull(DesignerTargetScore::getUserId)
|
||
.isNull(DesignerTargetScore::getEffectiveMonth)
|
||
);
|
||
|
||
if (config != null && config.getDeductionRate() != null) {
|
||
return config.getDeductionRate();
|
||
}
|
||
|
||
// 当前部门没有配置,向上查找父部门
|
||
Department dept = departmentMapper.selectById(deptId);
|
||
if (dept != null && dept.getParentId() != null && dept.getParentId() > 0) {
|
||
return findDeptDeductionRate(dept.getParentId(), month);
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
/**
|
||
* 获取设计师目标积分(三级部门继承二级部门的目标积分)
|
||
* @param userId 用户ID
|
||
* @param deptId 部门ID
|
||
* @param month 月份 yyyy-MM
|
||
* @return 目标积分
|
||
*/
|
||
private BigDecimal getTargetScore(Long userId, Long deptId, String month) {
|
||
try {
|
||
// 先查找用户个人配置
|
||
if (userId != null) {
|
||
DesignerTargetScore config = designerTargetScoreMapper.selectOne(
|
||
new LambdaQueryWrapper<DesignerTargetScore>()
|
||
.eq(DesignerTargetScore::getUserId, userId)
|
||
.eq(DesignerTargetScore::getEffectiveMonth, month)
|
||
);
|
||
|
||
if (config != null && config.getTargetScore() != null) {
|
||
return config.getTargetScore();
|
||
}
|
||
}
|
||
|
||
// 查找部门通用配置(支持向上继承:三级部门继承二级部门的配置)
|
||
if (deptId != null) {
|
||
BigDecimal targetScore = findDeptTargetScore(deptId, month);
|
||
if (targetScore != null && targetScore.compareTo(BigDecimal.ZERO) > 0) {
|
||
return targetScore;
|
||
}
|
||
}
|
||
} catch (Exception e) {
|
||
log.warn("获取目标积分配置失败: userId={}, deptId={}, month={}", userId, deptId, month, e);
|
||
}
|
||
|
||
return BigDecimal.ZERO; // 默认0
|
||
}
|
||
|
||
/**
|
||
* 递归查找部门目标积分(向上查找父部门配置)
|
||
*/
|
||
private BigDecimal findDeptTargetScore(Long deptId, String month) {
|
||
if (deptId == null || deptId == 0) {
|
||
return null;
|
||
}
|
||
|
||
// 先查找当前部门的配置(指定月份)
|
||
DesignerTargetScore config = designerTargetScoreMapper.selectOne(
|
||
new LambdaQueryWrapper<DesignerTargetScore>()
|
||
.eq(DesignerTargetScore::getDeptId, deptId)
|
||
.isNull(DesignerTargetScore::getUserId)
|
||
.eq(DesignerTargetScore::getEffectiveMonth, month)
|
||
);
|
||
|
||
if (config != null && config.getTargetScore() != null) {
|
||
return config.getTargetScore();
|
||
}
|
||
|
||
// 查找当前部门的默认配置(没有指定月份)
|
||
config = designerTargetScoreMapper.selectOne(
|
||
new LambdaQueryWrapper<DesignerTargetScore>()
|
||
.eq(DesignerTargetScore::getDeptId, deptId)
|
||
.isNull(DesignerTargetScore::getUserId)
|
||
.isNull(DesignerTargetScore::getEffectiveMonth)
|
||
);
|
||
|
||
if (config != null && config.getTargetScore() != null) {
|
||
return config.getTargetScore();
|
||
}
|
||
|
||
// 当前部门没有配置,向上查找父部门
|
||
Department dept = departmentMapper.selectById(deptId);
|
||
if (dept != null && dept.getParentId() != null && dept.getParentId() > 0) {
|
||
return findDeptTargetScore(dept.getParentId(), month);
|
||
}
|
||
|
||
return null;
|
||
}
|
||
}
|
||
|