package com.tcm.training.admin.statistics.service; import com.tcm.training.admin.statistics.mapper.UserStatisticsMapper; import com.tcm.training.admin.statistics.row.UserStatisticsActiveTrendRow; import com.tcm.training.admin.statistics.row.UserStatisticsCompositionRow; import com.tcm.training.admin.statistics.row.UserStatisticsOverviewRow; import com.tcm.training.admin.statistics.row.UserStatisticsRegisterTrendRow; import com.tcm.training.admin.statistics.vo.UserStatisticsMemberCompositionVO; import com.tcm.training.admin.statistics.vo.UserStatisticsOverviewVO; import com.tcm.training.admin.statistics.vo.UserStatisticsTrendItemVO; import com.tcm.training.admin.statistics.vo.UserStatisticsVO; import org.springframework.stereotype.Service; import java.math.BigDecimal; import java.math.RoundingMode; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.LocalTime; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; @Service public class UserStatisticsAppService { private static final String FILTER_ALL = "ALL"; private static final String FILTER_NORMAL = "NORMAL"; private static final String FILTER_MEMBER = "MEMBER"; private final UserStatisticsMapper userStatisticsMapper; public UserStatisticsAppService(UserStatisticsMapper userStatisticsMapper) { this.userStatisticsMapper = userStatisticsMapper; } public UserStatisticsVO statistics(String type, LocalDate dateFrom, LocalDate dateTo) { DateRange range = normalizeRange(dateFrom, dateTo); String normalizedType = normalizeType(type); String mapperUserType = resolveUserType(normalizedType); LocalDateTime startTime = range.getStart().atStartOfDay(); LocalDateTime endTime = LocalDateTime.of(range.getEnd(), LocalTime.MAX); UserStatisticsOverviewRow overviewRow = userStatisticsMapper.selectOverview(mapperUserType, startTime, endTime); List registerRows = userStatisticsMapper.selectRegisterTrend(mapperUserType, startTime, endTime); List activeRows = userStatisticsMapper.selectActiveTrend(mapperUserType, startTime, endTime); UserStatisticsCompositionRow compositionRow = userStatisticsMapper.selectMemberComposition(mapperUserType, startTime, endTime); UserStatisticsVO result = new UserStatisticsVO(); result.setOverview(buildOverview(overviewRow)); result.setTrend(buildTrend(registerRows, activeRows, range)); result.setMemberComposition(buildComposition(compositionRow)); return result; } private UserStatisticsOverviewVO buildOverview(UserStatisticsOverviewRow row) { UserStatisticsOverviewVO overview = new UserStatisticsOverviewVO(); overview.setRegisteredUserCount(nvl(row == null ? null : row.getRegisteredUserCount())); overview.setDoctorRegisteredCount(nvl(row == null ? null : row.getDoctorRegisteredCount())); overview.setMemberOpenCount(nvl(row == null ? null : row.getMemberOpenCount())); overview.setCoursePurchaseCount(nvl(row == null ? null : row.getCoursePurchaseCount())); overview.setLiveWatchClickCount(nvl(row == null ? null : row.getLiveWatchClickCount())); overview.setUserActivityRate(buildActivityRate( nvl(row == null ? null : row.getActiveEnterUserCount()), nvl(row == null ? null : row.getTotalUserCount()) )); return overview; } private UserStatisticsMemberCompositionVO buildComposition(UserStatisticsCompositionRow row) { UserStatisticsMemberCompositionVO composition = new UserStatisticsMemberCompositionVO(); composition.setMonthMemberPurchaseCount(nvl(row == null ? null : row.getMonthMemberPurchaseCount())); composition.setQuarterMemberPurchaseCount(nvl(row == null ? null : row.getQuarterMemberPurchaseCount())); composition.setCourseMemberUserCount(nvl(row == null ? null : row.getCourseMemberUserCount())); return composition; } private List buildTrend(List registerRows, List activeRows, DateRange range) { Map registerRowMap = new HashMap(); for (UserStatisticsRegisterTrendRow row : registerRows) { registerRowMap.put(row.getStatDate(), row); } Map activeRowMap = new HashMap(); for (UserStatisticsActiveTrendRow row : activeRows) { activeRowMap.put(row.getStatDate(), row); } List result = new ArrayList(); LocalDate cursor = range.getStart(); while (!cursor.isAfter(range.getEnd())) { UserStatisticsRegisterTrendRow registerRow = registerRowMap.get(cursor); UserStatisticsActiveTrendRow activeRow = activeRowMap.get(cursor); UserStatisticsTrendItemVO item = new UserStatisticsTrendItemVO(); item.setDate(cursor.toString()); item.setRegisteredUserCount(nvl(registerRow == null ? null : registerRow.getRegisteredUserCount())); item.setDoctorRegisteredCount(nvl(registerRow == null ? null : registerRow.getDoctorRegisteredCount())); item.setActiveEnterUserCount(nvl(activeRow == null ? null : activeRow.getActiveEnterUserCount())); result.add(item); cursor = cursor.plusDays(1); } return result; } private BigDecimal buildActivityRate(Long activeEnterUserCount, Long totalUserCount) { if (totalUserCount == null || totalUserCount <= 0) { return BigDecimal.ZERO; } return BigDecimal.valueOf(activeEnterUserCount == null ? 0L : activeEnterUserCount) .multiply(BigDecimal.valueOf(100)) .divide(BigDecimal.valueOf(totalUserCount), 2, RoundingMode.HALF_UP); } private String normalizeType(String type) { String normalized = trimToNull(type); if (normalized == null) { return FILTER_ALL; } if (FILTER_ALL.equals(normalized) || FILTER_NORMAL.equals(normalized) || FILTER_MEMBER.equals(normalized)) { return normalized; } throw new IllegalArgumentException("用户类型不合法"); } private String resolveUserType(String normalizedType) { if (FILTER_ALL.equals(normalizedType)) { return null; } return normalizedType; } private DateRange normalizeRange(LocalDate dateFrom, LocalDate dateTo) { LocalDate start = dateFrom; LocalDate end = dateTo; if (start == null && end == null) { start = LocalDate.now(); end = start; } else if (start == null) { start = end; } else if (end == null) { end = start; } if (start.isAfter(end)) { throw new IllegalArgumentException("开始日期不能晚于结束日期"); } return new DateRange(start, end); } private Long nvl(Long value) { return value == null ? 0L : value; } private String trimToNull(String value) { if (value == null) { return null; } String trimmed = value.trim(); return trimmed.isEmpty() ? null : trimmed; } private static class DateRange { private final LocalDate start; private final LocalDate end; private DateRange(LocalDate start, LocalDate end) { this.start = start; this.end = end; } private LocalDate getStart() { return start; } private LocalDate getEnd() { return end; } } }