588 lines
27 KiB
Java
588 lines
27 KiB
Java
package com.designhub.service;
|
|
|
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
|
import com.designhub.entity.Department;
|
|
import com.designhub.entity.Task;
|
|
import com.designhub.entity.TaskProgressSnapshot;
|
|
import com.designhub.entity.User;
|
|
import com.designhub.mapper.DepartmentMapper;
|
|
import com.designhub.mapper.TaskMapper;
|
|
import com.designhub.mapper.TaskProgressSnapshotMapper;
|
|
import com.designhub.mapper.UserMapper;
|
|
import com.designhub.vo.TaskProgressOverviewVO;
|
|
import com.fasterxml.jackson.core.type.TypeReference;
|
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
import lombok.RequiredArgsConstructor;
|
|
import lombok.extern.slf4j.Slf4j;
|
|
import org.springframework.beans.factory.annotation.Value;
|
|
import org.springframework.scheduling.annotation.Scheduled;
|
|
import org.springframework.stereotype.Service;
|
|
import org.springframework.util.CollectionUtils;
|
|
|
|
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.ArrayList;
|
|
import java.util.Comparator;
|
|
import java.util.HashMap;
|
|
import java.util.LinkedHashMap;
|
|
import java.util.LinkedList;
|
|
import java.util.List;
|
|
import java.util.Map;
|
|
import java.util.Objects;
|
|
import java.util.Optional;
|
|
import java.util.Set;
|
|
import java.util.concurrent.atomic.AtomicLong;
|
|
import java.util.stream.Collectors;
|
|
|
|
@Service
|
|
@Slf4j
|
|
@RequiredArgsConstructor
|
|
public class TaskProgressSnapshotService {
|
|
|
|
private final TaskMapper taskMapper;
|
|
private final UserMapper userMapper;
|
|
private final DepartmentMapper departmentMapper;
|
|
private final TaskProgressSnapshotMapper snapshotMapper;
|
|
private final ObjectMapper objectMapper;
|
|
|
|
@Value("${analytics.task-progress.snapshot.enabled:true}")
|
|
private boolean snapshotEnabled;
|
|
|
|
private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd");
|
|
|
|
@Scheduled(cron = "0 0/10 * * * ?")
|
|
public void scheduledSnapshot() {
|
|
if (!snapshotEnabled) {
|
|
return;
|
|
}
|
|
try {
|
|
buildAndPersistSnapshot();
|
|
} catch (Exception ex) {
|
|
log.error("构建任务进度快照失败", ex);
|
|
}
|
|
}
|
|
|
|
public TaskProgressOverviewVO getOverview(String dateRange, String month, Long deptId) {
|
|
TaskProgressOverviewVO overview = loadLatestSnapshot()
|
|
.orElseGet(this::buildAndPersistSnapshot);
|
|
return applyFilters(overview, dateRange, month, deptId);
|
|
}
|
|
|
|
private synchronized TaskProgressOverviewVO buildAndPersistSnapshot() {
|
|
List<Task> tasks = loadTasks();
|
|
Map<Long, User> userMap = loadUsers(tasks);
|
|
Map<Long, Department> deptMap = loadDepartments(tasks);
|
|
|
|
TaskProgressOverviewVO overview = buildOverview(tasks, userMap, deptMap);
|
|
persistSnapshot(overview);
|
|
return overview;
|
|
}
|
|
|
|
private List<Task> loadTasks() {
|
|
LambdaQueryWrapper<Task> wrapper = new LambdaQueryWrapper<Task>()
|
|
.select(
|
|
Task::getId,
|
|
Task::getTaskNo,
|
|
Task::getTitle,
|
|
Task::getStatus,
|
|
Task::getProgress,
|
|
Task::getPriority,
|
|
Task::getScore,
|
|
Task::getPublisherId,
|
|
Task::getPublisherDeptId,
|
|
Task::getReceiverId,
|
|
Task::getReceiverDeptId,
|
|
Task::getCreateTime,
|
|
Task::getActualStartTime,
|
|
Task::getStartTime,
|
|
Task::getSubmitTime,
|
|
Task::getActualEndTime,
|
|
Task::getCompleteTime,
|
|
Task::getDeadline,
|
|
Task::getAuditTime,
|
|
Task::getEstimatedHours,
|
|
Task::getDeleted
|
|
)
|
|
.eq(Task::getDeleted, 0)
|
|
.ne(Task::getStatus, "draft");
|
|
return taskMapper.selectList(wrapper);
|
|
}
|
|
|
|
private Map<Long, User> loadUsers(List<Task> tasks) {
|
|
Set<Long> userIds = tasks.stream()
|
|
.flatMap(task -> java.util.stream.Stream.of(task.getPublisherId(), task.getReceiverId()))
|
|
.filter(Objects::nonNull)
|
|
.collect(Collectors.toSet());
|
|
if (CollectionUtils.isEmpty(userIds)) {
|
|
return new HashMap<>();
|
|
}
|
|
return userMapper.selectBatchIds(userIds).stream()
|
|
.collect(Collectors.toMap(User::getId, user -> user, (a, b) -> a));
|
|
}
|
|
|
|
private Map<Long, Department> loadDepartments(List<Task> tasks) {
|
|
Set<Long> deptIds = tasks.stream()
|
|
.flatMap(task -> java.util.stream.Stream.of(task.getReceiverDeptId(), task.getPublisherDeptId()))
|
|
.filter(Objects::nonNull)
|
|
.collect(Collectors.toSet());
|
|
if (CollectionUtils.isEmpty(deptIds)) {
|
|
return new HashMap<>();
|
|
}
|
|
return departmentMapper.selectBatchIds(deptIds).stream()
|
|
.collect(Collectors.toMap(Department::getId, dept -> dept, (a, b) -> a));
|
|
}
|
|
|
|
private TaskProgressOverviewVO buildOverview(List<Task> tasks, Map<Long, User> userMap, Map<Long, Department> deptMap) {
|
|
LocalDateTime now = LocalDateTime.now();
|
|
|
|
Map<String, Long> statusDistribution = tasks.stream()
|
|
.collect(Collectors.groupingBy(task -> Optional.ofNullable(task.getStatus()).orElse("unknown"), Collectors.counting()));
|
|
|
|
long total = tasks.size();
|
|
long pending = tasks.stream().filter(task -> TaskStatusGroups.PENDING.contains(task.getStatus())).count();
|
|
long inProgress = tasks.stream().filter(task -> TaskStatusGroups.IN_PROGRESS.contains(task.getStatus())).count();
|
|
long review = tasks.stream().filter(task -> TaskStatusGroups.REVIEW.contains(task.getStatus())).count();
|
|
long completed = tasks.stream().filter(task -> TaskStatusGroups.COMPLETED.contains(task.getStatus())).count();
|
|
long overdue = tasks.stream().filter(task -> isOverdue(task, now)).count();
|
|
long onTimeCompleted = tasks.stream().filter(task -> isCompletedOnTime(task)).count();
|
|
|
|
double completionRate = total > 0 ? completed * 100.0 / total : 0d;
|
|
double onTimeRate = completed > 0 ? onTimeCompleted * 100.0 / completed : 0d;
|
|
|
|
double avgCycle = tasks.stream()
|
|
.filter(task -> task.getCompleteTime() != null)
|
|
.mapToDouble(task -> Duration.between(task.getCreateTime(), task.getCompleteTime()).toMinutes() / 60.0)
|
|
.average()
|
|
.orElse(0d);
|
|
|
|
TaskProgressOverviewVO.KpiMetrics metrics = TaskProgressOverviewVO.KpiMetrics.builder()
|
|
.totalTasks(total)
|
|
.pendingTasks(pending)
|
|
.inProgressTasks(inProgress)
|
|
.reviewTasks(review)
|
|
.completedTasks(completed)
|
|
.overdueTasks(overdue)
|
|
.completionRate(round(completionRate))
|
|
.onTimeRate(round(onTimeRate))
|
|
.avgCycleHours(round(avgCycle))
|
|
.build();
|
|
|
|
List<TaskProgressOverviewVO.TrendPoint> trend = buildTrend(tasks);
|
|
List<TaskProgressOverviewVO.StageSegment> stageSegments = buildStageSegments(tasks);
|
|
List<TaskProgressOverviewVO.DepartmentLoad> departmentLoads = buildDepartmentLoads(tasks, deptMap);
|
|
List<TaskProgressOverviewVO.TaskPoint> taskPoints = buildTaskPoints(tasks, userMap, deptMap, now);
|
|
List<TaskProgressOverviewVO.RiskTask> riskTasks = buildRiskTasks(taskPoints, now);
|
|
|
|
metrics.setRiskTaskCount(riskTasks.size());
|
|
|
|
return TaskProgressOverviewVO.builder()
|
|
.snapshotTime(now)
|
|
.metrics(metrics)
|
|
.statusDistribution(statusDistribution.entrySet().stream()
|
|
.sorted(Map.Entry.<String, Long>comparingByValue().reversed())
|
|
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (a, b) -> a, LinkedHashMap::new)))
|
|
.trend(trend)
|
|
.stageSegments(stageSegments)
|
|
.departmentLoads(departmentLoads)
|
|
.riskTasks(riskTasks)
|
|
.taskPoints(taskPoints)
|
|
.build();
|
|
}
|
|
|
|
private List<TaskProgressOverviewVO.TrendPoint> buildTrend(List<Task> tasks) {
|
|
LocalDate today = LocalDate.now();
|
|
LocalDate start = today.minusDays(29);
|
|
Map<String, TaskProgressOverviewVO.TrendPoint> map = new LinkedHashMap<>();
|
|
for (int i = 0; i < 30; i++) {
|
|
LocalDate date = start.plusDays(i);
|
|
map.put(date.format(DATE_FORMATTER), TaskProgressOverviewVO.TrendPoint.builder()
|
|
.date(date.format(DATE_FORMATTER))
|
|
.createdCount(0)
|
|
.completedCount(0)
|
|
.build());
|
|
}
|
|
|
|
for (Task task : tasks) {
|
|
if (task.getCreateTime() != null) {
|
|
LocalDate date = task.getCreateTime().toLocalDate();
|
|
if (!date.isBefore(start)) {
|
|
map.computeIfPresent(date.format(DATE_FORMATTER), (k, point) -> {
|
|
point.setCreatedCount(point.getCreatedCount() + 1);
|
|
return point;
|
|
});
|
|
}
|
|
}
|
|
if (task.getCompleteTime() != null) {
|
|
LocalDate date = task.getCompleteTime().toLocalDate();
|
|
if (!date.isBefore(start)) {
|
|
map.computeIfPresent(date.format(DATE_FORMATTER), (k, point) -> {
|
|
point.setCompletedCount(point.getCompletedCount() + 1);
|
|
return point;
|
|
});
|
|
}
|
|
}
|
|
}
|
|
return new ArrayList<>(map.values());
|
|
}
|
|
|
|
private List<TaskProgressOverviewVO.StageSegment> buildStageSegments(List<Task> tasks) {
|
|
AtomicLong approvalMinutes = new AtomicLong();
|
|
AtomicLong executionMinutes = new AtomicLong();
|
|
AtomicLong reviewMinutes = new AtomicLong();
|
|
AtomicLong approvalCount = new AtomicLong();
|
|
AtomicLong executionCount = new AtomicLong();
|
|
AtomicLong reviewCount = new AtomicLong();
|
|
|
|
tasks.forEach(task -> {
|
|
if (task.getAuditTime() != null) {
|
|
approvalMinutes.addAndGet(Duration.between(task.getCreateTime(), task.getAuditTime()).toMinutes());
|
|
approvalCount.incrementAndGet();
|
|
}
|
|
LocalDateTime start = task.getActualStartTime() != null ? task.getActualStartTime() : task.getStartTime();
|
|
LocalDateTime end = task.getActualEndTime() != null ? task.getActualEndTime() : task.getCompleteTime();
|
|
if (start != null && end != null) {
|
|
executionMinutes.addAndGet(Duration.between(start, end).toMinutes());
|
|
executionCount.incrementAndGet();
|
|
}
|
|
if (task.getSubmitTime() != null && task.getCompleteTime() != null) {
|
|
reviewMinutes.addAndGet(Duration.between(task.getSubmitTime(), task.getCompleteTime()).toMinutes());
|
|
reviewCount.incrementAndGet();
|
|
}
|
|
});
|
|
|
|
double approvalAvg = approvalCount.get() == 0 ? 0d : approvalMinutes.get() / 60.0 / approvalCount.get();
|
|
double executionAvg = executionCount.get() == 0 ? 0d : executionMinutes.get() / 60.0 / executionCount.get();
|
|
double reviewAvg = reviewCount.get() == 0 ? 0d : reviewMinutes.get() / 60.0 / reviewCount.get();
|
|
double total = approvalAvg + executionAvg + reviewAvg;
|
|
|
|
List<TaskProgressOverviewVO.StageSegment> segments = new ArrayList<>();
|
|
segments.add(buildStage("审批环节", approvalAvg, total));
|
|
segments.add(buildStage("制作环节", executionAvg, total));
|
|
segments.add(buildStage("评审环节", reviewAvg, total));
|
|
return segments;
|
|
}
|
|
|
|
private TaskProgressOverviewVO.StageSegment buildStage(String stage, double avg, double total) {
|
|
double percentage = total > 0 ? avg / total * 100 : 0;
|
|
return TaskProgressOverviewVO.StageSegment.builder()
|
|
.stage(stage)
|
|
.avgHours(round(avg))
|
|
.percentage(round(percentage))
|
|
.build();
|
|
}
|
|
|
|
private List<TaskProgressOverviewVO.DepartmentLoad> buildDepartmentLoads(List<Task> tasks, Map<Long, Department> deptMap) {
|
|
Map<Long, TaskProgressOverviewVO.DepartmentLoad> loads = new HashMap<>();
|
|
for (Task task : tasks) {
|
|
Long deptId = task.getReceiverDeptId() != null ? task.getReceiverDeptId() : task.getPublisherDeptId();
|
|
if (deptId == null) {
|
|
deptId = -1L;
|
|
}
|
|
TaskProgressOverviewVO.DepartmentLoad load = loads.computeIfAbsent(deptId, id -> {
|
|
String name = id == -1L ? "未分配" : deptMap.getOrDefault(id, new Department()).getDeptName();
|
|
return TaskProgressOverviewVO.DepartmentLoad.builder()
|
|
.deptId(id)
|
|
.deptName(name != null ? name : "未知部门")
|
|
.build();
|
|
});
|
|
|
|
String status = Optional.ofNullable(task.getStatus()).orElse("unknown");
|
|
if (TaskStatusGroups.PENDING.contains(status)) {
|
|
load.setPendingCount(load.getPendingCount() + 1);
|
|
} else if (TaskStatusGroups.IN_PROGRESS.contains(status)) {
|
|
load.setInProgressCount(load.getInProgressCount() + 1);
|
|
} else if (TaskStatusGroups.REVIEW.contains(status)) {
|
|
load.setReviewCount(load.getReviewCount() + 1);
|
|
} else if (TaskStatusGroups.COMPLETED.contains(status)) {
|
|
load.setCompletedCount(load.getCompletedCount() + 1);
|
|
}
|
|
if (isOverdue(task, LocalDateTime.now())) {
|
|
load.setOverdueCount(load.getOverdueCount() + 1);
|
|
}
|
|
}
|
|
|
|
loads.values().forEach(load -> {
|
|
double workloadIndex = load.getPendingCount() * 0.5
|
|
+ load.getInProgressCount()
|
|
+ load.getReviewCount() * 0.8
|
|
+ load.getOverdueCount() * 1.2;
|
|
load.setWorkloadIndex(round(workloadIndex));
|
|
});
|
|
|
|
return loads.values().stream()
|
|
.sorted(Comparator.comparingDouble(TaskProgressOverviewVO.DepartmentLoad::getWorkloadIndex).reversed())
|
|
.collect(Collectors.toList());
|
|
}
|
|
|
|
private List<TaskProgressOverviewVO.TaskPoint> buildTaskPoints(List<Task> tasks, Map<Long, User> userMap, Map<Long, Department> deptMap, LocalDateTime now) {
|
|
return tasks.stream()
|
|
.sorted(Comparator.comparing(Task::getCreateTime).reversed())
|
|
.limit(200)
|
|
.map(task -> {
|
|
User publisher = task.getPublisherId() != null ? userMap.get(task.getPublisherId()) : null;
|
|
User receiver = task.getReceiverId() != null ? userMap.get(task.getReceiverId()) : null;
|
|
Long deptId = task.getReceiverDeptId() != null ? task.getReceiverDeptId() : task.getPublisherDeptId();
|
|
Department dept = deptId != null ? deptMap.get(deptId) : null;
|
|
return TaskProgressOverviewVO.TaskPoint.builder()
|
|
.taskId(task.getId())
|
|
.deptId(deptId)
|
|
.taskNo(task.getTaskNo())
|
|
.title(task.getTitle())
|
|
.status(task.getStatus())
|
|
.progress(task.getProgress() != null ? task.getProgress() : 0)
|
|
.priority(task.getPriority())
|
|
.score(task.getScore())
|
|
.publisherName(publisher != null ? publisher.getRealName() : "")
|
|
.receiverName(receiver != null ? receiver.getRealName() : "")
|
|
.deptName(dept != null ? dept.getDeptName() : "未分配")
|
|
.createTime(task.getCreateTime())
|
|
.deadline(task.getDeadline())
|
|
.overdue(isOverdue(task, now))
|
|
.nearDeadline(isNearDeadline(task, now))
|
|
.build();
|
|
})
|
|
.collect(Collectors.toList());
|
|
}
|
|
|
|
private List<TaskProgressOverviewVO.RiskTask> buildRiskTasks(List<TaskProgressOverviewVO.TaskPoint> taskPoints, LocalDateTime now) {
|
|
List<TaskProgressOverviewVO.RiskTask> risks = new LinkedList<>();
|
|
for (TaskProgressOverviewVO.TaskPoint point : taskPoints) {
|
|
if (point.isOverdue() || point.isNearDeadline()) {
|
|
TaskProgressOverviewVO.RiskTask risk = TaskProgressOverviewVO.RiskTask.builder()
|
|
.taskId(point.getTaskId())
|
|
.deptId(point.getDeptId())
|
|
.taskNo(point.getTaskNo())
|
|
.title(point.getTitle())
|
|
.status(point.getStatus())
|
|
.deptName(point.getDeptName())
|
|
.ownerName(point.getReceiverName())
|
|
.deadline(point.getDeadline())
|
|
.createTime(point.getCreateTime())
|
|
.warningType(point.isOverdue() ? "overdue" : "near_deadline")
|
|
.warningReason(point.isOverdue() ? "任务已超时" : "任务即将超时")
|
|
.severity(point.isOverdue() ? "danger" : "warning")
|
|
.build();
|
|
risks.add(risk);
|
|
}
|
|
}
|
|
return risks.stream()
|
|
.sorted(Comparator.comparing(TaskProgressOverviewVO.RiskTask::getSeverity)
|
|
.thenComparing(risk -> Optional.ofNullable(risk.getDeadline()).orElse(now)))
|
|
.limit(20)
|
|
.collect(Collectors.toList());
|
|
}
|
|
|
|
private void persistSnapshot(TaskProgressOverviewVO overview) {
|
|
try {
|
|
TaskProgressSnapshot snapshot = new TaskProgressSnapshot();
|
|
snapshot.setSnapshotTime(LocalDateTime.now());
|
|
snapshot.setMetricsJson(objectMapper.writeValueAsString(overview.getMetrics()));
|
|
snapshot.setStatusDistributionJson(objectMapper.writeValueAsString(overview.getStatusDistribution()));
|
|
snapshot.setTrendJson(objectMapper.writeValueAsString(overview.getTrend()));
|
|
snapshot.setStageSegmentsJson(objectMapper.writeValueAsString(overview.getStageSegments()));
|
|
snapshot.setDepartmentLoadJson(objectMapper.writeValueAsString(overview.getDepartmentLoads()));
|
|
snapshot.setRiskTasksJson(objectMapper.writeValueAsString(overview.getRiskTasks()));
|
|
snapshot.setTaskPointsJson(objectMapper.writeValueAsString(overview.getTaskPoints()));
|
|
snapshotMapper.insert(snapshot);
|
|
} catch (Exception ex) {
|
|
log.error("写入任务进度快照失败", ex);
|
|
}
|
|
}
|
|
|
|
private Optional<TaskProgressOverviewVO> loadLatestSnapshot() {
|
|
LambdaQueryWrapper<TaskProgressSnapshot> wrapper = new LambdaQueryWrapper<TaskProgressSnapshot>()
|
|
.orderByDesc(TaskProgressSnapshot::getSnapshotTime)
|
|
.last("LIMIT 1");
|
|
TaskProgressSnapshot snapshot = snapshotMapper.selectOne(wrapper);
|
|
if (snapshot == null) {
|
|
return Optional.empty();
|
|
}
|
|
try {
|
|
return Optional.of(TaskProgressOverviewVO.builder()
|
|
.snapshotTime(snapshot.getSnapshotTime())
|
|
.metrics(read(snapshot.getMetricsJson(), TaskProgressOverviewVO.KpiMetrics.class))
|
|
.statusDistribution(read(snapshot.getStatusDistributionJson(), new TypeReference<Map<String, Long>>() {}))
|
|
.trend(read(snapshot.getTrendJson(), new TypeReference<List<TaskProgressOverviewVO.TrendPoint>>() {}))
|
|
.stageSegments(read(snapshot.getStageSegmentsJson(), new TypeReference<List<TaskProgressOverviewVO.StageSegment>>() {}))
|
|
.departmentLoads(read(snapshot.getDepartmentLoadJson(), new TypeReference<List<TaskProgressOverviewVO.DepartmentLoad>>() {}))
|
|
.riskTasks(read(snapshot.getRiskTasksJson(), new TypeReference<List<TaskProgressOverviewVO.RiskTask>>() {}))
|
|
.taskPoints(read(snapshot.getTaskPointsJson(), new TypeReference<List<TaskProgressOverviewVO.TaskPoint>>() {}))
|
|
.build());
|
|
} catch (Exception ex) {
|
|
log.error("解析任务进度快照失败", ex);
|
|
return Optional.empty();
|
|
}
|
|
}
|
|
|
|
private <T> T read(String json, TypeReference<T> type) throws Exception {
|
|
if (json == null) {
|
|
return null;
|
|
}
|
|
return objectMapper.readValue(json, type);
|
|
}
|
|
|
|
private <T> T read(String json, Class<T> type) throws Exception {
|
|
if (json == null) {
|
|
return null;
|
|
}
|
|
return objectMapper.readValue(json, type);
|
|
}
|
|
|
|
private TaskProgressOverviewVO applyFilters(TaskProgressOverviewVO overview, String dateRange, String month, Long deptId) {
|
|
if (overview == null) {
|
|
return null;
|
|
}
|
|
LocalDateTime start = resolveStart(dateRange, month);
|
|
LocalDateTime end = resolveEnd(month);
|
|
|
|
List<TaskProgressOverviewVO.TrendPoint> filteredTrend = Optional.ofNullable(overview.getTrend())
|
|
.map(ArrayList::new)
|
|
.orElseGet(ArrayList::new);
|
|
if (start != null) {
|
|
filteredTrend = filteredTrend.stream()
|
|
.filter(point -> {
|
|
LocalDate date = LocalDate.parse(point.getDate(), DATE_FORMATTER);
|
|
return !date.atStartOfDay().isBefore(start);
|
|
})
|
|
.collect(Collectors.toList());
|
|
}
|
|
|
|
List<TaskProgressOverviewVO.TaskPoint> filteredTaskPoints = Optional.ofNullable(overview.getTaskPoints())
|
|
.map(ArrayList::new)
|
|
.orElseGet(ArrayList::new);
|
|
if (start != null) {
|
|
filteredTaskPoints = filteredTaskPoints.stream()
|
|
.filter(point -> point.getCreateTime() == null || !point.getCreateTime().isBefore(start))
|
|
.collect(Collectors.toList());
|
|
}
|
|
if (end != null) {
|
|
filteredTaskPoints = filteredTaskPoints.stream()
|
|
.filter(point -> point.getCreateTime() == null || !point.getCreateTime().isAfter(end))
|
|
.collect(Collectors.toList());
|
|
}
|
|
|
|
TaskProgressOverviewVO.KpiMetrics metrics = overview.getMetrics();
|
|
List<TaskProgressOverviewVO.DepartmentLoad> deptLoads = Optional.ofNullable(overview.getDepartmentLoads())
|
|
.map(ArrayList::new)
|
|
.orElseGet(ArrayList::new);
|
|
List<TaskProgressOverviewVO.RiskTask> filteredRisks = Optional.ofNullable(overview.getRiskTasks())
|
|
.map(ArrayList::new)
|
|
.orElseGet(ArrayList::new);
|
|
if (deptId != null) {
|
|
deptLoads = deptLoads.stream()
|
|
.filter(load -> load.getDeptId() != null && load.getDeptId().equals(deptId))
|
|
.collect(Collectors.toList());
|
|
if (!deptLoads.isEmpty()) {
|
|
TaskProgressOverviewVO.DepartmentLoad load = deptLoads.get(0);
|
|
long totalTasks = load.getPendingCount() + load.getInProgressCount() + load.getReviewCount() + load.getCompletedCount();
|
|
double completionRate = totalTasks == 0 ? 0 : round((double) load.getCompletedCount() * 100 / totalTasks);
|
|
metrics = TaskProgressOverviewVO.KpiMetrics.builder()
|
|
.totalTasks(totalTasks)
|
|
.pendingTasks(load.getPendingCount())
|
|
.inProgressTasks(load.getInProgressCount())
|
|
.reviewTasks(load.getReviewCount())
|
|
.completedTasks(load.getCompletedCount())
|
|
.overdueTasks(load.getOverdueCount())
|
|
.completionRate(completionRate)
|
|
.onTimeRate(overview.getMetrics().getOnTimeRate())
|
|
.avgCycleHours(overview.getMetrics().getAvgCycleHours())
|
|
.riskTaskCount(filteredRisks.stream()
|
|
.filter(risk -> deptId.equals(risk.getDeptId()))
|
|
.count())
|
|
.build();
|
|
}
|
|
Long targetDeptId = deptId;
|
|
filteredTaskPoints = filteredTaskPoints.stream()
|
|
.filter(point -> Objects.equals(point.getDeptId(), targetDeptId))
|
|
.collect(Collectors.toList());
|
|
filteredRisks = filteredRisks.stream()
|
|
.filter(risk -> Objects.equals(risk.getDeptId(), targetDeptId))
|
|
.collect(Collectors.toList());
|
|
}
|
|
if (metrics != null) {
|
|
metrics.setRiskTaskCount(filteredRisks != null ? filteredRisks.size() : 0);
|
|
}
|
|
|
|
return TaskProgressOverviewVO.builder()
|
|
.snapshotTime(overview.getSnapshotTime())
|
|
.metrics(metrics)
|
|
.statusDistribution(overview.getStatusDistribution())
|
|
.trend(filteredTrend)
|
|
.stageSegments(overview.getStageSegments())
|
|
.departmentLoads(deptLoads)
|
|
.riskTasks(filteredRisks)
|
|
.taskPoints(filteredTaskPoints)
|
|
.build();
|
|
}
|
|
|
|
private LocalDateTime resolveStart(String dateRange, String month) {
|
|
if (month != null && !month.isEmpty()) {
|
|
LocalDate date = LocalDate.parse(month + "-01", DateTimeFormatter.ofPattern("yyyy-MM-dd"));
|
|
return date.atStartOfDay();
|
|
}
|
|
LocalDateTime now = LocalDateTime.now();
|
|
if (dateRange == null) {
|
|
return null;
|
|
}
|
|
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 LocalDateTime resolveEnd(String month) {
|
|
if (month == null || month.isEmpty()) {
|
|
return null;
|
|
}
|
|
LocalDate date = LocalDate.parse(month + "-01", DateTimeFormatter.ofPattern("yyyy-MM-dd"));
|
|
return date.with(TemporalAdjusters.lastDayOfMonth()).atTime(LocalTime.MAX);
|
|
}
|
|
|
|
private boolean isOverdue(Task task, LocalDateTime now) {
|
|
return task.getDeadline() != null && task.getDeadline().isBefore(now)
|
|
&& !TaskStatusGroups.COMPLETED.contains(task.getStatus());
|
|
}
|
|
|
|
private boolean isNearDeadline(Task task, LocalDateTime now) {
|
|
return task.getDeadline() != null
|
|
&& !TaskStatusGroups.COMPLETED.contains(task.getStatus())
|
|
&& Duration.between(now, task.getDeadline()).toHours() <= 12
|
|
&& Duration.between(now, task.getDeadline()).toMinutes() >= 0;
|
|
}
|
|
|
|
private boolean isCompletedOnTime(Task task) {
|
|
if (!TaskStatusGroups.COMPLETED.contains(task.getStatus())) {
|
|
return false;
|
|
}
|
|
if (task.getDeadline() == null || task.getCompleteTime() == null) {
|
|
return true;
|
|
}
|
|
return !task.getCompleteTime().isAfter(task.getDeadline());
|
|
}
|
|
|
|
private double round(double value) {
|
|
return BigDecimal.valueOf(value).setScale(2, RoundingMode.HALF_UP).doubleValue();
|
|
}
|
|
|
|
private static class TaskStatusGroups {
|
|
private static final Set<String> PENDING = Set.of("pending", "pending_audit", "pending_demand_approval", "pending_receiving_approval");
|
|
private static final Set<String> IN_PROGRESS = Set.of("in_progress");
|
|
private static final Set<String> REVIEW = Set.of("review", "submitted");
|
|
private static final Set<String> COMPLETED = Set.of("completed");
|
|
}
|
|
}
|