515 lines
20 KiB
Java
515 lines
20 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 lombok.RequiredArgsConstructor;
|
||
import lombok.extern.slf4j.Slf4j;
|
||
import org.springframework.stereotype.Service;
|
||
|
||
import java.math.BigDecimal;
|
||
import java.math.RoundingMode;
|
||
import java.time.LocalDate;
|
||
import java.time.LocalDateTime;
|
||
import java.time.format.DateTimeFormatter;
|
||
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;
|
||
|
||
/**
|
||
* 获取需求统计数据(根据角色返回不同维度的数据)
|
||
* @param currentUserId 当前登录用户ID
|
||
* @param dateRange 时间范围
|
||
* @param deptId 部门ID(可选,用于管理员查看特定部门)
|
||
* @param userId 用户ID(可选,用于管理员查看特定用户)
|
||
*/
|
||
public DemandStatisticsVO getDemandStatistics(Long currentUserId, String dateRange, Long deptId, Long userId) {
|
||
// 获取当前用户角色和部门信息
|
||
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()));
|
||
|
||
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={}",
|
||
currentUserId, isAdmin, isDeptHead, currentUser != null ? currentUser.getDeptId() : null, dateRange, deptId, userId);
|
||
|
||
// 计算时间范围
|
||
LocalDateTime startTime = calculateStartTime(dateRange);
|
||
LocalDateTime 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) {
|
||
Map<String, List<Task>> tasksByType = tasks.stream()
|
||
.filter(t -> t.getDemandTypeId() != null)
|
||
.collect(Collectors.groupingBy(t -> String.valueOf(t.getDemandTypeId())));
|
||
|
||
int totalTasks = tasks.size();
|
||
List<DemandStatisticsVO.TypeDistribution> distribution = new ArrayList<>();
|
||
|
||
for (Map.Entry<String, List<Task>> entry : tasksByType.entrySet()) {
|
||
List<Task> typeTasks = entry.getValue();
|
||
int count = typeTasks.size();
|
||
double percentage = totalTasks > 0 ? (double) count / totalTasks * 100 : 0.0;
|
||
|
||
BigDecimal totalScore = typeTasks.stream()
|
||
.map(Task::getScoreReward)
|
||
.filter(Objects::nonNull)
|
||
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||
|
||
distribution.add(DemandStatisticsVO.TypeDistribution.builder()
|
||
.typeName("类型" + entry.getKey())
|
||
.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;
|
||
}
|
||
}
|
||
}
|
||
|