新增e2e测试脚本,修复部分问题
This commit was merged in pull request #51.
This commit is contained in:
+108
-1
@@ -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();
|
||||
}
|
||||
}
|
||||
+49
@@ -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;
|
||||
}
|
||||
+101
@@ -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();
|
||||
}
|
||||
}
|
||||
+17
@@ -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);
|
||||
}
|
||||
|
||||
+131
@@ -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) {}
|
||||
|
||||
/**
|
||||
* 根据周期类型调整时间范围
|
||||
* 用于定时任务中的周期统计
|
||||
|
||||
+280
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+74
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user