zjk
2026-09-29 54dcf0a82e7490fa58b1c056d5c0b9cb739c2931
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
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 mapperUserTypeEnum = resolveUserTypeEnum(normalizedType);
        LocalDateTime startTime = range.getStart().atStartOfDay();
        LocalDateTime endTime = LocalDateTime.of(range.getEnd(), LocalTime.MAX);
 
        UserStatisticsOverviewRow overviewRow = userStatisticsMapper.selectOverview(mapperUserTypeEnum, startTime, endTime);
        List<UserStatisticsRegisterTrendRow> registerRows = userStatisticsMapper.selectRegisterTrend(mapperUserTypeEnum, startTime, endTime);
        List<UserStatisticsActiveTrendRow> activeRows = userStatisticsMapper.selectActiveTrend(mapperUserTypeEnum, startTime, endTime);
        UserStatisticsCompositionRow compositionRow = userStatisticsMapper.selectMemberComposition(mapperUserTypeEnum, 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<UserStatisticsTrendItemVO> buildTrend(List<UserStatisticsRegisterTrendRow> registerRows,
                                                       List<UserStatisticsActiveTrendRow> activeRows,
                                                       DateRange range) {
        Map<LocalDate, UserStatisticsRegisterTrendRow> registerRowMap = new HashMap<LocalDate, UserStatisticsRegisterTrendRow>();
        for (UserStatisticsRegisterTrendRow row : registerRows) {
            registerRowMap.put(row.getStatDate(), row);
        }
 
        Map<LocalDate, UserStatisticsActiveTrendRow> activeRowMap = new HashMap<LocalDate, UserStatisticsActiveTrendRow>();
        for (UserStatisticsActiveTrendRow row : activeRows) {
            activeRowMap.put(row.getStatDate(), row);
        }
 
        List<UserStatisticsTrendItemVO> result = new ArrayList<UserStatisticsTrendItemVO>();
        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 resolveUserTypeEnum(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;
        }
    }
}