package com.sinata.appointment.service; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.sinata.appointment.constants.ApprovalInstanceStatus; import com.sinata.appointment.constants.MiniUserType; import com.sinata.appointment.dto.DashboardAppointmentStatsVo; import com.sinata.appointment.dto.DashboardDailyTotalVo; import com.sinata.appointment.dto.DashboardOverviewVo; import com.sinata.appointment.dto.DashboardTrendPointVo; import com.sinata.appointment.entity.AppointmentOrder; import com.sinata.appointment.entity.ApprovalInstance; import com.sinata.appointment.entity.MpUser; import com.sinata.appointment.entity.SysDept; import com.sinata.appointment.mapper.AppointmentOrderMapper; import com.sinata.appointment.mapper.ApprovalInstanceMapper; import com.sinata.appointment.mapper.MpUserMapper; import com.sinata.appointment.mapper.SysDeptMapper; import java.time.DayOfWeek; 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.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @Service @RequiredArgsConstructor public class DashboardApplicationService { private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ISO_LOCAL_DATE; private static final DateTimeFormatter LABEL_FORMATTER = DateTimeFormatter.ofPattern("M-d"); private final AppointmentOrderMapper appointmentOrderMapper; private final ApprovalInstanceMapper approvalInstanceMapper; private final MpUserMapper mpUserMapper; private final SysDeptMapper sysDeptMapper; public DashboardOverviewVo getOverview(LocalDate startDate, LocalDate endDate) { DateRange range = resolveRange(startDate, endDate); LocalDate dailyDate = resolveDailyDate(range); DashboardOverviewVo vo = new DashboardOverviewVo(); vo.setStartDate(format(range.startDate())); vo.setEndDate(format(range.endDate())); vo.setDailyDate(format(dailyDate)); vo.setDailyAppointmentCount(countAppointments(dailyDate, dailyDate, null)); vo.setTotalAppointmentCount(countAppointments(range.startDate(), range.endDate(), null)); vo.setRegisteredUserCount(countRegisteredUsers()); vo.setEmployeeCount(countEmployees()); return vo; } public DashboardAppointmentStatsVo getAppointmentStats( LocalDate startDate, LocalDate endDate, Long visitedDeptId) { DateRange range = resolveRange(startDate, endDate); DashboardAppointmentStatsVo vo = new DashboardAppointmentStatsVo(); vo.setStartDate(format(range.startDate())); vo.setEndDate(format(range.endDate())); vo.setVisitedDeptId(visitedDeptId); vo.setAppointmentCount(countAppointments(range.startDate(), range.endDate(), visitedDeptId)); vo.setInProgressCount( countApprovalInstancesByCreateTime( range.startDate(), range.endDate(), visitedDeptId, ApprovalInstanceStatus.IN_PROGRESS)); vo.setRejectedCount( countApprovalInstancesByCreateTime( range.startDate(), range.endDate(), visitedDeptId, ApprovalInstanceStatus.REJECTED)); vo.setCancelledCount( countApprovalInstancesByCreateTime( range.startDate(), range.endDate(), visitedDeptId, ApprovalInstanceStatus.CANCELLED)); vo.setTrend(buildTrend(range, visitedDeptId)); return vo; } private List buildTrend(DateRange range, Long visitedDeptId) { List deptIds = resolveDeptIds(visitedDeptId); List orderRows = appointmentOrderMapper.listDashboardTrend(range.startDate(), range.endDate(), deptIds); Map orderRowMap = new LinkedHashMap<>(); if (orderRows != null) { for (DashboardTrendPointVo row : orderRows) { if (row == null || row.getStatDate() == null) { continue; } orderRowMap.put(row.getStatDate(), row); } } Map inProgressMap = buildInProgressTrendMap(range, deptIds); Map rejectedMap = buildFinishedTrendMap(range, deptIds, ApprovalInstanceStatus.REJECTED); Map cancelledMap = buildFinishedTrendMap(range, deptIds, ApprovalInstanceStatus.CANCELLED); List result = new ArrayList<>(); LocalDate cursor = range.startDate(); while (!cursor.isAfter(range.endDate())) { DashboardTrendPointVo row = orderRowMap.get(cursor); DashboardTrendPointVo point = new DashboardTrendPointVo(); point.setStatDate(cursor); point.setLabel(cursor.format(LABEL_FORMATTER)); point.setAppointmentCount(row == null ? 0L : defaultLong(row.getAppointmentCount())); point.setInProgressCount(inProgressMap.getOrDefault(cursor, 0L)); point.setRejectedCount(rejectedMap.getOrDefault(cursor, 0L)); point.setCancelledCount(cancelledMap.getOrDefault(cursor, 0L)); result.add(point); cursor = cursor.plusDays(1); } return result; } private long countAppointments(LocalDate startDate, LocalDate endDate, Long visitedDeptId) { return appointmentOrderMapper.selectCount( appointmentRangeWrapper(startDate, endDate, resolveDeptIds(visitedDeptId), null)); } private LambdaQueryWrapper appointmentRangeWrapper( LocalDate startDate, LocalDate endDate, List deptIds, String status) { LambdaQueryWrapper wrapper = new LambdaQueryWrapper() .eq(AppointmentOrder::getDelFlag, 0) .ge(AppointmentOrder::getCreateTime, atStartOfDay(startDate)) .lt(AppointmentOrder::getCreateTime, atStartOfNextDay(endDate)); if (deptIds != null && !deptIds.isEmpty()) { wrapper.in(AppointmentOrder::getVisitedDeptId, deptIds); } if (status != null && !status.isBlank()) { wrapper.eq(AppointmentOrder::getStatus, status); } return wrapper; } private long countApprovalInstancesByCreateTime( LocalDate startDate, LocalDate endDate, Long visitedDeptId, String status) { Long count = approvalInstanceMapper.countDashboardByCreateTime( startDate, endDate, resolveDeptIds(visitedDeptId), status); return defaultLong(count); } private Map buildFinishedTrendMap( DateRange range, List deptIds, String status) { List rows = approvalInstanceMapper.listDashboardFinishedTrend( range.startDate(), range.endDate(), deptIds, status); Map result = new HashMap<>(); if (rows == null) { return result; } for (DashboardDailyTotalVo row : rows) { if (row == null || row.getStatDate() == null) { continue; } result.put(row.getStatDate(), defaultLong(row.getTotalCount())); } return result; } private Map buildInProgressTrendMap(DateRange range, List deptIds) { List rows = approvalInstanceMapper.listDashboardInProgressInstances( range.startDate(), range.endDate(), deptIds); Map result = new HashMap<>(); if (rows == null || rows.isEmpty()) { return result; } for (ApprovalInstance row : rows) { if (row.getCreateTime() == null) { continue; } LocalDate start = row.getCreateTime().toLocalDate(); LocalDate end = row.getFinishedAt() == null ? range.endDate() : row.getFinishedAt().toLocalDate(); if (end.isBefore(range.startDate()) || start.isAfter(range.endDate())) { continue; } LocalDate cursor = start.isBefore(range.startDate()) ? range.startDate() : start; LocalDate limit = end.isAfter(range.endDate()) ? range.endDate() : end; while (!cursor.isAfter(limit)) { result.merge(cursor, 1L, Long::sum); cursor = cursor.plusDays(1); } } return result; } private long countRegisteredUsers() { return mpUserMapper.selectCount(new LambdaQueryWrapper().eq(MpUser::getDelFlag, 0)); } private long countEmployees() { return mpUserMapper.selectCount( new LambdaQueryWrapper() .eq(MpUser::getDelFlag, 0) .eq(MpUser::getUserType, MiniUserType.EMPLOYEE)); } private DateRange resolveRange(LocalDate startDate, LocalDate endDate) { LocalDate today = LocalDate.now(); LocalDate defaultStart = today.with(TemporalAdjusters.previousOrSame(DayOfWeek.MONDAY)); LocalDate defaultEnd = today.with(TemporalAdjusters.nextOrSame(DayOfWeek.SUNDAY)); LocalDate resolvedStart = startDate == null ? defaultStart : startDate; LocalDate resolvedEnd = endDate == null ? defaultEnd : endDate; if (resolvedStart.isAfter(resolvedEnd)) { return new DateRange(resolvedEnd, resolvedStart); } return new DateRange(resolvedStart, resolvedEnd); } private LocalDate resolveDailyDate(DateRange range) { LocalDate today = LocalDate.now(); if (!today.isBefore(range.startDate()) && !today.isAfter(range.endDate())) { return today; } return range.endDate(); } private LocalDateTime atStartOfDay(LocalDate date) { return date.atStartOfDay(); } private LocalDateTime atStartOfNextDay(LocalDate date) { return LocalDateTime.of(date.plusDays(1), LocalTime.MIN); } private String format(LocalDate date) { return date == null ? null : date.format(DATE_FORMATTER); } private List resolveDeptIds(Long visitedDeptId) { if (visitedDeptId == null) { return List.of(); } List depts = sysDeptMapper.selectList( new LambdaQueryWrapper() .eq(SysDept::getDelFlag, 0) .orderByAsc(SysDept::getId)); if (depts == null || depts.isEmpty()) { return List.of(visitedDeptId); } Map> childrenMap = new HashMap<>(); Set existingIds = new HashSet<>(); for (SysDept dept : depts) { if (dept.getId() == null) { continue; } existingIds.add(dept.getId()); Long parentId = dept.getParentId() == null ? 0L : dept.getParentId(); childrenMap.computeIfAbsent(parentId, key -> new ArrayList<>()).add(dept.getId()); } if (!existingIds.contains(visitedDeptId)) { return List.of(visitedDeptId); } LinkedHashSet result = new LinkedHashSet<>(); ArrayList stack = new ArrayList<>(); stack.add(visitedDeptId); while (!stack.isEmpty()) { Long current = stack.remove(stack.size() - 1); if (current == null || !result.add(current)) { continue; } for (Long childId : childrenMap.getOrDefault(current, List.of())) { if (Objects.nonNull(childId)) { stack.add(childId); } } } return new ArrayList<>(result); } private long defaultLong(Long value) { return value == null ? 0L : value; } private record DateRange(LocalDate startDate, LocalDate endDate) {} }