新增e2e测试脚本,修复部分问题

This commit was merged in pull request #51.
This commit is contained in:
2026-07-22 20:00:13 +08:00
parent 53d1ce6fb2
commit 4c07ec5455
105 changed files with 21700 additions and 318 deletions
@@ -190,7 +190,7 @@ public class DataStatisticsDao {
SELECT COUNT(*) FROM sys_user u
INNER JOIN user_role ur ON u.id = ur.user_id
INNER JOIN sys_role sr ON ur.role_id = sr.id
WHERE sr.role_name = '教练' AND u.deleted_at IS NULL
WHERE sr.role_key = 'coach' AND u.deleted_at IS NULL
""")
.map(row -> row.get(0, Long.class))
.one();
@@ -245,4 +245,111 @@ public class DataStatisticsDao {
.map(row -> row.get(0, Long.class))
.one();
}
// ========== 教练业绩统计 ==========
/**
* 获取所有教练基本信息(ID、昵称、用户名)
*/
public reactor.core.publisher.Flux<java.util.Map<String, Object>> getAllCoachesWithInfo() {
return databaseClient.sql("""
SELECT u.id, u.nickname, u.username
FROM sys_user u
INNER JOIN user_role ur ON u.id = ur.user_id
INNER JOIN sys_role sr ON ur.role_id = sr.id
WHERE sr.role_key = 'coach' AND u.deleted_at IS NULL
ORDER BY u.id
""")
.fetch()
.all();
}
/**
* 按教练统计已完成课程数(status=2或6)
*/
public reactor.core.publisher.Flux<java.util.Map<String, Object>> countCompletedCoursesByCoach(LocalDateTime startTime, LocalDateTime endTime) {
return databaseClient.sql("""
SELECT coach_id, COUNT(*) AS count
FROM group_course
WHERE start_time >= :startTime AND start_time < :endTime
AND status IN ('2', '6') AND deleted_at IS NULL
GROUP BY coach_id
""")
.bind("startTime", startTime)
.bind("endTime", endTime)
.fetch()
.all();
}
/**
* 按教练统计出席人次(通过团课关联,booking.status='2'
*/
public reactor.core.publisher.Flux<java.util.Map<String, Object>> countAttendedStudentsByCoach(LocalDateTime startTime, LocalDateTime endTime) {
return databaseClient.sql("""
SELECT gc.coach_id, COUNT(*) AS count
FROM group_course_booking b
INNER JOIN group_course gc ON b.course_id = gc.id
WHERE b.status = '2' AND b.deleted_at IS NULL
AND gc.deleted_at IS NULL
AND gc.start_time >= :startTime AND gc.start_time < :endTime
GROUP BY gc.coach_id
""")
.bind("startTime", startTime)
.bind("endTime", endTime)
.fetch()
.all();
}
/**
* 按教练统计总非取消预约数(用于计算出勤率分母)
*/
public reactor.core.publisher.Flux<java.util.Map<String, Object>> countTotalBookingsByCoach(LocalDateTime startTime, LocalDateTime endTime) {
return databaseClient.sql("""
SELECT gc.coach_id, COUNT(*) AS count
FROM group_course_booking b
INNER JOIN group_course gc ON b.course_id = gc.id
WHERE b.status != '1' AND b.deleted_at IS NULL
AND gc.deleted_at IS NULL
AND gc.start_time >= :startTime AND gc.start_time < :endTime
GROUP BY gc.coach_id
""")
.bind("startTime", startTime)
.bind("endTime", endTime)
.fetch()
.all();
}
/**
* 按教练获取满员率明细(每个已完成课程的出席人数和最大容量)
*/
public reactor.core.publisher.Flux<java.util.Map<String, Object>> getFillRateDetailByCoach(LocalDateTime startTime, LocalDateTime endTime) {
return databaseClient.sql("""
SELECT gc.coach_id, gc.max_members, COUNT(b.id) AS attended
FROM group_course gc
LEFT JOIN group_course_booking b ON gc.id = b.course_id AND b.status = '2' AND b.deleted_at IS NULL
WHERE gc.start_time >= :startTime AND gc.start_time < :endTime
AND gc.status IN ('2', '6') AND gc.deleted_at IS NULL
GROUP BY gc.coach_id, gc.id, gc.max_members
""")
.bind("startTime", startTime)
.bind("endTime", endTime)
.fetch()
.all();
}
/**
* 按教练统计违规次数
*/
public reactor.core.publisher.Flux<java.util.Map<String, Object>> countViolationsByCoach(LocalDateTime startTime, LocalDateTime endTime) {
return databaseClient.sql("""
SELECT coach_id, COUNT(*) AS count
FROM coach_violation
WHERE violation_time >= :startTime AND violation_time < :endTime AND deleted_at IS NULL
GROUP BY coach_id
""")
.bind("startTime", startTime)
.bind("endTime", endTime)
.fetch()
.all();
}
}
@@ -0,0 +1,49 @@
package cn.novalon.gym.manage.datacount.domain;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 教练个人业绩
*
* @author system
* @date 2026-07-22
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class CoachPerformance {
/** 教练ID */
private Long coachId;
/** 教练昵称 */
private String coachName;
/** 教练头像 */
private String avatar;
/** 授课量(已完成课程数) */
private Long completedCourses;
/** 出席人次 */
private Long attendedStudents;
/** 总非取消预约数 */
private Long totalBookings;
/** 出勤率(百分比) */
private Double attendanceRate;
/** 满员率(百分比) */
private Double fillRate;
/** 违规次数 */
private Long violationCount;
/** 综合评分 */
private Double compositeScore;
}
@@ -0,0 +1,101 @@
package cn.novalon.gym.manage.datacount.handler;
import cn.novalon.gym.manage.datacount.domain.CoachPerformance;
import cn.novalon.gym.manage.datacount.domain.StatisticsQuery;
import cn.novalon.gym.manage.datacount.service.IDataStatisticsService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
import reactor.core.publisher.Mono;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
/**
* 教练业绩 Handler
*
* @author system
* @date 2026-07-22
*/
@Component
@Tag(name = "教练业绩", description = "教练业绩统计相关操作")
public class CoachPerformanceHandler {
private static final Logger log = LoggerFactory.getLogger(CoachPerformanceHandler.class);
@Autowired
private IDataStatisticsService dataStatisticsService;
@Operation(summary = "获取教练业绩排行榜", description = "获取所有教练的业绩排名(按综合评分降序)")
public Mono<ServerResponse> getCoachPerformanceRanking(ServerRequest request) {
StatisticsQuery query = buildQueryFromRequest(request);
return dataStatisticsService.getCoachPerformanceList(query)
.collectList()
.flatMap(list -> ServerResponse.ok().bodyValue(list))
.onErrorResume(e -> {
log.error("获取教练业绩排行榜失败", e);
return ServerResponse.ok().bodyValue(java.util.List.of());
});
}
@Operation(summary = "获取指定教练业绩", description = "获取单个教练的业绩详情")
public Mono<ServerResponse> getCoachPerformanceById(ServerRequest request) {
Long coachId = Long.parseLong(request.pathVariable("coachId"));
StatisticsQuery query = buildQueryFromRequest(request);
return dataStatisticsService.getCoachPerformanceById(coachId, query)
.flatMap(p -> ServerResponse.ok().bodyValue(p))
.onErrorResume(e -> {
log.error("获取教练{}业绩失败", coachId, e);
return ServerResponse.ok().bodyValue(
CoachPerformance.builder().coachId(coachId).coachName("获取失败").build());
});
}
@Operation(summary = "获取当前教练业绩", description = "当前登录的教练查看自己的业绩")
public Mono<ServerResponse> getMyPerformance(ServerRequest request) {
return request.queryParam("coachId")
.map(coachIdStr -> {
Long coachId = Long.parseLong(coachIdStr);
StatisticsQuery query = buildQueryFromRequest(request);
return dataStatisticsService.getCoachPerformanceById(coachId, query)
.flatMap(p -> ServerResponse.ok().bodyValue(p));
})
.orElse(ServerResponse.badRequest().bodyValue("缺少 coachId 参数"));
}
private StatisticsQuery buildQueryFromRequest(ServerRequest request) {
StatisticsQuery.StatisticsQueryBuilder builder = StatisticsQuery.builder();
request.queryParam("statType").ifPresent(builder::statType);
request.queryParam("periodType").ifPresent(builder::periodType);
request.queryParam("startTime").ifPresent(startTimeStr -> {
try {
builder.startTime(LocalDateTime.parse(startTimeStr, DateTimeFormatter.ISO_LOCAL_DATE_TIME));
} catch (Exception e) {
try {
builder.startTime(LocalDateTime.parse(startTimeStr));
} catch (Exception ignored) {
}
}
});
request.queryParam("endTime").ifPresent(endTimeStr -> {
try {
builder.endTime(LocalDateTime.parse(endTimeStr, DateTimeFormatter.ISO_LOCAL_DATE_TIME));
} catch (Exception e) {
try {
builder.endTime(LocalDateTime.parse(endTimeStr));
} catch (Exception ignored) {
}
}
});
return builder.build();
}
}
@@ -75,4 +75,21 @@ public interface IDataStatisticsService {
* @return 统计数据
*/
Mono<StatisticsSummary> getStatisticsSummaryWithCache(StatisticsQuery query);
/**
* 获取教练业绩排行榜
*
* @param query 查询条件
* @return 教练业绩列表(按综合评分降序)
*/
reactor.core.publisher.Flux<CoachPerformance> getCoachPerformanceList(StatisticsQuery query);
/**
* 获取单个教练业绩
*
* @param coachId 教练ID
* @param query 查询条件
* @return 单个教练业绩
*/
Mono<CoachPerformance> getCoachPerformanceById(Long coachId, StatisticsQuery query);
}
@@ -22,8 +22,11 @@ import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.temporal.TemporalAdjusters;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* 数据统计服务实现类
@@ -522,6 +525,134 @@ public class DataStatisticsServiceImpl implements IDataStatisticsService {
}
}
// ========== 教练业绩统计 ==========
@Override
public reactor.core.publisher.Flux<CoachPerformance> getCoachPerformanceList(StatisticsQuery query) {
LocalDateTime startTime = getStartTime(query);
LocalDateTime endTime = getEndTime(query);
// 1. 获取所有教练基本信息
Mono<Map<Long, Map<String, Object>>> coachesMono = dataStatisticsDao.getAllCoachesWithInfo()
.collectMap(row -> ((Number) row.get("id")).longValue(), row -> row);
// 2. 各教练授课量
Mono<Map<Long, Long>> coursesMono = dataStatisticsDao.countCompletedCoursesByCoach(startTime, endTime)
.collectMap(row -> ((Number) row.get("coach_id")).longValue(),
row -> ((Number) row.get("count")).longValue());
// 3. 各教练出席人次
Mono<Map<Long, Long>> attendedMono = dataStatisticsDao.countAttendedStudentsByCoach(startTime, endTime)
.collectMap(row -> ((Number) row.get("coach_id")).longValue(),
row -> ((Number) row.get("count")).longValue());
// 4. 各教练总预约数(非取消)
Mono<Map<Long, Long>> totalBookingsMono = dataStatisticsDao.countTotalBookingsByCoach(startTime, endTime)
.collectMap(row -> ((Number) row.get("coach_id")).longValue(),
row -> ((Number) row.get("count")).longValue());
// 5. 满员率明细(collectMultimap 返回 Collection<V> 而非 List<V>
Mono<Map<Long, Collection<FillRateItem>>> fillRateMono = dataStatisticsDao.getFillRateDetailByCoach(startTime, endTime)
.map(row -> new FillRateItem(
((Number) row.get("coach_id")).longValue(),
((Number) row.get("max_members")).intValue(),
((Number) row.get("attended")).longValue()
))
.collectMultimap(FillRateItem::coachId);
// 6. 各教练违规次数
Mono<Map<Long, Long>> violationsMono = dataStatisticsDao.countViolationsByCoach(startTime, endTime)
.collectMap(row -> ((Number) row.get("coach_id")).longValue(),
row -> ((Number) row.get("count")).longValue());
return Mono.zip(coachesMono, coursesMono, attendedMono, totalBookingsMono, fillRateMono, violationsMono)
.flatMapMany(tuple -> {
Map<Long, Map<String, Object>> coaches = tuple.getT1();
Map<Long, Long> coursesMap = tuple.getT2();
Map<Long, Long> attendedMap = tuple.getT3();
Map<Long, Long> totalBookingsMap = tuple.getT4();
Map<Long, Collection<FillRateItem>> fillRateMap = tuple.getT5();
Map<Long, Long> violationsMap = tuple.getT6();
// 计算最大授课量(用于归一化)
long maxCourses = coursesMap.values().stream().mapToLong(Long::longValue).max().orElse(1L);
List<CoachPerformance> performances = coaches.keySet().stream()
.map(coachId -> {
Map<String, Object> coachInfo = coaches.get(coachId);
long courses = coursesMap.getOrDefault(coachId, 0L);
long attended = attendedMap.getOrDefault(coachId, 0L);
long totalBookings = totalBookingsMap.getOrDefault(coachId, 0L);
long violations = violationsMap.getOrDefault(coachId, 0L);
double attendanceRate = totalBookings > 0
? (double) attended / totalBookings * 100 : 0;
double fillRate = calculateFillRate(fillRateMap.getOrDefault(coachId, List.of()));
double normalizedCourses = maxCourses > 0
? (double) courses / maxCourses * 100 : 0;
double compositeScore = normalizedCourses * 0.4
+ attendanceRate * 0.3 + fillRate * 0.3;
return CoachPerformance.builder()
.coachId(coachId)
.coachName(getString(coachInfo, "nickname", getString(coachInfo, "username", "")))
.avatar(getString(coachInfo, "avatar", null))
.completedCourses(courses)
.attendedStudents(attended)
.totalBookings(totalBookings)
.attendanceRate(Math.round(attendanceRate * 100.0) / 100.0)
.fillRate(Math.round(fillRate * 100.0) / 100.0)
.violationCount(violations)
.compositeScore(Math.round(compositeScore * 100.0) / 100.0)
.build();
})
.sorted((a, b) -> Double.compare(b.getCompositeScore(), a.getCompositeScore()))
.collect(Collectors.toList());
return reactor.core.publisher.Flux.fromIterable(performances);
});
}
@Override
public Mono<CoachPerformance> getCoachPerformanceById(Long coachId, StatisticsQuery query) {
return getCoachPerformanceList(query)
.filter(p -> p.getCoachId().equals(coachId))
.next()
.switchIfEmpty(Mono.just(CoachPerformance.builder()
.coachId(coachId)
.coachName("未知教练")
.completedCourses(0L)
.attendedStudents(0L)
.totalBookings(0L)
.attendanceRate(0.0)
.fillRate(0.0)
.violationCount(0L)
.compositeScore(0.0)
.build()));
}
/**
* 计算满员率:各课程 (出席人数/maxMembers) 的平均值
*/
private double calculateFillRate(Collection<FillRateItem> items) {
if (items == null || items.isEmpty()) return 0;
return items.stream()
.mapToDouble(item -> item.maxMembers > 0
? (double) item.attended / item.maxMembers * 100 : 0)
.average()
.orElse(0);
}
private String getString(Map<String, Object> map, String key, String defaultValue) {
Object val = map.get(key);
return val != null ? val.toString() : defaultValue;
}
/**
* 满员率明细项
*/
private record FillRateItem(Long coachId, int maxMembers, long attended) {}
/**
* 根据周期类型调整时间范围
* 用于定时任务中的周期统计
@@ -0,0 +1,280 @@
package cn.novalon.gym.manage.datacount.domain;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
@DisplayName("DataCount 领域对象单元测试")
class DomainObjectsTest {
@Nested
@DisplayName("MemberStatistics 测试")
class MemberStatisticsTest {
@Test
@DisplayName("无参构造和setter/getter应正确设置和读取所有字段")
void shouldSetAndGetAllFields() {
MemberStatistics ms = new MemberStatistics();
ms.setStatDate("2026-07-22");
ms.setNewMembers(150L);
ms.setActiveMembers(320L);
ms.setTotalMembers(5000L);
ms.setSignInMembers(200L);
ms.setBookingMembers(180L);
ms.setCancelBookingMembers(15L);
assertThat(ms.getStatDate()).isEqualTo("2026-07-22");
assertThat(ms.getNewMembers()).isEqualTo(150L);
assertThat(ms.getActiveMembers()).isEqualTo(320L);
assertThat(ms.getTotalMembers()).isEqualTo(5000L);
assertThat(ms.getSignInMembers()).isEqualTo(200L);
assertThat(ms.getBookingMembers()).isEqualTo(180L);
assertThat(ms.getCancelBookingMembers()).isEqualTo(15L);
}
@Test
@DisplayName("Builder构造应正确设置所有字段")
void shouldBuildCorrectly() {
MemberStatistics ms = MemberStatistics.builder()
.statDate("2026-06-15")
.newMembers(50L)
.activeMembers(100L)
.totalMembers(2000L)
.signInMembers(80L)
.bookingMembers(70L)
.cancelBookingMembers(5L)
.build();
assertThat(ms.getStatDate()).isEqualTo("2026-06-15");
assertThat(ms.getNewMembers()).isEqualTo(50L);
assertThat(ms.getActiveMembers()).isEqualTo(100L);
assertThat(ms.getTotalMembers()).isEqualTo(2000L);
assertThat(ms.getSignInMembers()).isEqualTo(80L);
assertThat(ms.getBookingMembers()).isEqualTo(70L);
assertThat(ms.getCancelBookingMembers()).isEqualTo(5L);
}
}
@Nested
@DisplayName("BookingStatistics 测试")
class BookingStatisticsTest {
@Test
@DisplayName("无参构造和setter/getter应正确设置和读取所有字段")
void shouldSetAndGetAllFields() {
BookingStatistics bs = new BookingStatistics();
bs.setStatDate("2026-07-22");
bs.setNewBookings(60L);
bs.setCancelBookings(10L);
bs.setAttendBookings(45L);
bs.setAbsentBookings(5L);
bs.setAttendanceRate(0.90);
bs.setCancelRate(0.10);
bs.setBookingMembers(55L);
bs.setCancelMembers(8L);
assertThat(bs.getStatDate()).isEqualTo("2026-07-22");
assertThat(bs.getNewBookings()).isEqualTo(60L);
assertThat(bs.getCancelBookings()).isEqualTo(10L);
assertThat(bs.getAttendBookings()).isEqualTo(45L);
assertThat(bs.getAbsentBookings()).isEqualTo(5L);
assertThat(bs.getAttendanceRate()).isEqualTo(0.90);
assertThat(bs.getCancelRate()).isEqualTo(0.10);
assertThat(bs.getBookingMembers()).isEqualTo(55L);
assertThat(bs.getCancelMembers()).isEqualTo(8L);
}
@Test
@DisplayName("Builder构造应正确设置所有字段")
void shouldBuildCorrectly() {
BookingStatistics bs = BookingStatistics.builder()
.statDate("2026-07-01")
.newBookings(30L)
.cancelBookings(3L)
.attendBookings(25L)
.absentBookings(2L)
.attendanceRate(0.83)
.cancelRate(0.10)
.bookingMembers(28L)
.cancelMembers(3L)
.build();
assertThat(bs.getStatDate()).isEqualTo("2026-07-01");
assertThat(bs.getNewBookings()).isEqualTo(30L);
assertThat(bs.getCancelBookings()).isEqualTo(3L);
assertThat(bs.getAttendBookings()).isEqualTo(25L);
assertThat(bs.getAbsentBookings()).isEqualTo(2L);
assertThat(bs.getAttendanceRate()).isEqualTo(0.83);
assertThat(bs.getCancelRate()).isEqualTo(0.10);
assertThat(bs.getBookingMembers()).isEqualTo(28L);
assertThat(bs.getCancelMembers()).isEqualTo(3L);
}
}
@Nested
@DisplayName("SignInStatistics 测试")
class SignInStatisticsTest {
@Test
@DisplayName("无参构造和setter/getter应正确设置和读取所有字段")
void shouldSetAndGetAllFields() {
SignInStatistics ss = new SignInStatistics();
ss.setStatDate("2026-07-22");
ss.setTotalSignIns(200L);
ss.setSuccessSignIns(180L);
ss.setFailedSignIns(20L);
ss.setSuccessRate(0.90);
ss.setSignInMembers(150L);
ss.setQrCodeSignIns(100L);
ss.setManualSignIns(50L);
ss.setFaceSignIns(30L);
assertThat(ss.getStatDate()).isEqualTo("2026-07-22");
assertThat(ss.getTotalSignIns()).isEqualTo(200L);
assertThat(ss.getSuccessSignIns()).isEqualTo(180L);
assertThat(ss.getFailedSignIns()).isEqualTo(20L);
assertThat(ss.getSuccessRate()).isEqualTo(0.90);
assertThat(ss.getSignInMembers()).isEqualTo(150L);
assertThat(ss.getQrCodeSignIns()).isEqualTo(100L);
assertThat(ss.getManualSignIns()).isEqualTo(50L);
assertThat(ss.getFaceSignIns()).isEqualTo(30L);
}
@Test
@DisplayName("Builder构造应正确设置所有字段")
void shouldBuildCorrectly() {
SignInStatistics ss = SignInStatistics.builder()
.statDate("2026-06-01")
.totalSignIns(500L)
.successSignIns(480L)
.failedSignIns(20L)
.successRate(0.96)
.signInMembers(400L)
.qrCodeSignIns(300L)
.manualSignIns(100L)
.faceSignIns(80L)
.build();
assertThat(ss.getStatDate()).isEqualTo("2026-06-01");
assertThat(ss.getTotalSignIns()).isEqualTo(500L);
assertThat(ss.getSuccessSignIns()).isEqualTo(480L);
assertThat(ss.getFailedSignIns()).isEqualTo(20L);
assertThat(ss.getSuccessRate()).isEqualTo(0.96);
assertThat(ss.getSignInMembers()).isEqualTo(400L);
assertThat(ss.getQrCodeSignIns()).isEqualTo(300L);
assertThat(ss.getManualSignIns()).isEqualTo(100L);
assertThat(ss.getFaceSignIns()).isEqualTo(80L);
}
}
@Nested
@DisplayName("StatisticsSummary 测试")
class StatisticsSummaryTest {
@Test
@DisplayName("无参构造和setter/getter应正确设置和读取所有字段")
void shouldSetAndGetAllFields() {
MemberStatistics ms = MemberStatistics.builder().newMembers(10L).build();
BookingStatistics bs = BookingStatistics.builder().newBookings(20L).build();
SignInStatistics ss = SignInStatistics.builder().totalSignIns(30L).build();
CoachStatistics cs = CoachStatistics.builder().totalCoaches(5L).build();
StatisticsSummary summary = new StatisticsSummary();
summary.setStatDate("2026-07-22");
summary.setMemberStatistics(ms);
summary.setBookingStatistics(bs);
summary.setSignInStatistics(ss);
summary.setCoachStatistics(cs);
summary.setGeneratedAt("2026-07-22T10:00:00");
assertThat(summary.getStatDate()).isEqualTo("2026-07-22");
assertThat(summary.getMemberStatistics()).isSameAs(ms);
assertThat(summary.getBookingStatistics()).isSameAs(bs);
assertThat(summary.getSignInStatistics()).isSameAs(ss);
assertThat(summary.getCoachStatistics()).isSameAs(cs);
assertThat(summary.getGeneratedAt()).isEqualTo("2026-07-22T10:00:00");
}
@Test
@DisplayName("Builder构造应正确设置所有字段")
void shouldBuildCorrectly() {
MemberStatistics ms = MemberStatistics.builder().newMembers(5L).build();
StatisticsSummary summary = StatisticsSummary.builder()
.statDate("2026-07-22")
.memberStatistics(ms)
.generatedAt("2026-07-22T12:00:00")
.build();
assertThat(summary.getStatDate()).isEqualTo("2026-07-22");
assertThat(summary.getMemberStatistics()).isSameAs(ms);
assertThat(summary.getGeneratedAt()).isEqualTo("2026-07-22T12:00:00");
}
}
@Nested
@DisplayName("CoachPerformance 测试")
class CoachPerformanceTest {
@Test
@DisplayName("无参构造和setter/getter应正确设置和读取所有字段")
void shouldSetAndGetAllFields() {
CoachPerformance cp = new CoachPerformance();
cp.setCoachId(1L);
cp.setCoachName("张教练");
cp.setAvatar("https://avatar.jpg");
cp.setCompletedCourses(50L);
cp.setAttendedStudents(200L);
cp.setTotalBookings(220L);
cp.setAttendanceRate(0.91);
cp.setFillRate(0.85);
cp.setViolationCount(2L);
cp.setCompositeScore(88.5);
assertThat(cp.getCoachId()).isEqualTo(1L);
assertThat(cp.getCoachName()).isEqualTo("张教练");
assertThat(cp.getAvatar()).isEqualTo("https://avatar.jpg");
assertThat(cp.getCompletedCourses()).isEqualTo(50L);
assertThat(cp.getAttendedStudents()).isEqualTo(200L);
assertThat(cp.getTotalBookings()).isEqualTo(220L);
assertThat(cp.getAttendanceRate()).isEqualTo(0.91);
assertThat(cp.getFillRate()).isEqualTo(0.85);
assertThat(cp.getViolationCount()).isEqualTo(2L);
assertThat(cp.getCompositeScore()).isEqualTo(88.5);
}
@Test
@DisplayName("Builder构造应正确设置所有字段")
void shouldBuildCorrectly() {
CoachPerformance cp = CoachPerformance.builder()
.coachId(2L)
.coachName("李教练")
.avatar("http://example.com/avatar.png")
.completedCourses(100L)
.attendedStudents(500L)
.totalBookings(520L)
.attendanceRate(0.96)
.fillRate(0.92)
.violationCount(0L)
.compositeScore(95.0)
.build();
assertThat(cp.getCoachId()).isEqualTo(2L);
assertThat(cp.getCoachName()).isEqualTo("李教练");
assertThat(cp.getAvatar()).isEqualTo("http://example.com/avatar.png");
assertThat(cp.getCompletedCourses()).isEqualTo(100L);
assertThat(cp.getAttendedStudents()).isEqualTo(500L);
assertThat(cp.getTotalBookings()).isEqualTo(520L);
assertThat(cp.getAttendanceRate()).isEqualTo(0.96);
assertThat(cp.getFillRate()).isEqualTo(0.92);
assertThat(cp.getViolationCount()).isEqualTo(0L);
assertThat(cp.getCompositeScore()).isEqualTo(95.0);
}
}
}
@@ -0,0 +1,74 @@
package cn.novalon.gym.manage.datacount.domain;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.time.LocalDateTime;
import static org.assertj.core.api.Assertions.assertThat;
@DisplayName("StatisticsQuery 单元测试")
class StatisticsQueryTest {
@Test
@DisplayName("无参构造和setter/getter应正确设置和读取所有字段")
void shouldSetAndGetAllFields() {
StatisticsQuery query = new StatisticsQuery();
LocalDateTime start = LocalDateTime.of(2026, 7, 1, 0, 0);
LocalDateTime end = LocalDateTime.of(2026, 7, 22, 23, 59);
query.setStatType("MEMBER");
query.setPeriodType("MONTH");
query.setStartTime(start);
query.setEndTime(end);
query.setPage(0);
query.setSize(20);
assertThat(query.getStatType()).isEqualTo("MEMBER");
assertThat(query.getPeriodType()).isEqualTo("MONTH");
assertThat(query.getStartTime()).isEqualTo(start);
assertThat(query.getEndTime()).isEqualTo(end);
assertThat(query.getPage()).isEqualTo(0);
assertThat(query.getSize()).isEqualTo(20);
}
@Test
@DisplayName("Builder构造应正确设置所有字段")
void shouldBuildWithAllFields() {
LocalDateTime start = LocalDateTime.of(2026, 1, 1, 0, 0);
LocalDateTime end = LocalDateTime.of(2026, 12, 31, 23, 59);
StatisticsQuery query = StatisticsQuery.builder()
.statType("BOOKING")
.periodType("WEEK")
.startTime(start)
.endTime(end)
.page(1)
.size(50)
.build();
assertThat(query.getStatType()).isEqualTo("BOOKING");
assertThat(query.getPeriodType()).isEqualTo("WEEK");
assertThat(query.getStartTime()).isEqualTo(start);
assertThat(query.getEndTime()).isEqualTo(end);
assertThat(query.getPage()).isEqualTo(1);
assertThat(query.getSize()).isEqualTo(50);
}
@Test
@DisplayName("全参构造应正确设置所有字段")
void shouldConstructWithAllArgs() {
LocalDateTime start = LocalDateTime.of(2026, 3, 1, 8, 0);
LocalDateTime end = LocalDateTime.of(2026, 3, 31, 20, 0);
StatisticsQuery query = new StatisticsQuery(
"SIGN_IN", "DAY", start, end, 2, 100);
assertThat(query.getStatType()).isEqualTo("SIGN_IN");
assertThat(query.getPeriodType()).isEqualTo("DAY");
assertThat(query.getStartTime()).isEqualTo(start);
assertThat(query.getEndTime()).isEqualTo(end);
assertThat(query.getPage()).isEqualTo(2);
assertThat(query.getSize()).isEqualTo(100);
}
}