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 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 queryWrapper = new LambdaQueryWrapper() .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 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 buildUserStats(List tasks) { Map> tasksByUser = tasks.stream() .collect(Collectors.groupingBy(Task::getPublisherId)); List stats = new ArrayList<>(); for (Map.Entry> entry : tasksByUser.entrySet()) { Long userId = entry.getKey(); List 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 buildDeptStats(List tasks) { Map> tasksByDept = tasks.stream() .filter(t -> t.getPublisherDeptId() != null) .collect(Collectors.groupingBy(Task::getPublisherDeptId)); List stats = new ArrayList<>(); for (Map.Entry> entry : tasksByDept.entrySet()) { Long deptId = entry.getKey(); List deptTasks = entry.getValue(); Department dept = departmentMapper.selectById(deptId); if (dept == null) continue; TeamScorePool scorePool = teamScorePoolMapper.selectOne( new LambdaQueryWrapper().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 buildTrendData(List tasks, String dateRange) { Map> 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 trendData = new ArrayList<>(); for (Map.Entry> entry : tasksByDate.entrySet()) { List 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 buildTypeDistribution(List tasks) { Map> tasksByType = tasks.stream() .filter(t -> t.getDemandTypeId() != null) .collect(Collectors.groupingBy(t -> String.valueOf(t.getDemandTypeId()))); int totalTasks = tasks.size(); List distribution = new ArrayList<>(); for (Map.Entry> entry : tasksByType.entrySet()) { List 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 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 tasks) { try { List 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() .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 tasks) { List 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 tasks) { try { List completedTaskIds = tasks.stream() .filter(t -> "completed".equals(t.getStatus())) .map(Task::getId) .collect(Collectors.toList()); if (completedTaskIds.isEmpty()) { return 0.0; } List reviews = taskReviewMapper.selectList( new LambdaQueryWrapper() .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; } } }