重写会员端,新增教练端 #50
@@ -0,0 +1,62 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|
||||||
|
<parent>
|
||||||
|
<groupId>cn.novalon.gym.manage</groupId>
|
||||||
|
<artifactId>gym-manage-api</artifactId>
|
||||||
|
<version>1.0.0</version>
|
||||||
|
</parent>
|
||||||
|
|
||||||
|
<artifactId>gym-coach</artifactId>
|
||||||
|
<packaging>jar</packaging>
|
||||||
|
|
||||||
|
<name>Gym Coach</name>
|
||||||
|
<description>Coach Management Module - Course Start/End, Violation Tracking</description>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>cn.novalon.gym.manage</groupId>
|
||||||
|
<artifactId>manage-common</artifactId>
|
||||||
|
<version>${project.version}</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>cn.novalon.gym.manage</groupId>
|
||||||
|
<artifactId>manage-db</artifactId>
|
||||||
|
<version>${project.version}</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>cn.novalon.gym.manage</groupId>
|
||||||
|
<artifactId>manage-sys</artifactId>
|
||||||
|
<version>${project.version}</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>cn.novalon.gym.manage</groupId>
|
||||||
|
<artifactId>gym-groupCourse</artifactId>
|
||||||
|
<version>${project.version}</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-webflux</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-security</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.data</groupId>
|
||||||
|
<artifactId>spring-data-commons</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springdoc</groupId>
|
||||||
|
<artifactId>springdoc-openapi-starter-webflux-ui</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.projectlombok</groupId>
|
||||||
|
<artifactId>lombok</artifactId>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
</project>
|
||||||
+15
@@ -0,0 +1,15 @@
|
|||||||
|
package cn.novalon.gym.manage.coach.dao;
|
||||||
|
|
||||||
|
import cn.novalon.gym.manage.coach.entity.CoachViolationEntity;
|
||||||
|
import org.springframework.data.r2dbc.repository.R2dbcRepository;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 教练违规记录 DAO
|
||||||
|
*
|
||||||
|
* @author 张翔
|
||||||
|
* @date 2026-07-20
|
||||||
|
*/
|
||||||
|
@Repository
|
||||||
|
public interface CoachViolationDao extends R2dbcRepository<CoachViolationEntity, Long> {
|
||||||
|
}
|
||||||
+61
@@ -0,0 +1,61 @@
|
|||||||
|
package cn.novalon.gym.manage.coach.entity;
|
||||||
|
|
||||||
|
import cn.novalon.gym.manage.db.entity.BaseEntity;
|
||||||
|
import org.springframework.data.relational.core.mapping.Column;
|
||||||
|
import org.springframework.data.relational.core.mapping.Table;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 教练违规记录实体类 - 对应 coach_violation 表
|
||||||
|
*
|
||||||
|
* @author 张翔
|
||||||
|
* @date 2026-07-20
|
||||||
|
*/
|
||||||
|
@Table("coach_violation")
|
||||||
|
public class CoachViolationEntity extends BaseEntity {
|
||||||
|
|
||||||
|
@Column("coach_id")
|
||||||
|
private Long coachId;
|
||||||
|
|
||||||
|
@Column("course_id")
|
||||||
|
private Long courseId;
|
||||||
|
|
||||||
|
@Column("violation_time")
|
||||||
|
private LocalDateTime violationTime;
|
||||||
|
|
||||||
|
@Column("violation_reason")
|
||||||
|
private String violationReason;
|
||||||
|
|
||||||
|
public Long getCoachId() {
|
||||||
|
return coachId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCoachId(Long coachId) {
|
||||||
|
this.coachId = coachId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getCourseId() {
|
||||||
|
return courseId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCourseId(Long courseId) {
|
||||||
|
this.courseId = courseId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LocalDateTime getViolationTime() {
|
||||||
|
return violationTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setViolationTime(LocalDateTime violationTime) {
|
||||||
|
this.violationTime = violationTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getViolationReason() {
|
||||||
|
return violationReason;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setViolationReason(String violationReason) {
|
||||||
|
this.violationReason = violationReason;
|
||||||
|
}
|
||||||
|
}
|
||||||
+42
@@ -0,0 +1,42 @@
|
|||||||
|
package cn.novalon.gym.manage.coach.enums;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 团课预约状态枚举
|
||||||
|
*
|
||||||
|
* @author 张翔
|
||||||
|
* @date 2026-07-20
|
||||||
|
*/
|
||||||
|
public enum BookingStatus {
|
||||||
|
|
||||||
|
BOOKED("0", "已预约"),
|
||||||
|
CANCELLED("1", "已取消"),
|
||||||
|
ATTENDED("2", "已出席"),
|
||||||
|
ABSENT("3", "缺席"),
|
||||||
|
COACH_ABSENT("4", "教练缺席"),
|
||||||
|
LATE("5", "迟到");
|
||||||
|
|
||||||
|
private final String value;
|
||||||
|
private final String desc;
|
||||||
|
|
||||||
|
BookingStatus(String value, String desc) {
|
||||||
|
this.value = value;
|
||||||
|
this.desc = desc;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getValue() {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getDesc() {
|
||||||
|
return desc;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static BookingStatus fromValue(String value) {
|
||||||
|
for (BookingStatus status : values()) {
|
||||||
|
if (status.value.equals(value)) {
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return BOOKED;
|
||||||
|
}
|
||||||
|
}
|
||||||
+30
@@ -0,0 +1,30 @@
|
|||||||
|
package cn.novalon.gym.manage.coach.enums;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 教练违规原因枚举
|
||||||
|
*
|
||||||
|
* @author 张翔
|
||||||
|
* @date 2026-07-20
|
||||||
|
*/
|
||||||
|
public enum ViolationReason {
|
||||||
|
|
||||||
|
COACH_LATE("COACH_LATE", "教练迟到"),
|
||||||
|
COACH_ABSENT("COACH_ABSENT", "教练缺席"),
|
||||||
|
NOT_MANUAL_END("NOT_MANUAL_END", "教练未手动结课");
|
||||||
|
|
||||||
|
private final String value;
|
||||||
|
private final String desc;
|
||||||
|
|
||||||
|
ViolationReason(String value, String desc) {
|
||||||
|
this.value = value;
|
||||||
|
this.desc = desc;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getValue() {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getDesc() {
|
||||||
|
return desc;
|
||||||
|
}
|
||||||
|
}
|
||||||
+68
@@ -0,0 +1,68 @@
|
|||||||
|
package cn.novalon.gym.manage.coach.handler;
|
||||||
|
|
||||||
|
import cn.novalon.gym.manage.coach.service.CoachCourseService;
|
||||||
|
import cn.novalon.gym.manage.sys.util.AuthUtil;
|
||||||
|
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.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.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 教练开课/结课处理器
|
||||||
|
*
|
||||||
|
* @author 张翔
|
||||||
|
* @date 2026-07-20
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
@Tag(name = "教练开课结课", description = "教练开课与结课操作")
|
||||||
|
public class CoachCourseHandler {
|
||||||
|
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(CoachCourseHandler.class);
|
||||||
|
private final CoachCourseService coachCourseService;
|
||||||
|
private final AuthUtil authUtil;
|
||||||
|
|
||||||
|
public CoachCourseHandler(CoachCourseService coachCourseService, AuthUtil authUtil) {
|
||||||
|
this.coachCourseService = coachCourseService;
|
||||||
|
this.authUtil = authUtil;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "教练开课", description = "教练手动开课,记录实际开课时间,若超时则判定迟到")
|
||||||
|
public Mono<ServerResponse> startCourse(ServerRequest request) {
|
||||||
|
Long courseId = Long.valueOf(request.pathVariable("courseId"));
|
||||||
|
Long coachId = authUtil.getMemberIdOrThrow(request);
|
||||||
|
|
||||||
|
return coachCourseService.startCourse(courseId, coachId)
|
||||||
|
.flatMap(result -> ServerResponse.ok().bodyValue(Map.of(
|
||||||
|
"message", "开课成功",
|
||||||
|
"status", result.getStatus(),
|
||||||
|
"actualStartTime", result.getActualStartTime() != null ? result.getActualStartTime().toString() : ""
|
||||||
|
)))
|
||||||
|
.onErrorResume(e -> {
|
||||||
|
logger.error("开课失败: {}", e.getMessage());
|
||||||
|
return ServerResponse.badRequest().bodyValue(Map.of("error", e.getMessage()));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "教练结课", description = "教练手动结课,记录实际结课时间")
|
||||||
|
public Mono<ServerResponse> endCourse(ServerRequest request) {
|
||||||
|
Long courseId = Long.valueOf(request.pathVariable("courseId"));
|
||||||
|
Long coachId = authUtil.getMemberIdOrThrow(request);
|
||||||
|
|
||||||
|
return coachCourseService.endCourse(courseId, coachId)
|
||||||
|
.flatMap(result -> ServerResponse.ok().bodyValue(Map.of(
|
||||||
|
"message", "结课成功",
|
||||||
|
"status", result.getStatus(),
|
||||||
|
"actualEndTime", result.getActualEndTime() != null ? result.getActualEndTime().toString() : ""
|
||||||
|
)))
|
||||||
|
.onErrorResume(e -> {
|
||||||
|
logger.error("结课失败: {}", e.getMessage());
|
||||||
|
return ServerResponse.badRequest().bodyValue(Map.of("error", e.getMessage()));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
+118
@@ -0,0 +1,118 @@
|
|||||||
|
package cn.novalon.gym.manage.coach.handler;
|
||||||
|
|
||||||
|
import cn.novalon.gym.manage.coach.service.CoachCourseService;
|
||||||
|
import cn.novalon.gym.manage.sys.core.domain.SysUser;
|
||||||
|
import cn.novalon.gym.manage.sys.dto.request.CoachCreateRequest;
|
||||||
|
import cn.novalon.gym.manage.sys.dto.request.CoachUpdateRequest;
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import jakarta.validation.Validator;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
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.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 教练管理处理器(从 manage-app 迁移至 gym-coach 模块)
|
||||||
|
*
|
||||||
|
* @author 张翔
|
||||||
|
* @date 2026-07-19
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
@Tag(name = "教练管理", description = "教练相关操作")
|
||||||
|
public class CoachHandler {
|
||||||
|
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(CoachHandler.class);
|
||||||
|
private final CoachCourseService coachCourseService;
|
||||||
|
private final Validator validator;
|
||||||
|
|
||||||
|
public CoachHandler(CoachCourseService coachCourseService, Validator validator) {
|
||||||
|
this.coachCourseService = coachCourseService;
|
||||||
|
this.validator = validator;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "获取所有教练", description = "获取系统中所有教练列表")
|
||||||
|
public Mono<ServerResponse> getAllCoaches(ServerRequest request) {
|
||||||
|
return ServerResponse.ok()
|
||||||
|
.body(coachCourseService.getAllCoaches(), SysUser.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "创建教练", description = "创建新教练(创建用户并分配教练角色)")
|
||||||
|
public Mono<ServerResponse> createCoach(ServerRequest request) {
|
||||||
|
return request.bodyToMono(CoachCreateRequest.class)
|
||||||
|
.flatMap(req -> {
|
||||||
|
var violations = validator.validate(req);
|
||||||
|
if (!violations.isEmpty()) {
|
||||||
|
Map<String, String> errors = new HashMap<>();
|
||||||
|
violations.forEach(v -> errors.put(v.getPropertyPath().toString(), v.getMessage()));
|
||||||
|
return ServerResponse.badRequest().bodyValue(errors);
|
||||||
|
}
|
||||||
|
return coachCourseService.createCoach(
|
||||||
|
req.getUsername(), req.getPassword(),
|
||||||
|
req.getNickname(), req.getEmail(), req.getPhone())
|
||||||
|
.flatMap(user -> ServerResponse.status(HttpStatus.CREATED).bodyValue(user))
|
||||||
|
.onErrorResume(e -> {
|
||||||
|
logger.error("创建教练失败", e);
|
||||||
|
return ServerResponse.badRequest()
|
||||||
|
.bodyValue(Map.of("error", e.getMessage()));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "更新教练信息", description = "更新教练基本信息(昵称、邮箱、手机号)")
|
||||||
|
public Mono<ServerResponse> updateCoach(ServerRequest request) {
|
||||||
|
Long id = Long.valueOf(request.pathVariable("id"));
|
||||||
|
return request.bodyToMono(CoachUpdateRequest.class)
|
||||||
|
.flatMap(req -> coachCourseService.updateCoach(id, req.getNickname(), req.getEmail(), req.getPhone())
|
||||||
|
.flatMap(user -> ServerResponse.ok().bodyValue(user))
|
||||||
|
.switchIfEmpty(ServerResponse.notFound().build())
|
||||||
|
.onErrorResume(e -> {
|
||||||
|
logger.error("更新教练失败", e);
|
||||||
|
return ServerResponse.badRequest()
|
||||||
|
.bodyValue(Map.of("error", e.getMessage()));
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "禁用教练", description = "禁用教练账号,自动取消其所有非进行中的团课")
|
||||||
|
public Mono<ServerResponse> disableCoach(ServerRequest request) {
|
||||||
|
Long id = Long.valueOf(request.pathVariable("id"));
|
||||||
|
return coachCourseService.disableCoach(id)
|
||||||
|
.then(ServerResponse.ok().bodyValue(Map.of("message", "教练已禁用")))
|
||||||
|
.onErrorResume(e -> {
|
||||||
|
logger.error("禁用教练失败: {}", e.getMessage());
|
||||||
|
return ServerResponse.badRequest().bodyValue(Map.of("error", e.getMessage()));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "获取教练的团课", description = "获取指定教练教授的所有团课")
|
||||||
|
public Mono<ServerResponse> getCoachCourses(ServerRequest request) {
|
||||||
|
Long id = Long.valueOf(request.pathVariable("id"));
|
||||||
|
return coachCourseService.getCoachCourses(id)
|
||||||
|
.collectList()
|
||||||
|
.flatMap(courses -> ServerResponse.ok().bodyValue(courses))
|
||||||
|
.switchIfEmpty(ServerResponse.ok().bodyValue(List.of()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "获取教练违规次数统计", description = "获取所有教练的违规次数")
|
||||||
|
public Mono<ServerResponse> getViolationCounts(ServerRequest request) {
|
||||||
|
return coachCourseService.getViolationCounts()
|
||||||
|
.collectList()
|
||||||
|
.flatMap(counts -> ServerResponse.ok().bodyValue(counts));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "获取教练违规记录", description = "获取指定教练的违规记录")
|
||||||
|
public Mono<ServerResponse> getCoachViolations(ServerRequest request) {
|
||||||
|
Long id = Long.valueOf(request.pathVariable("id"));
|
||||||
|
return coachCourseService.getCoachViolations(id)
|
||||||
|
.collectList()
|
||||||
|
.flatMap(violations -> ServerResponse.ok().bodyValue(violations))
|
||||||
|
.switchIfEmpty(ServerResponse.ok().bodyValue(List.of()));
|
||||||
|
}
|
||||||
|
}
|
||||||
+178
@@ -0,0 +1,178 @@
|
|||||||
|
package cn.novalon.gym.manage.coach.scheduler;
|
||||||
|
|
||||||
|
import cn.novalon.gym.manage.coach.enums.ViolationReason;
|
||||||
|
import cn.novalon.gym.manage.common.util.RedisUtil;
|
||||||
|
import cn.novalon.gym.manage.groupcourse.dao.GroupCourseBookingDao;
|
||||||
|
import cn.novalon.gym.manage.groupcourse.dao.GroupCourseDao;
|
||||||
|
import cn.novalon.gym.manage.groupcourse.entity.GroupCourseEntity;
|
||||||
|
import cn.novalon.gym.manage.groupcourse.enums.CourseStatus;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.r2dbc.core.DatabaseClient;
|
||||||
|
import org.springframework.scheduling.annotation.Scheduled;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 教练课程定时调度器
|
||||||
|
*
|
||||||
|
* 功能:
|
||||||
|
* 1. 检查未手动开课的课程,超时标记为教练缺席(5)并记录违规
|
||||||
|
* 2. 检查未手动结课的课程,超时标记为自动结束(6)并记录违规
|
||||||
|
*
|
||||||
|
* @author 张翔
|
||||||
|
* @date 2026-07-20
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
public class CoachCourseScheduler {
|
||||||
|
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(CoachCourseScheduler.class);
|
||||||
|
private static final long ONE_HOUR_MINUTES = 60;
|
||||||
|
private static final long END_GRACE_MINUTES = 10;
|
||||||
|
|
||||||
|
private final GroupCourseDao groupCourseDao;
|
||||||
|
private final GroupCourseBookingDao groupCourseBookingDao;
|
||||||
|
private final DatabaseClient databaseClient;
|
||||||
|
private final RedisUtil redisUtil;
|
||||||
|
|
||||||
|
public CoachCourseScheduler(GroupCourseDao groupCourseDao,
|
||||||
|
GroupCourseBookingDao groupCourseBookingDao,
|
||||||
|
DatabaseClient databaseClient,
|
||||||
|
RedisUtil redisUtil) {
|
||||||
|
this.groupCourseDao = groupCourseDao;
|
||||||
|
this.groupCourseBookingDao = groupCourseBookingDao;
|
||||||
|
this.databaseClient = databaseClient;
|
||||||
|
this.redisUtil = redisUtil;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 每分钟检查一次:
|
||||||
|
* - status=0(NORMAL) 的课程是否已过开课缺席阈值
|
||||||
|
* - status=3(IN_PROGRESS) 或 status=7(COACH_LATE) 的课程是否已过结课宽限期
|
||||||
|
*/
|
||||||
|
@Scheduled(fixedRate = 60000)
|
||||||
|
public void checkAndProcessCourses() {
|
||||||
|
logger.debug("教练课程调度器开始检查");
|
||||||
|
|
||||||
|
LocalDateTime now = LocalDateTime.now();
|
||||||
|
|
||||||
|
// 1. 检查未开课的课程
|
||||||
|
processAbsentCourses(now)
|
||||||
|
.subscribe(
|
||||||
|
count -> {
|
||||||
|
if (count > 0) {
|
||||||
|
logger.info("教练课程调度器:处理了 {} 门教练缺席课程", count);
|
||||||
|
invalidateCache();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error -> logger.error("教练课程调度器(缺席检查)执行失败:{}", error.getMessage(), error)
|
||||||
|
);
|
||||||
|
|
||||||
|
// 2. 检查未结课的课程
|
||||||
|
processAutoEndCourses(now)
|
||||||
|
.subscribe(
|
||||||
|
count -> {
|
||||||
|
if (count > 0) {
|
||||||
|
logger.info("教练课程调度器:处理了 {} 门自动结束课程", count);
|
||||||
|
invalidateCache();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error -> logger.error("教练课程调度器(自动结课)执行失败:{}", error.getMessage(), error)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理缺席课程:status=0 且已过开课缺席阈值
|
||||||
|
*/
|
||||||
|
private Mono<Long> processAbsentCourses(LocalDateTime now) {
|
||||||
|
return groupCourseDao.findByStatusAndStartTimeBefore(databaseClient, "0", now)
|
||||||
|
.filter(course -> isAbsentThresholdExceeded(course, now))
|
||||||
|
.flatMap(course -> markAsCoachAbsent(course, now))
|
||||||
|
.count();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理自动结课:status IN ('3','7') 且 end_time + 10分钟 已过
|
||||||
|
*/
|
||||||
|
private Mono<Long> processAutoEndCourses(LocalDateTime now) {
|
||||||
|
LocalDateTime endThreshold = now.minusMinutes(END_GRACE_MINUTES);
|
||||||
|
return groupCourseDao.findByStatusInAndEndTimeBefore(databaseClient,
|
||||||
|
new String[]{String.valueOf(CourseStatus.IN_PROGRESS.getValue()),
|
||||||
|
String.valueOf(CourseStatus.COACH_LATE.getValue())},
|
||||||
|
endThreshold)
|
||||||
|
.flatMap(course -> markAsAutoEnded(course, now))
|
||||||
|
.count();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断课程是否已过缺席阈值
|
||||||
|
*/
|
||||||
|
private boolean isAbsentThresholdExceeded(GroupCourseEntity course, LocalDateTime now) {
|
||||||
|
long courseDurationMinutes = Duration.between(course.getStartTime(), course.getEndTime()).toMinutes();
|
||||||
|
long minutesSinceStart = Duration.between(course.getStartTime(), now).toMinutes();
|
||||||
|
|
||||||
|
if (courseDurationMinutes >= ONE_HOUR_MINUTES) {
|
||||||
|
return minutesSinceStart > 30;
|
||||||
|
} else {
|
||||||
|
long thresholdB = Math.max(1, (long) (courseDurationMinutes * 0.25));
|
||||||
|
return minutesSinceStart > thresholdB;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 标记课程为教练缺席(5),更新预约记录为教练缺席(4),记录违规
|
||||||
|
*/
|
||||||
|
private Mono<GroupCourseEntity> markAsCoachAbsent(GroupCourseEntity course, LocalDateTime now) {
|
||||||
|
logger.info("课程 {} 教练缺席,标记为 COACH_ABSENT", course.getId());
|
||||||
|
|
||||||
|
return insertViolation(course.getCoachId(), course.getId(), now, ViolationReason.COACH_ABSENT)
|
||||||
|
.then(groupCourseDao.updateToCoachAbsent(course.getId(), now, now))
|
||||||
|
.then(groupCourseBookingDao.updateStatusByCourseId(course.getId(), "0", "4"))
|
||||||
|
.thenReturn(course);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 标记课程为自动结束(6),记录违规
|
||||||
|
*/
|
||||||
|
private Mono<GroupCourseEntity> markAsAutoEnded(GroupCourseEntity course, LocalDateTime now) {
|
||||||
|
logger.info("课程 {} 未手动结课,标记为 AUTO_ENDED", course.getId());
|
||||||
|
|
||||||
|
return insertViolation(course.getCoachId(), course.getId(), now, ViolationReason.NOT_MANUAL_END)
|
||||||
|
.then(groupCourseDao.updateToAutoEnded(course.getId(), now, now))
|
||||||
|
.thenReturn(course);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 插入违规记录(使用 DatabaseClient 直连)
|
||||||
|
*/
|
||||||
|
private Mono<Void> insertViolation(Long coachId, Long courseId, LocalDateTime violationTime,
|
||||||
|
ViolationReason reason) {
|
||||||
|
return databaseClient.sql("""
|
||||||
|
INSERT INTO coach_violation (coach_id, course_id, violation_time, violation_reason, created_at, updated_at)
|
||||||
|
VALUES (:coachId, :courseId, :violationTime, :reason, :now, :now)
|
||||||
|
""")
|
||||||
|
.bind("coachId", coachId)
|
||||||
|
.bind("courseId", courseId)
|
||||||
|
.bind("violationTime", violationTime)
|
||||||
|
.bind("reason", reason.getValue())
|
||||||
|
.bind("now", LocalDateTime.now())
|
||||||
|
.then();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清除统计缓存和团课缓存 —— 调度器触发时,如有课程状态变更则必须及时失效
|
||||||
|
*/
|
||||||
|
private void invalidateCache() {
|
||||||
|
redisUtil.deleteByPattern("datacount:statistics:*").subscribe(
|
||||||
|
deleted -> logger.debug("调度器清除统计缓存,已删除 {} 条", deleted),
|
||||||
|
error -> logger.warn("调度器清除统计缓存失败: {}", error.getMessage())
|
||||||
|
);
|
||||||
|
redisUtil.deleteByPattern("group_course:*").subscribe(
|
||||||
|
deleted -> logger.debug("调度器清除团课缓存,已删除 {} 条", deleted),
|
||||||
|
error -> logger.warn("调度器清除团课缓存失败: {}", error.getMessage())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
+357
@@ -0,0 +1,357 @@
|
|||||||
|
package cn.novalon.gym.manage.coach.service;
|
||||||
|
|
||||||
|
import cn.novalon.gym.manage.coach.dao.CoachViolationDao;
|
||||||
|
import cn.novalon.gym.manage.coach.entity.CoachViolationEntity;
|
||||||
|
import cn.novalon.gym.manage.coach.enums.ViolationReason;
|
||||||
|
import cn.novalon.gym.manage.common.util.RedisUtil;
|
||||||
|
import cn.novalon.gym.manage.common.util.StatusConstants;
|
||||||
|
import cn.novalon.gym.manage.groupcourse.dao.GroupCourseBookingDao;
|
||||||
|
import cn.novalon.gym.manage.groupcourse.dao.GroupCourseDao;
|
||||||
|
import cn.novalon.gym.manage.groupcourse.domain.GroupCourse;
|
||||||
|
import cn.novalon.gym.manage.groupcourse.entity.GroupCourseEntity;
|
||||||
|
import cn.novalon.gym.manage.groupcourse.enums.CourseStatus;
|
||||||
|
import cn.novalon.gym.manage.groupcourse.repository.IGroupCourseBookingRepository;
|
||||||
|
import cn.novalon.gym.manage.groupcourse.repository.IGroupCourseRepository;
|
||||||
|
import cn.novalon.gym.manage.sys.core.domain.SysRole;
|
||||||
|
import cn.novalon.gym.manage.sys.core.domain.SysUser;
|
||||||
|
import cn.novalon.gym.manage.sys.core.domain.UserRole;
|
||||||
|
import cn.novalon.gym.manage.sys.core.repository.ISysRoleRepository;
|
||||||
|
import cn.novalon.gym.manage.sys.core.repository.ISysUserRepository;
|
||||||
|
import cn.novalon.gym.manage.sys.core.repository.IUserRoleRepository;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.data.domain.Sort;
|
||||||
|
import org.springframework.r2dbc.core.DatabaseClient;
|
||||||
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
import reactor.core.publisher.Flux;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 教练课程服务(含教练管理 + 开课/结课逻辑 + 违规记录)
|
||||||
|
*
|
||||||
|
* @author 张翔
|
||||||
|
* @date 2026-07-20
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
public class CoachCourseService {
|
||||||
|
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(CoachCourseService.class);
|
||||||
|
private static final String COACH_ROLE_NAME = "教练";
|
||||||
|
private static final long ONE_HOUR_MINUTES = 60;
|
||||||
|
|
||||||
|
private final ISysUserRepository userRepository;
|
||||||
|
private final ISysRoleRepository roleRepository;
|
||||||
|
private final IUserRoleRepository userRoleRepository;
|
||||||
|
private final IGroupCourseRepository groupCourseRepository;
|
||||||
|
private final IGroupCourseBookingRepository bookingRepository;
|
||||||
|
private final GroupCourseDao groupCourseDao;
|
||||||
|
private final GroupCourseBookingDao groupCourseBookingDao;
|
||||||
|
private final CoachViolationDao violationDao;
|
||||||
|
private final DatabaseClient databaseClient;
|
||||||
|
private final PasswordEncoder passwordEncoder;
|
||||||
|
private final RedisUtil redisUtil;
|
||||||
|
|
||||||
|
public CoachCourseService(ISysUserRepository userRepository,
|
||||||
|
ISysRoleRepository roleRepository,
|
||||||
|
IUserRoleRepository userRoleRepository,
|
||||||
|
IGroupCourseRepository groupCourseRepository,
|
||||||
|
IGroupCourseBookingRepository bookingRepository,
|
||||||
|
GroupCourseDao groupCourseDao,
|
||||||
|
GroupCourseBookingDao groupCourseBookingDao,
|
||||||
|
CoachViolationDao violationDao,
|
||||||
|
DatabaseClient databaseClient,
|
||||||
|
PasswordEncoder passwordEncoder,
|
||||||
|
RedisUtil redisUtil) {
|
||||||
|
this.userRepository = userRepository;
|
||||||
|
this.roleRepository = roleRepository;
|
||||||
|
this.userRoleRepository = userRoleRepository;
|
||||||
|
this.groupCourseRepository = groupCourseRepository;
|
||||||
|
this.bookingRepository = bookingRepository;
|
||||||
|
this.groupCourseDao = groupCourseDao;
|
||||||
|
this.groupCourseBookingDao = groupCourseBookingDao;
|
||||||
|
this.violationDao = violationDao;
|
||||||
|
this.databaseClient = databaseClient;
|
||||||
|
this.passwordEncoder = passwordEncoder;
|
||||||
|
this.redisUtil = redisUtil;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 教练管理(从原 CoachService 迁移) ====================
|
||||||
|
|
||||||
|
public Flux<SysUser> getAllCoaches() {
|
||||||
|
return getCoachRoleId()
|
||||||
|
.flatMapMany(roleId -> userRoleRepository.findByRoleId(roleId)
|
||||||
|
.map(UserRole::getUserId)
|
||||||
|
.collectList()
|
||||||
|
.flatMapMany(userIds -> {
|
||||||
|
if (userIds.isEmpty()) {
|
||||||
|
return Flux.empty();
|
||||||
|
}
|
||||||
|
return Flux.fromIterable(userIds)
|
||||||
|
.flatMap(userRepository::findById)
|
||||||
|
.filter(user -> user.getDeletedAt() == null);
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
public Mono<SysUser> createCoach(String username, String password, String nickname, String email, String phone) {
|
||||||
|
return getCoachRoleId().flatMap(coachRoleId -> {
|
||||||
|
SysUser user = new SysUser();
|
||||||
|
user.generateId();
|
||||||
|
user.setUsername(username);
|
||||||
|
user.setPassword(passwordEncoder.encode(password));
|
||||||
|
user.setNickname(nickname);
|
||||||
|
user.setEmail(email);
|
||||||
|
user.setPhone(phone);
|
||||||
|
user.setStatus(StatusConstants.ENABLED);
|
||||||
|
|
||||||
|
return userRepository.save(user)
|
||||||
|
.flatMap(saved -> {
|
||||||
|
UserRole userRole = new UserRole();
|
||||||
|
userRole.setUserId(saved.getId());
|
||||||
|
userRole.setRoleId(coachRoleId);
|
||||||
|
return userRoleRepository.save(userRole).thenReturn(saved);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public Mono<SysUser> updateCoach(Long id, String nickname, String email, String phone) {
|
||||||
|
return userRepository.findById(id)
|
||||||
|
.switchIfEmpty(Mono.error(new RuntimeException("教练不存在")))
|
||||||
|
.flatMap(user -> {
|
||||||
|
if (nickname != null) user.setNickname(nickname);
|
||||||
|
if (email != null) user.setEmail(email);
|
||||||
|
if (phone != null) user.setPhone(phone);
|
||||||
|
user.setUpdatedAt(LocalDateTime.now());
|
||||||
|
return userRepository.update(user);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional(transactionManager = "connectionFactoryTransactionManager")
|
||||||
|
public Mono<Void> disableCoach(Long id) {
|
||||||
|
return userRepository.findById(id)
|
||||||
|
.switchIfEmpty(Mono.error(new RuntimeException("教练不存在")))
|
||||||
|
.flatMap(user ->
|
||||||
|
groupCourseRepository.countByCoachIdAndStatus(id, 3L)
|
||||||
|
.flatMap(inProgressCount -> {
|
||||||
|
if (inProgressCount > 0) {
|
||||||
|
return Mono.error(new RuntimeException(
|
||||||
|
"该教练有 " + inProgressCount + " 门正在进行中的团课,无法禁用"));
|
||||||
|
}
|
||||||
|
return groupCourseRepository.cancelCoursesByCoachIdExceptStatus(id, 3L)
|
||||||
|
.then();
|
||||||
|
})
|
||||||
|
.then(Mono.defer(() -> {
|
||||||
|
user.setStatus(StatusConstants.DISABLED);
|
||||||
|
user.setUpdatedAt(LocalDateTime.now());
|
||||||
|
return userRepository.update(user).then();
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
.then(invalidateStatisticsCache());
|
||||||
|
}
|
||||||
|
|
||||||
|
public Flux<GroupCourse> getCoachCourses(Long coachId) {
|
||||||
|
return groupCourseRepository.findByCoachId(coachId, Sort.by(Sort.Direction.DESC, "startTime"))
|
||||||
|
.flatMap(course ->
|
||||||
|
bookingRepository.countValidBookings(course.getId())
|
||||||
|
.map(count -> {
|
||||||
|
course.setCurrentMembers(count.intValue());
|
||||||
|
return course;
|
||||||
|
})
|
||||||
|
.defaultIfEmpty(course)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Mono<Long> getCoachRoleId() {
|
||||||
|
return roleRepository.findByRoleName(COACH_ROLE_NAME)
|
||||||
|
.map(SysRole::getId)
|
||||||
|
.switchIfEmpty(Mono.error(new RuntimeException("教练角色未找到,请先执行数据库迁移")));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 开课逻辑 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 教练手动开课
|
||||||
|
* 判定逻辑:
|
||||||
|
* - 长课时(>=1h): 10分钟内正常,10~30分钟迟到,>30分钟拒绝
|
||||||
|
* - 短课时(<1h): 10%时长内正常,10%~25%迟到,>25%拒绝
|
||||||
|
*/
|
||||||
|
public Mono<GroupCourseEntity> startCourse(Long courseId, Long coachId) {
|
||||||
|
return groupCourseDao.findByIdIsAndDeletedAtIsNull(courseId)
|
||||||
|
.switchIfEmpty(Mono.error(new RuntimeException("团课不存在")))
|
||||||
|
.flatMap(course -> {
|
||||||
|
// 验证教练身份
|
||||||
|
if (!course.getCoachId().equals(coachId)) {
|
||||||
|
return Mono.error(new RuntimeException("您不是该课程的教练,无权开课"));
|
||||||
|
}
|
||||||
|
// 验证课程状态:只有 NORMAL(0) 可以开课
|
||||||
|
if (!CourseStatus.NORMAL.getValue().equals(course.getStatus())) {
|
||||||
|
return Mono.error(new RuntimeException("当前课程状态不允许开课,当前状态: " + course.getStatus()));
|
||||||
|
}
|
||||||
|
|
||||||
|
LocalDateTime now = LocalDateTime.now();
|
||||||
|
long courseDurationMinutes = Duration.between(course.getStartTime(), course.getEndTime()).toMinutes();
|
||||||
|
|
||||||
|
if (courseDurationMinutes >= ONE_HOUR_MINUTES) {
|
||||||
|
return handleLongCourseStart(course, now);
|
||||||
|
} else {
|
||||||
|
return handleShortCourseStart(course, now, courseDurationMinutes);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private Mono<GroupCourseEntity> handleLongCourseStart(GroupCourseEntity course, LocalDateTime now) {
|
||||||
|
long minutesSinceStart = Duration.between(course.getStartTime(), now).toMinutes();
|
||||||
|
|
||||||
|
if (minutesSinceStart < 0) {
|
||||||
|
return Mono.error(new RuntimeException("课程尚未到开课时间"));
|
||||||
|
}
|
||||||
|
if (minutesSinceStart <= 10) {
|
||||||
|
// 正常开课
|
||||||
|
return doStartCourse(course, now, CourseStatus.IN_PROGRESS, null);
|
||||||
|
}
|
||||||
|
if (minutesSinceStart <= 30) {
|
||||||
|
// 教练迟到
|
||||||
|
return recordViolation(course.getCoachId(), course.getId(), now, ViolationReason.COACH_LATE)
|
||||||
|
.then(doStartCourse(course, now, CourseStatus.COACH_LATE, ViolationReason.COACH_LATE));
|
||||||
|
}
|
||||||
|
// >30分钟,拒绝(调度器应已标记为缺席)
|
||||||
|
return Mono.error(new RuntimeException("已超过开课时间30分钟,无法开课"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private Mono<GroupCourseEntity> handleShortCourseStart(GroupCourseEntity course, LocalDateTime now,
|
||||||
|
long courseDurationMinutes) {
|
||||||
|
long minutesSinceStart = Duration.between(course.getStartTime(), now).toMinutes();
|
||||||
|
long thresholdA = Math.max(1, (long) (courseDurationMinutes * 0.10));
|
||||||
|
long thresholdB = Math.max(1, (long) (courseDurationMinutes * 0.25));
|
||||||
|
|
||||||
|
if (minutesSinceStart < 0) {
|
||||||
|
return Mono.error(new RuntimeException("课程尚未到开课时间"));
|
||||||
|
}
|
||||||
|
if (minutesSinceStart <= thresholdA) {
|
||||||
|
// 正常开课
|
||||||
|
return doStartCourse(course, now, CourseStatus.IN_PROGRESS, null);
|
||||||
|
}
|
||||||
|
if (minutesSinceStart <= thresholdB) {
|
||||||
|
// 教练迟到
|
||||||
|
return recordViolation(course.getCoachId(), course.getId(), now, ViolationReason.COACH_LATE)
|
||||||
|
.then(doStartCourse(course, now, CourseStatus.COACH_LATE, ViolationReason.COACH_LATE));
|
||||||
|
}
|
||||||
|
// >thresholdB,拒绝
|
||||||
|
return Mono.error(new RuntimeException("已超过开课时间,无法开课"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private Mono<GroupCourseEntity> doStartCourse(GroupCourseEntity course, LocalDateTime now,
|
||||||
|
CourseStatus newStatus, ViolationReason violationReason) {
|
||||||
|
course.setStatus(newStatus.getValue());
|
||||||
|
course.setActualStartTime(now);
|
||||||
|
course.setUpdatedAt(now);
|
||||||
|
return groupCourseDao.updateStartInfo(course.getId(), String.valueOf(newStatus.getValue()), now, now)
|
||||||
|
.then(groupCourseDao.findByIdIsAndDeletedAtIsNull(course.getId()))
|
||||||
|
.flatMap(entity -> invalidateStatisticsCache().thenReturn(entity));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 结课逻辑 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 教练手动结课
|
||||||
|
* 可在 IN_PROGRESS(3) 或 COACH_LATE(7) 状态下结课
|
||||||
|
* 必须在标注结课时间 + 10分钟内
|
||||||
|
*/
|
||||||
|
public Mono<GroupCourseEntity> endCourse(Long courseId, Long coachId) {
|
||||||
|
return groupCourseDao.findByIdIsAndDeletedAtIsNull(courseId)
|
||||||
|
.switchIfEmpty(Mono.error(new RuntimeException("团课不存在")))
|
||||||
|
.flatMap(course -> {
|
||||||
|
// 验证教练身份
|
||||||
|
if (!course.getCoachId().equals(coachId)) {
|
||||||
|
return Mono.error(new RuntimeException("您不是该课程的教练,无权结课"));
|
||||||
|
}
|
||||||
|
// 验证状态:IN_PROGRESS(3) 或 COACH_LATE(7)
|
||||||
|
Long status = course.getStatus();
|
||||||
|
if (!CourseStatus.IN_PROGRESS.getValue().equals(status)
|
||||||
|
&& !CourseStatus.COACH_LATE.getValue().equals(status)) {
|
||||||
|
return Mono.error(new RuntimeException("当前课程状态不允许结课,当前状态: " + status));
|
||||||
|
}
|
||||||
|
|
||||||
|
LocalDateTime now = LocalDateTime.now();
|
||||||
|
long minutesAfterEnd = Duration.between(course.getEndTime(), now).toMinutes();
|
||||||
|
|
||||||
|
if (minutesAfterEnd > 10) {
|
||||||
|
return Mono.error(new RuntimeException("已超过结课时间10分钟,请等待系统自动结课"));
|
||||||
|
}
|
||||||
|
|
||||||
|
course.setStatus(CourseStatus.ENDED.getValue());
|
||||||
|
course.setActualEndTime(now);
|
||||||
|
course.setUpdatedAt(now);
|
||||||
|
return groupCourseDao.updateEndInfo(course.getId(), String.valueOf(CourseStatus.ENDED.getValue()), now, now)
|
||||||
|
.then(groupCourseDao.findByIdIsAndDeletedAtIsNull(course.getId()))
|
||||||
|
.flatMap(entity -> invalidateStatisticsCache().thenReturn(entity));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 违规记录 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 记录教练违规行为(使用 DatabaseClient 直连,避免 R2DBC Entity 映射问题)
|
||||||
|
*/
|
||||||
|
public Mono<Void> recordViolation(Long coachId, Long courseId, LocalDateTime violationTime,
|
||||||
|
ViolationReason reason) {
|
||||||
|
LocalDateTime now = LocalDateTime.now();
|
||||||
|
return databaseClient.sql("""
|
||||||
|
INSERT INTO coach_violation (coach_id, course_id, violation_time, violation_reason, created_at, updated_at)
|
||||||
|
VALUES (:coachId, :courseId, :violationTime, :reason, :now, :now)
|
||||||
|
""")
|
||||||
|
.bind("coachId", coachId)
|
||||||
|
.bind("courseId", courseId)
|
||||||
|
.bind("violationTime", violationTime)
|
||||||
|
.bind("reason", reason.getValue())
|
||||||
|
.bind("now", now)
|
||||||
|
.then();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 违规查询 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取所有教练的违规次数统计
|
||||||
|
*/
|
||||||
|
public Flux<Map<String, Object>> getViolationCounts() {
|
||||||
|
return databaseClient.sql("""
|
||||||
|
SELECT coach_id, COUNT(*) AS count
|
||||||
|
FROM coach_violation
|
||||||
|
WHERE deleted_at IS NULL
|
||||||
|
GROUP BY coach_id
|
||||||
|
""")
|
||||||
|
.fetch()
|
||||||
|
.all();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取指定教练的违规记录
|
||||||
|
*/
|
||||||
|
public Flux<Map<String, Object>> getCoachViolations(Long coachId) {
|
||||||
|
return databaseClient.sql("""
|
||||||
|
SELECT v.*, gc.course_name
|
||||||
|
FROM coach_violation v
|
||||||
|
LEFT JOIN group_course gc ON v.course_id = gc.id AND gc.deleted_at IS NULL
|
||||||
|
WHERE v.coach_id = :coachId AND v.deleted_at IS NULL
|
||||||
|
ORDER BY v.violation_time DESC
|
||||||
|
""")
|
||||||
|
.bind("coachId", coachId)
|
||||||
|
.fetch()
|
||||||
|
.all();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清除统计缓存和团课缓存 —— 课程状态变更后调用,返回 Mono 确保链式执行
|
||||||
|
*/
|
||||||
|
private Mono<Void> invalidateStatisticsCache() {
|
||||||
|
return redisUtil.deleteByPattern("datacount:statistics:*")
|
||||||
|
.then(redisUtil.deleteByPattern("group_course:*"))
|
||||||
|
.doOnNext(count -> {})
|
||||||
|
.then();
|
||||||
|
}
|
||||||
|
}
|
||||||
+65
@@ -6,6 +6,7 @@ import reactor.core.publisher.Flux;
|
|||||||
import reactor.core.publisher.Mono;
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 数据统计 DAO - 使用 DatabaseClient 执行跨表聚合查询
|
* 数据统计 DAO - 使用 DatabaseClient 执行跨表聚合查询
|
||||||
@@ -180,4 +181,68 @@ public class DataStatisticsDao {
|
|||||||
this.count = count;
|
this.count = count;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ========== 教练相关统计 ==========
|
||||||
|
|
||||||
|
/** 统计教练总数(角色为"教练"的用户) */
|
||||||
|
public Mono<Long> countTotalCoaches() {
|
||||||
|
return databaseClient.sql("""
|
||||||
|
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
|
||||||
|
""")
|
||||||
|
.map(row -> row.get(0, Long.class))
|
||||||
|
.one();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 统计违规总数(时间范围) */
|
||||||
|
public Mono<Long> countTotalViolations(LocalDateTime startTime, LocalDateTime endTime) {
|
||||||
|
return databaseClient.sql("""
|
||||||
|
SELECT COUNT(*) FROM coach_violation
|
||||||
|
WHERE violation_time >= :startTime AND violation_time < :endTime AND deleted_at IS NULL
|
||||||
|
""")
|
||||||
|
.bind("startTime", startTime)
|
||||||
|
.bind("endTime", endTime)
|
||||||
|
.map(row -> row.get(0, Long.class))
|
||||||
|
.one();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 按违规类型统计次数 */
|
||||||
|
public Flux<Map<String, Object>> countViolationsByReason(LocalDateTime startTime, LocalDateTime endTime) {
|
||||||
|
return databaseClient.sql("""
|
||||||
|
SELECT violation_reason, COUNT(*) AS count
|
||||||
|
FROM coach_violation
|
||||||
|
WHERE violation_time >= :startTime AND violation_time < :endTime AND deleted_at IS NULL
|
||||||
|
GROUP BY violation_reason
|
||||||
|
""")
|
||||||
|
.bind("startTime", startTime)
|
||||||
|
.bind("endTime", endTime)
|
||||||
|
.fetch()
|
||||||
|
.all();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 统计有违规记录的教练数 */
|
||||||
|
public Mono<Long> countViolatedCoaches(LocalDateTime startTime, LocalDateTime endTime) {
|
||||||
|
return databaseClient.sql("""
|
||||||
|
SELECT COUNT(DISTINCT coach_id) FROM coach_violation
|
||||||
|
WHERE violation_time >= :startTime AND violation_time < :endTime AND deleted_at IS NULL
|
||||||
|
""")
|
||||||
|
.bind("startTime", startTime)
|
||||||
|
.bind("endTime", endTime)
|
||||||
|
.map(row -> row.get(0, Long.class))
|
||||||
|
.one();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 统计区间内开课的团课数 */
|
||||||
|
public Mono<Long> countCourses(LocalDateTime startTime, LocalDateTime endTime) {
|
||||||
|
return databaseClient.sql("""
|
||||||
|
SELECT COUNT(*) FROM group_course
|
||||||
|
WHERE start_time >= :startTime AND start_time < :endTime AND deleted_at IS NULL
|
||||||
|
""")
|
||||||
|
.bind("startTime", startTime)
|
||||||
|
.bind("endTime", endTime)
|
||||||
|
.map(row -> row.get(0, Long.class))
|
||||||
|
.one();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+40
@@ -0,0 +1,40 @@
|
|||||||
|
package cn.novalon.gym.manage.datacount.domain;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 教练统计数据
|
||||||
|
*
|
||||||
|
* @author 张翔
|
||||||
|
* @date 2026-07-20
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class CoachStatistics {
|
||||||
|
|
||||||
|
/** 教练总数 */
|
||||||
|
private Long totalCoaches;
|
||||||
|
|
||||||
|
/** 违规总数 */
|
||||||
|
private Long totalViolations;
|
||||||
|
|
||||||
|
/** 迟到次数 */
|
||||||
|
private Long lateCount;
|
||||||
|
|
||||||
|
/** 缺席次数 */
|
||||||
|
private Long absentCount;
|
||||||
|
|
||||||
|
/** 未手动结课次数 */
|
||||||
|
private Long notManualEndCount;
|
||||||
|
|
||||||
|
/** 有违规记录的教练数 */
|
||||||
|
private Long violatedCoaches;
|
||||||
|
|
||||||
|
/** 统计区间内开课的团课数 */
|
||||||
|
private Long totalCourses;
|
||||||
|
}
|
||||||
+5
@@ -37,6 +37,11 @@ public class StatisticsSummary {
|
|||||||
*/
|
*/
|
||||||
private SignInStatistics signInStatistics;
|
private SignInStatistics signInStatistics;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 教练统计数据
|
||||||
|
*/
|
||||||
|
private CoachStatistics coachStatistics;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 统计数据生成时间
|
* 统计数据生成时间
|
||||||
*/
|
*/
|
||||||
|
|||||||
+30
-1
@@ -171,18 +171,47 @@ public class DataStatisticsServiceImpl implements IDataStatisticsService {
|
|||||||
return count != null ? count : 0L;
|
return count != null ? count : 0L;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Mono<CoachStatistics> getCoachStatistics(StatisticsQuery query) {
|
||||||
|
LocalDateTime startTime = getStartTime(query);
|
||||||
|
LocalDateTime endTime = getEndTime(query);
|
||||||
|
|
||||||
|
Mono<Long> totalCoachesMono = dataStatisticsDao.countTotalCoaches();
|
||||||
|
Mono<Long> totalViolationsMono = dataStatisticsDao.countTotalViolations(startTime, endTime);
|
||||||
|
Mono<Long> violatedCoachesMono = dataStatisticsDao.countViolatedCoaches(startTime, endTime);
|
||||||
|
Mono<Long> totalCoursesMono = dataStatisticsDao.countCourses(startTime, endTime);
|
||||||
|
Mono<Map<String, Long>> violationByReasonMono = dataStatisticsDao.countViolationsByReason(startTime, endTime)
|
||||||
|
.collectMap(row -> (String) row.get("violation_reason"),
|
||||||
|
row -> ((Number) row.get("count")).longValue());
|
||||||
|
|
||||||
|
return Mono.zip(totalCoachesMono, totalViolationsMono, violatedCoachesMono, totalCoursesMono, violationByReasonMono)
|
||||||
|
.map(tuple -> {
|
||||||
|
Map<String, Long> reasonMap = tuple.getT5();
|
||||||
|
return CoachStatistics.builder()
|
||||||
|
.totalCoaches(tuple.getT1())
|
||||||
|
.totalViolations(tuple.getT2())
|
||||||
|
.lateCount(reasonMap.getOrDefault("COACH_LATE", 0L))
|
||||||
|
.absentCount(reasonMap.getOrDefault("COACH_ABSENT", 0L))
|
||||||
|
.notManualEndCount(reasonMap.getOrDefault("NOT_MANUAL_END", 0L))
|
||||||
|
.violatedCoaches(tuple.getT3())
|
||||||
|
.totalCourses(tuple.getT4())
|
||||||
|
.build();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Mono<StatisticsSummary> getStatisticsSummary(StatisticsQuery query) {
|
public Mono<StatisticsSummary> getStatisticsSummary(StatisticsQuery query) {
|
||||||
Mono<MemberStatistics> memberStatsMono = getMemberStatistics(query);
|
Mono<MemberStatistics> memberStatsMono = getMemberStatistics(query);
|
||||||
Mono<BookingStatistics> bookingStatsMono = getBookingStatistics(query);
|
Mono<BookingStatistics> bookingStatsMono = getBookingStatistics(query);
|
||||||
Mono<SignInStatistics> signInStatsMono = getSignInStatistics(query);
|
Mono<SignInStatistics> signInStatsMono = getSignInStatistics(query);
|
||||||
|
Mono<CoachStatistics> coachStatsMono = getCoachStatistics(query);
|
||||||
|
|
||||||
return Mono.zip(memberStatsMono, bookingStatsMono, signInStatsMono)
|
return Mono.zip(memberStatsMono, bookingStatsMono, signInStatsMono, coachStatsMono)
|
||||||
.map(tuple -> StatisticsSummary.builder()
|
.map(tuple -> StatisticsSummary.builder()
|
||||||
.statDate(LocalDateTime.now().toLocalDate().toString())
|
.statDate(LocalDateTime.now().toLocalDate().toString())
|
||||||
.memberStatistics(tuple.getT1())
|
.memberStatistics(tuple.getT1())
|
||||||
.bookingStatistics(tuple.getT2())
|
.bookingStatistics(tuple.getT2())
|
||||||
.signInStatistics(tuple.getT3())
|
.signInStatistics(tuple.getT3())
|
||||||
|
.coachStatistics(tuple.getT4())
|
||||||
.generatedAt(LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME))
|
.generatedAt(LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME))
|
||||||
.build());
|
.build());
|
||||||
}
|
}
|
||||||
|
|||||||
+7
@@ -114,4 +114,11 @@ public interface GroupCourseBookingDao extends R2dbcRepository<GroupCourseBookin
|
|||||||
@org.springframework.data.r2dbc.repository.Modifying
|
@org.springframework.data.r2dbc.repository.Modifying
|
||||||
@org.springframework.data.r2dbc.repository.Query("UPDATE group_course_booking SET status = '3', updated_at = :updatedAt WHERE id = :id AND deleted_at IS NULL")
|
@org.springframework.data.r2dbc.repository.Query("UPDATE group_course_booking SET status = '3', updated_at = :updatedAt WHERE id = :id AND deleted_at IS NULL")
|
||||||
Mono<Integer> updateToAbsent(Long id, java.time.LocalDateTime updatedAt);
|
Mono<Integer> updateToAbsent(Long id, java.time.LocalDateTime updatedAt);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量更新某课程的预约状态(教练缺席场景)
|
||||||
|
*/
|
||||||
|
@org.springframework.data.r2dbc.repository.Modifying
|
||||||
|
@org.springframework.data.r2dbc.repository.Query("UPDATE group_course_booking SET status = :newStatus, updated_at = NOW() WHERE course_id = :courseId AND status = :oldStatus AND deleted_at IS NULL")
|
||||||
|
Mono<Integer> updateStatusByCourseId(Long courseId, String oldStatus, String newStatus);
|
||||||
}
|
}
|
||||||
+123
-72
@@ -33,60 +33,110 @@ public interface GroupCourseDao extends R2dbcRepository<GroupCourseEntity, Long>
|
|||||||
@Query("UPDATE group_course SET status = '1', updated_at = :updatedAt WHERE id = :id AND deleted_at IS NULL")
|
@Query("UPDATE group_course SET status = '1', updated_at = :updatedAt WHERE id = :id AND deleted_at IS NULL")
|
||||||
Mono<Integer> cancelCourse(Long id, LocalDateTime updatedAt);
|
Mono<Integer> cancelCourse(Long id, LocalDateTime updatedAt);
|
||||||
|
|
||||||
@Modifying
|
|
||||||
@Query("UPDATE group_course SET current_members = current_members + :delta, updated_at = :updatedAt WHERE id = :id AND deleted_at IS NULL")
|
|
||||||
Mono<Integer> updateCurrentMembers(Long id, Integer delta, LocalDateTime updatedAt);
|
|
||||||
|
|
||||||
@Modifying
|
@Modifying
|
||||||
@Query("UPDATE group_course SET deleted_at = :deletedAt WHERE id = :id")
|
@Query("UPDATE group_course SET deleted_at = :deletedAt WHERE id = :id")
|
||||||
Mono<Integer> softDelete(Long id, LocalDateTime deletedAt);
|
Mono<Integer> softDelete(Long id, LocalDateTime deletedAt);
|
||||||
|
|
||||||
|
// ---------- 教练相关 SQL 方法 ----------
|
||||||
|
|
||||||
@Modifying
|
@Modifying
|
||||||
@Query("UPDATE group_course SET status = '2', updated_at = :updatedAt WHERE status = '0' AND end_time <= NOW() AND deleted_at IS NULL")
|
@Query("UPDATE group_course SET status = :status, actual_start_time = :actualStartTime, updated_at = :updatedAt WHERE id = :id AND deleted_at IS NULL")
|
||||||
Mono<Integer> completeExpiredCourses(LocalDateTime updatedAt);
|
Mono<Integer> updateStartInfo(Long id, String status, LocalDateTime actualStartTime, LocalDateTime updatedAt);
|
||||||
|
|
||||||
|
@Modifying
|
||||||
|
@Query("UPDATE group_course SET status = :status, actual_end_time = :actualEndTime, updated_at = :updatedAt WHERE id = :id AND deleted_at IS NULL")
|
||||||
|
Mono<Integer> updateEndInfo(Long id, String status, LocalDateTime actualEndTime, LocalDateTime updatedAt);
|
||||||
|
|
||||||
|
@Modifying
|
||||||
|
@Query("UPDATE group_course SET status = '5', actual_end_time = :actualEndTime, updated_at = :updatedAt WHERE id = :id AND deleted_at IS NULL")
|
||||||
|
Mono<Integer> updateToCoachAbsent(Long id, LocalDateTime actualEndTime, LocalDateTime updatedAt);
|
||||||
|
|
||||||
|
@Modifying
|
||||||
|
@Query("UPDATE group_course SET status = '6', actual_end_time = :actualEndTime, updated_at = :updatedAt WHERE id = :id AND deleted_at IS NULL")
|
||||||
|
Mono<Integer> updateToAutoEnded(Long id, LocalDateTime actualEndTime, LocalDateTime updatedAt);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询指定状态且开始时间早于指定时间的课程(用于缺席检查)
|
||||||
|
*/
|
||||||
|
default Flux<GroupCourseEntity> findByStatusAndStartTimeBefore(DatabaseClient databaseClient, String status, LocalDateTime time) {
|
||||||
|
return databaseClient.sql("SELECT * FROM group_course WHERE status = :status AND start_time < :time AND deleted_at IS NULL")
|
||||||
|
.bind("status", status)
|
||||||
|
.bind("time", time)
|
||||||
|
.map((row, meta) -> mapRowToEntity(row))
|
||||||
|
.all();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询指定状态集合且结束时间早于指定时间的课程(用于自动结课检查)
|
||||||
|
*/
|
||||||
|
default Flux<GroupCourseEntity> findByStatusInAndEndTimeBefore(DatabaseClient databaseClient, String[] statuses, LocalDateTime time) {
|
||||||
|
return databaseClient.sql("SELECT * FROM group_course WHERE status = ANY(:statuses::varchar[]) AND end_time < :time AND deleted_at IS NULL")
|
||||||
|
.bind("statuses", statuses)
|
||||||
|
.bind("time", time)
|
||||||
|
.map((row, meta) -> mapRowToEntity(row))
|
||||||
|
.all();
|
||||||
|
}
|
||||||
|
|
||||||
Flux<GroupCourseEntity> findByCourseTypeAndDeletedAtIsNull(Long courseType);
|
Flux<GroupCourseEntity> findByCourseTypeAndDeletedAtIsNull(Long courseType);
|
||||||
|
|
||||||
|
// ==================== 教练相关查询 ====================
|
||||||
|
|
||||||
|
Flux<GroupCourseEntity> findByCoachIdAndDeletedAtIsNull(Long coachId);
|
||||||
|
|
||||||
|
Flux<GroupCourseEntity> findByCoachIdAndDeletedAtIsNull(Long coachId, Sort sort);
|
||||||
|
|
||||||
|
@Query("SELECT COUNT(*) FROM group_course WHERE coach_id = :coachId AND status = :status AND deleted_at IS NULL")
|
||||||
|
Mono<Long> countByCoachIdAndStatus(Long coachId, String status);
|
||||||
|
|
||||||
|
@Modifying
|
||||||
|
@Query("UPDATE group_course SET status = '1', updated_at = :updatedAt WHERE coach_id = :coachId AND status != :excludeStatus AND deleted_at IS NULL")
|
||||||
|
Mono<Integer> cancelCoursesByCoachIdExceptStatus(Long coachId, String excludeStatus, LocalDateTime updatedAt);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 多条件查询团课(使用 DatabaseClient 构建动态 SQL)
|
* 多条件查询团课(使用 DatabaseClient 构建动态 SQL)
|
||||||
*/
|
*/
|
||||||
default Flux<GroupCourseEntity> searchGroupCourses(DatabaseClient databaseClient, GroupCourseQueryDto query) {
|
default Flux<GroupCourseEntity> searchGroupCourses(DatabaseClient databaseClient, GroupCourseQueryDto query) {
|
||||||
StringBuilder sql = new StringBuilder("SELECT * FROM group_course WHERE deleted_at IS NULL");
|
StringBuilder sql = new StringBuilder(
|
||||||
|
"SELECT gc.*, COALESCE(b.cnt, 0) AS current_members " +
|
||||||
|
"FROM group_course gc " +
|
||||||
|
"LEFT JOIN (SELECT course_id, COUNT(*) AS cnt FROM group_course_booking WHERE status = '0' GROUP BY course_id) b " +
|
||||||
|
"ON gc.id = b.course_id " +
|
||||||
|
"WHERE gc.deleted_at IS NULL");
|
||||||
List<String> conditions = new ArrayList<>();
|
List<String> conditions = new ArrayList<>();
|
||||||
|
|
||||||
// 默认不查询可预约团课(status = '0' 且未过期)
|
// 默认不查询可预约团课(status = '0' 且未过期)
|
||||||
conditions.add("status = '0'");
|
conditions.add("gc.status = '0'");
|
||||||
conditions.add("end_time > NOW()");
|
conditions.add("gc.end_time > NOW()");
|
||||||
|
|
||||||
// 1. 团课名称模糊查询
|
// 1. 团课名称模糊查询
|
||||||
if (query.getCourseName() != null && !query.getCourseName().isEmpty()) {
|
if (query.getCourseName() != null && !query.getCourseName().isEmpty()) {
|
||||||
conditions.add("course_name ILIKE :courseName");
|
conditions.add("gc.course_name ILIKE :courseName");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. 基于团课类型查询
|
// 2. 基于团课类型查询
|
||||||
if (query.getCourseType() != null) {
|
if (query.getCourseType() != null) {
|
||||||
conditions.add("course_type = :courseType");
|
conditions.add("gc.course_type = :courseType");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. 基于日期时间段查询
|
// 3. 基于日期时间段查询
|
||||||
if (query.getStartDate() != null) {
|
if (query.getStartDate() != null) {
|
||||||
conditions.add("start_time >= :startDate");
|
conditions.add("gc.start_time >= :startDate");
|
||||||
}
|
}
|
||||||
if (query.getEndDate() != null) {
|
if (query.getEndDate() != null) {
|
||||||
conditions.add("start_time <= :endDate");
|
conditions.add("gc.start_time <= :endDate");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. 基于早晨/下午/夜晚时间段查询
|
// 4. 基于早晨/下午/夜晚时间段查询
|
||||||
if (query.getTimePeriod() != null && !query.getTimePeriod().isEmpty()) {
|
if (query.getTimePeriod() != null && !query.getTimePeriod().isEmpty()) {
|
||||||
switch (query.getTimePeriod().toLowerCase()) {
|
switch (query.getTimePeriod().toLowerCase()) {
|
||||||
case "morning":
|
case "morning":
|
||||||
conditions.add("EXTRACT(HOUR FROM start_time) >= 6 AND EXTRACT(HOUR FROM start_time) < 12");
|
conditions.add("EXTRACT(HOUR FROM gc.start_time) >= 6 AND EXTRACT(HOUR FROM gc.start_time) < 12");
|
||||||
break;
|
break;
|
||||||
case "afternoon":
|
case "afternoon":
|
||||||
conditions.add("EXTRACT(HOUR FROM start_time) >= 12 AND EXTRACT(HOUR FROM start_time) < 18");
|
conditions.add("EXTRACT(HOUR FROM gc.start_time) >= 12 AND EXTRACT(HOUR FROM gc.start_time) < 18");
|
||||||
break;
|
break;
|
||||||
case "evening":
|
case "evening":
|
||||||
conditions.add("EXTRACT(HOUR FROM start_time) >= 18 AND EXTRACT(HOUR FROM start_time) < 24");
|
conditions.add("EXTRACT(HOUR FROM gc.start_time) >= 18 AND EXTRACT(HOUR FROM gc.start_time) < 24");
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
@@ -104,20 +154,20 @@ public interface GroupCourseDao extends R2dbcRepository<GroupCourseEntity, Long>
|
|||||||
List<String> orderClauses = new ArrayList<>();
|
List<String> orderClauses = new ArrayList<>();
|
||||||
|
|
||||||
if (hasRemainingMost) {
|
if (hasRemainingMost) {
|
||||||
orderClauses.add(" (max_members - current_members) DESC");
|
orderClauses.add(" (gc.max_members - COALESCE(b.cnt, 0)) DESC");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hasPriceSort) {
|
if (hasPriceSort) {
|
||||||
if ("asc".equalsIgnoreCase(query.getPriceSort())) {
|
if ("asc".equalsIgnoreCase(query.getPriceSort())) {
|
||||||
orderClauses.add(" stored_value_amount ASC");
|
orderClauses.add(" gc.stored_value_amount ASC");
|
||||||
} else if ("desc".equalsIgnoreCase(query.getPriceSort())) {
|
} else if ("desc".equalsIgnoreCase(query.getPriceSort())) {
|
||||||
orderClauses.add(" stored_value_amount DESC");
|
orderClauses.add(" gc.stored_value_amount DESC");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sql.append(String.join(",", orderClauses));
|
sql.append(String.join(",", orderClauses));
|
||||||
} else {
|
} else {
|
||||||
sql.append(" ORDER BY start_time ASC");
|
sql.append(" ORDER BY gc.start_time ASC");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 分页
|
// 分页
|
||||||
@@ -145,30 +195,7 @@ public interface GroupCourseDao extends R2dbcRepository<GroupCourseEntity, Long>
|
|||||||
spec = spec.bind("limit", size);
|
spec = spec.bind("limit", size);
|
||||||
spec = spec.bind("offset", offset);
|
spec = spec.bind("offset", offset);
|
||||||
|
|
||||||
return spec.map((row, meta) -> {
|
return spec.map((row, meta) -> mapRowToEntity(row)).all();
|
||||||
GroupCourseEntity entity = new GroupCourseEntity();
|
|
||||||
entity.setId(row.get("id", Long.class));
|
|
||||||
entity.setCourseName(row.get("course_name", String.class));
|
|
||||||
entity.setCoachId(row.get("coach_id", Long.class));
|
|
||||||
entity.setCourseType(row.get("course_type", Long.class));
|
|
||||||
entity.setStartTime(row.get("start_time", LocalDateTime.class));
|
|
||||||
entity.setEndTime(row.get("end_time", LocalDateTime.class));
|
|
||||||
entity.setMaxMembers(row.get("max_members", Integer.class));
|
|
||||||
entity.setCurrentMembers(row.get("current_members", Integer.class));
|
|
||||||
String statusStr = row.get("status", String.class);
|
|
||||||
entity.setStatus(statusStr != null ? Long.parseLong(statusStr) : null);
|
|
||||||
entity.setLocation(row.get("location", String.class));
|
|
||||||
entity.setCoverImage(row.get("cover_image", String.class));
|
|
||||||
entity.setDescription(row.get("description", String.class));
|
|
||||||
entity.setStoredValueAmount(row.get("stored_value_amount", java.math.BigDecimal.class));
|
|
||||||
entity.setQrCodePath(row.get("qr_code_path", String.class));
|
|
||||||
entity.setCreateBy(row.get("create_by", String.class));
|
|
||||||
entity.setUpdateBy(row.get("update_by", String.class));
|
|
||||||
entity.setCreatedAt(row.get("created_at", LocalDateTime.class));
|
|
||||||
entity.setUpdatedAt(row.get("updated_at", LocalDateTime.class));
|
|
||||||
entity.setDeletedAt(row.get("deleted_at", LocalDateTime.class));
|
|
||||||
return entity;
|
|
||||||
}).all();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -233,14 +260,19 @@ public interface GroupCourseDao extends R2dbcRepository<GroupCourseEntity, Long>
|
|||||||
* 分页查询团课(支持 keyword 和 status 过滤)
|
* 分页查询团课(支持 keyword 和 status 过滤)
|
||||||
*/
|
*/
|
||||||
default Flux<GroupCourseEntity> findByPageFiltered(DatabaseClient databaseClient, PageRequest pageRequest) {
|
default Flux<GroupCourseEntity> findByPageFiltered(DatabaseClient databaseClient, PageRequest pageRequest) {
|
||||||
StringBuilder sql = new StringBuilder("SELECT * FROM group_course WHERE deleted_at IS NULL");
|
StringBuilder sql = new StringBuilder(
|
||||||
|
"SELECT gc.*, COALESCE(b.cnt, 0) AS current_members " +
|
||||||
|
"FROM group_course gc " +
|
||||||
|
"LEFT JOIN (SELECT course_id, COUNT(*) AS cnt FROM group_course_booking WHERE status = '0' GROUP BY course_id) b " +
|
||||||
|
"ON gc.id = b.course_id " +
|
||||||
|
"WHERE gc.deleted_at IS NULL");
|
||||||
List<String> conditions = new ArrayList<>();
|
List<String> conditions = new ArrayList<>();
|
||||||
|
|
||||||
if (pageRequest.getKeyword() != null && !pageRequest.getKeyword().isEmpty()) {
|
if (pageRequest.getKeyword() != null && !pageRequest.getKeyword().isEmpty()) {
|
||||||
conditions.add("course_name ILIKE :keyword");
|
conditions.add("gc.course_name ILIKE :keyword");
|
||||||
}
|
}
|
||||||
if (pageRequest.getStatus() != null && !pageRequest.getStatus().isEmpty()) {
|
if (pageRequest.getStatus() != null && !pageRequest.getStatus().isEmpty()) {
|
||||||
conditions.add("status = :status");
|
conditions.add("gc.status = :status");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!conditions.isEmpty()) {
|
if (!conditions.isEmpty()) {
|
||||||
@@ -249,7 +281,7 @@ public interface GroupCourseDao extends R2dbcRepository<GroupCourseEntity, Long>
|
|||||||
|
|
||||||
String sort = pageRequest.getSort() != null ? pageRequest.getSort() : "id";
|
String sort = pageRequest.getSort() != null ? pageRequest.getSort() : "id";
|
||||||
String order = "desc".equalsIgnoreCase(pageRequest.getOrder()) ? "DESC" : "ASC";
|
String order = "desc".equalsIgnoreCase(pageRequest.getOrder()) ? "DESC" : "ASC";
|
||||||
sql.append(" ORDER BY ").append(sort).append(" ").append(order);
|
sql.append(" ORDER BY gc.").append(sort).append(" ").append(order);
|
||||||
|
|
||||||
int size = pageRequest.getSize();
|
int size = pageRequest.getSize();
|
||||||
if (size < 1) size = 10;
|
if (size < 1) size = 10;
|
||||||
@@ -268,30 +300,7 @@ public interface GroupCourseDao extends R2dbcRepository<GroupCourseEntity, Long>
|
|||||||
spec = spec.bind("limit", size);
|
spec = spec.bind("limit", size);
|
||||||
spec = spec.bind("offset", offset);
|
spec = spec.bind("offset", offset);
|
||||||
|
|
||||||
return spec.map((row, meta) -> {
|
return spec.map((row, meta) -> mapRowToEntity(row)).all();
|
||||||
GroupCourseEntity entity = new GroupCourseEntity();
|
|
||||||
entity.setId(row.get("id", Long.class));
|
|
||||||
entity.setCourseName(row.get("course_name", String.class));
|
|
||||||
entity.setCoachId(row.get("coach_id", Long.class));
|
|
||||||
entity.setCourseType(row.get("course_type", Long.class));
|
|
||||||
entity.setStartTime(row.get("start_time", LocalDateTime.class));
|
|
||||||
entity.setEndTime(row.get("end_time", LocalDateTime.class));
|
|
||||||
entity.setMaxMembers(row.get("max_members", Integer.class));
|
|
||||||
entity.setCurrentMembers(row.get("current_members", Integer.class));
|
|
||||||
String statusStr = row.get("status", String.class);
|
|
||||||
entity.setStatus(statusStr != null ? Long.parseLong(statusStr) : null);
|
|
||||||
entity.setLocation(row.get("location", String.class));
|
|
||||||
entity.setCoverImage(row.get("cover_image", String.class));
|
|
||||||
entity.setDescription(row.get("description", String.class));
|
|
||||||
entity.setStoredValueAmount(row.get("stored_value_amount", java.math.BigDecimal.class));
|
|
||||||
entity.setQrCodePath(row.get("qr_code_path", String.class));
|
|
||||||
entity.setCreateBy(row.get("create_by", String.class));
|
|
||||||
entity.setUpdateBy(row.get("update_by", String.class));
|
|
||||||
entity.setCreatedAt(row.get("created_at", LocalDateTime.class));
|
|
||||||
entity.setUpdatedAt(row.get("updated_at", LocalDateTime.class));
|
|
||||||
entity.setDeletedAt(row.get("deleted_at", LocalDateTime.class));
|
|
||||||
return entity;
|
|
||||||
}).all();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -323,4 +332,46 @@ public interface GroupCourseDao extends R2dbcRepository<GroupCourseEntity, Long>
|
|||||||
|
|
||||||
return spec.map((row, meta) -> row.get(0, Long.class)).one();
|
return spec.map((row, meta) -> row.get(0, Long.class)).one();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 统计单门课程的有效预约人数(status='0')
|
||||||
|
*/
|
||||||
|
default Mono<Long> countValidBookings(DatabaseClient databaseClient, Long courseId) {
|
||||||
|
return databaseClient.sql("SELECT COUNT(*) FROM group_course_booking WHERE course_id = :id AND status = '0'")
|
||||||
|
.bind("id", courseId)
|
||||||
|
.map((row, meta) -> row.get(0, Long.class))
|
||||||
|
.one()
|
||||||
|
.defaultIfEmpty(0L);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将数据库行映射为 GroupCourseEntity
|
||||||
|
*/
|
||||||
|
private GroupCourseEntity mapRowToEntity(io.r2dbc.spi.Row row) {
|
||||||
|
GroupCourseEntity entity = new GroupCourseEntity();
|
||||||
|
entity.setId(row.get("id", Long.class));
|
||||||
|
entity.setCourseName(row.get("course_name", String.class));
|
||||||
|
entity.setCoachId(row.get("coach_id", Long.class));
|
||||||
|
entity.setCourseType(row.get("course_type", Long.class));
|
||||||
|
entity.setStartTime(row.get("start_time", LocalDateTime.class));
|
||||||
|
entity.setEndTime(row.get("end_time", LocalDateTime.class));
|
||||||
|
entity.setMaxMembers(row.get("max_members", Integer.class));
|
||||||
|
Integer cm = row.get("current_members", Integer.class);
|
||||||
|
entity.setCurrentMembers(cm != null ? cm : 0);
|
||||||
|
String statusStr = row.get("status", String.class);
|
||||||
|
entity.setStatus(statusStr != null ? Long.parseLong(statusStr) : null);
|
||||||
|
entity.setLocation(row.get("location", String.class));
|
||||||
|
entity.setCoverImage(row.get("cover_image", String.class));
|
||||||
|
entity.setDescription(row.get("description", String.class));
|
||||||
|
entity.setStoredValueAmount(row.get("stored_value_amount", java.math.BigDecimal.class));
|
||||||
|
entity.setActualStartTime(row.get("actual_start_time", LocalDateTime.class));
|
||||||
|
entity.setActualEndTime(row.get("actual_end_time", LocalDateTime.class));
|
||||||
|
entity.setQrCodePath(row.get("qr_code_path", String.class));
|
||||||
|
entity.setCreateBy(row.get("create_by", String.class));
|
||||||
|
entity.setUpdateBy(row.get("update_by", String.class));
|
||||||
|
entity.setCreatedAt(row.get("created_at", LocalDateTime.class));
|
||||||
|
entity.setUpdatedAt(row.get("updated_at", LocalDateTime.class));
|
||||||
|
entity.setDeletedAt(row.get("deleted_at", LocalDateTime.class));
|
||||||
|
return entity;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+24
@@ -40,6 +40,14 @@ public class GroupCourse extends BaseDomain{
|
|||||||
@Schema(description = "课程状态", example = "0")
|
@Schema(description = "课程状态", example = "0")
|
||||||
private Long status;
|
private Long status;
|
||||||
|
|
||||||
|
//实际开课时间
|
||||||
|
@Schema(description = "实际开课时间")
|
||||||
|
private LocalDateTime actualStartTime;
|
||||||
|
|
||||||
|
//实际结课时间
|
||||||
|
@Schema(description = "实际结课时间")
|
||||||
|
private LocalDateTime actualEndTime;
|
||||||
|
|
||||||
//上课地点
|
//上课地点
|
||||||
@Schema(description = "上课地点", example = "龙泉驿区幸福路")
|
@Schema(description = "上课地点", example = "龙泉驿区幸福路")
|
||||||
private String location;
|
private String location;
|
||||||
@@ -124,6 +132,22 @@ public class GroupCourse extends BaseDomain{
|
|||||||
this.status = status;
|
this.status = status;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public LocalDateTime getActualStartTime() {
|
||||||
|
return actualStartTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setActualStartTime(LocalDateTime actualStartTime) {
|
||||||
|
this.actualStartTime = actualStartTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LocalDateTime getActualEndTime() {
|
||||||
|
return actualEndTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setActualEndTime(LocalDateTime actualEndTime) {
|
||||||
|
this.actualEndTime = actualEndTime;
|
||||||
|
}
|
||||||
|
|
||||||
public String getLocation() {
|
public String getLocation() {
|
||||||
return location;
|
return location;
|
||||||
}
|
}
|
||||||
|
|||||||
+11
@@ -21,6 +21,9 @@ public class GroupCourseDetail extends BaseDomain {
|
|||||||
@Schema(description = "教练ID", example = "1")
|
@Schema(description = "教练ID", example = "1")
|
||||||
private Long coachId;
|
private Long coachId;
|
||||||
|
|
||||||
|
@Schema(description = "教练名称", example = "张教练")
|
||||||
|
private String coachName;
|
||||||
|
|
||||||
@Schema(description = "课程类型ID", example = "1")
|
@Schema(description = "课程类型ID", example = "1")
|
||||||
private Long courseType;
|
private Long courseType;
|
||||||
|
|
||||||
@@ -99,6 +102,14 @@ public class GroupCourseDetail extends BaseDomain {
|
|||||||
this.coachId = coachId;
|
this.coachId = coachId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getCoachName() {
|
||||||
|
return coachName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCoachName(String coachName) {
|
||||||
|
this.coachName = coachName;
|
||||||
|
}
|
||||||
|
|
||||||
public Long getCourseType() {
|
public Long getCourseType() {
|
||||||
return courseType;
|
return courseType;
|
||||||
}
|
}
|
||||||
|
|||||||
+25
-1
@@ -38,7 +38,15 @@ public class GroupCourseEntity extends BaseEntity {
|
|||||||
@Column("current_members")
|
@Column("current_members")
|
||||||
private Integer currentMembers;
|
private Integer currentMembers;
|
||||||
|
|
||||||
//课程状态:0-正常,1-已取消,2-已结束
|
//实际开课时间
|
||||||
|
@Column("actual_start_time")
|
||||||
|
private LocalDateTime actualStartTime;
|
||||||
|
|
||||||
|
//实际结课时间
|
||||||
|
@Column("actual_end_time")
|
||||||
|
private LocalDateTime actualEndTime;
|
||||||
|
|
||||||
|
//课程状态:0-正常,1-已取消,2-已结束,3-进行中,5-教练缺席,6-自动结束,7-教练迟到
|
||||||
@Column("status")
|
@Column("status")
|
||||||
private Long status;
|
private Long status;
|
||||||
|
|
||||||
@@ -158,6 +166,22 @@ public class GroupCourseEntity extends BaseEntity {
|
|||||||
this.storedValueAmount = storedValueAmount;
|
this.storedValueAmount = storedValueAmount;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public LocalDateTime getActualStartTime() {
|
||||||
|
return actualStartTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setActualStartTime(LocalDateTime actualStartTime) {
|
||||||
|
this.actualStartTime = actualStartTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LocalDateTime getActualEndTime() {
|
||||||
|
return actualEndTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setActualEndTime(LocalDateTime actualEndTime) {
|
||||||
|
this.actualEndTime = actualEndTime;
|
||||||
|
}
|
||||||
|
|
||||||
public String getQrCodePath() {
|
public String getQrCodePath() {
|
||||||
return qrCodePath;
|
return qrCodePath;
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-1
@@ -11,7 +11,10 @@ public enum CourseStatus {
|
|||||||
NORMAL(0L, "正常"),
|
NORMAL(0L, "正常"),
|
||||||
CANCELLED(1L, "已取消"),
|
CANCELLED(1L, "已取消"),
|
||||||
ENDED(2L, "已结束"),
|
ENDED(2L, "已结束"),
|
||||||
IN_PROGRESS(3L, "进行中");
|
IN_PROGRESS(3L, "进行中"),
|
||||||
|
COACH_ABSENT(5L, "教练缺席"),
|
||||||
|
AUTO_ENDED(6L, "自动结束"),
|
||||||
|
COACH_LATE(7L, "教练迟到");
|
||||||
|
|
||||||
private final Long value;
|
private final Long value;
|
||||||
private final String desc;
|
private final String desc;
|
||||||
|
|||||||
-56
@@ -2,8 +2,6 @@ package cn.novalon.gym.manage.groupcourse.handler;
|
|||||||
|
|
||||||
import cn.novalon.gym.manage.groupcourse.domain.GroupCourseBooking;
|
import cn.novalon.gym.manage.groupcourse.domain.GroupCourseBooking;
|
||||||
import cn.novalon.gym.manage.groupcourse.repository.IGroupCourseBookingRepository;
|
import cn.novalon.gym.manage.groupcourse.repository.IGroupCourseBookingRepository;
|
||||||
import cn.novalon.gym.manage.groupcourse.repository.IGroupCourseRepository;
|
|
||||||
import cn.novalon.gym.manage.groupcourse.service.impl.GroupCourseRedisService;
|
|
||||||
import cn.novalon.gym.manage.member.entity.MemberCard;
|
import cn.novalon.gym.manage.member.entity.MemberCard;
|
||||||
import cn.novalon.gym.manage.member.entity.MemberCardRecord;
|
import cn.novalon.gym.manage.member.entity.MemberCardRecord;
|
||||||
import cn.novalon.gym.manage.member.enums.MemberCardType;
|
import cn.novalon.gym.manage.member.enums.MemberCardType;
|
||||||
@@ -34,11 +32,9 @@ import java.util.List;
|
|||||||
public class BookingSagaHandler {
|
public class BookingSagaHandler {
|
||||||
|
|
||||||
private final IGroupCourseBookingRepository bookingRepository;
|
private final IGroupCourseBookingRepository bookingRepository;
|
||||||
private final IGroupCourseRepository courseRepository;
|
|
||||||
private final IMemberCardRecordService memberCardRecordService;
|
private final IMemberCardRecordService memberCardRecordService;
|
||||||
private final IMemberStoredCardService memberStoredCardService;
|
private final IMemberStoredCardService memberStoredCardService;
|
||||||
private final MemberCardRepository memberCardRepository;
|
private final MemberCardRepository memberCardRepository;
|
||||||
private final GroupCourseRedisService redisService;
|
|
||||||
|
|
||||||
private static final double DEFAULT_GROUP_COURSE_PRICE = 50.0;
|
private static final double DEFAULT_GROUP_COURSE_PRICE = 50.0;
|
||||||
|
|
||||||
@@ -72,24 +68,6 @@ public class BookingSagaHandler {
|
|||||||
steps.add(step2);
|
steps.add(step2);
|
||||||
rollbackSteps.add(0, step2);
|
rollbackSteps.add(0, step2);
|
||||||
|
|
||||||
// 步骤3:更新课程当前人数
|
|
||||||
SagaStep step3 = new SagaStep(
|
|
||||||
"更新课程当前人数",
|
|
||||||
incrementCourseCurrentMembers(booking.getCourseId()),
|
|
||||||
Mono.defer(() -> decrementCourseCurrentMembers(booking.getCourseId()))
|
|
||||||
);
|
|
||||||
steps.add(step3);
|
|
||||||
rollbackSteps.add(0, step3);
|
|
||||||
|
|
||||||
// 步骤4:更新Redis预约计数
|
|
||||||
SagaStep step4 = new SagaStep(
|
|
||||||
"更新Redis预约计数",
|
|
||||||
incrementRedisBookingCount(booking.getCourseId()),
|
|
||||||
Mono.defer(() -> decrementRedisBookingCount(booking.getCourseId()))
|
|
||||||
);
|
|
||||||
steps.add(step4);
|
|
||||||
rollbackSteps.add(0, step4);
|
|
||||||
|
|
||||||
return executeSaga(steps, rollbackSteps)
|
return executeSaga(steps, rollbackSteps)
|
||||||
.then(Mono.just(booking));
|
.then(Mono.just(booking));
|
||||||
}
|
}
|
||||||
@@ -225,24 +203,6 @@ public class BookingSagaHandler {
|
|||||||
steps.add(step2);
|
steps.add(step2);
|
||||||
rollbackSteps.add(0, step2);
|
rollbackSteps.add(0, step2);
|
||||||
|
|
||||||
// 步骤3:减少课程当前人数
|
|
||||||
SagaStep step3 = new SagaStep(
|
|
||||||
"减少课程当前人数",
|
|
||||||
decrementCourseCurrentMembers(courseId),
|
|
||||||
Mono.defer(() -> incrementCourseCurrentMembers(courseId))
|
|
||||||
);
|
|
||||||
steps.add(step3);
|
|
||||||
rollbackSteps.add(0, step3);
|
|
||||||
|
|
||||||
// 步骤4:更新Redis预约计数
|
|
||||||
SagaStep step4 = new SagaStep(
|
|
||||||
"更新Redis预约计数",
|
|
||||||
decrementRedisBookingCount(courseId),
|
|
||||||
Mono.defer(() -> incrementRedisBookingCount(courseId))
|
|
||||||
);
|
|
||||||
steps.add(step4);
|
|
||||||
rollbackSteps.add(0, step4);
|
|
||||||
|
|
||||||
return executeSaga(steps, rollbackSteps)
|
return executeSaga(steps, rollbackSteps)
|
||||||
.then(bookingRepository.findById(bookingId));
|
.then(bookingRepository.findById(bookingId));
|
||||||
}
|
}
|
||||||
@@ -301,22 +261,6 @@ public class BookingSagaHandler {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private Mono<Void> incrementCourseCurrentMembers(Long courseId) {
|
|
||||||
return courseRepository.updateCurrentMembers(courseId, 1).then();
|
|
||||||
}
|
|
||||||
|
|
||||||
private Mono<Void> decrementCourseCurrentMembers(Long courseId) {
|
|
||||||
return courseRepository.updateCurrentMembers(courseId, -1).then();
|
|
||||||
}
|
|
||||||
|
|
||||||
private Mono<Void> incrementRedisBookingCount(Long courseId) {
|
|
||||||
return redisService.incrementBookingCount(courseId).then();
|
|
||||||
}
|
|
||||||
|
|
||||||
private Mono<Void> decrementRedisBookingCount(Long courseId) {
|
|
||||||
return redisService.decrementBookingCount(courseId).then();
|
|
||||||
}
|
|
||||||
|
|
||||||
private Mono<Void> executeSaga(List<SagaStep> steps, List<SagaStep> rollbackSteps) {
|
private Mono<Void> executeSaga(List<SagaStep> steps, List<SagaStep> rollbackSteps) {
|
||||||
List<SagaStep> completedSteps = new ArrayList<>();
|
List<SagaStep> completedSteps = new ArrayList<>();
|
||||||
return executeStep(steps, 0, completedSteps);
|
return executeStep(steps, 0, completedSteps);
|
||||||
|
|||||||
+57
-4
@@ -7,21 +7,27 @@ import cn.novalon.gym.manage.groupcourse.domain.GroupCourse;
|
|||||||
import cn.novalon.gym.manage.groupcourse.domain.GroupCourseDetail;
|
import cn.novalon.gym.manage.groupcourse.domain.GroupCourseDetail;
|
||||||
import cn.novalon.gym.manage.groupcourse.dto.GroupCourseQueryDto;
|
import cn.novalon.gym.manage.groupcourse.dto.GroupCourseQueryDto;
|
||||||
import cn.novalon.gym.manage.groupcourse.service.IGroupCourseService;
|
import cn.novalon.gym.manage.groupcourse.service.IGroupCourseService;
|
||||||
|
import cn.novalon.gym.manage.groupcourse.vo.GroupCourseVO;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import jakarta.validation.Validator;
|
import jakarta.validation.Validator;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
import org.springframework.web.reactive.function.server.ServerRequest;
|
import org.springframework.web.reactive.function.server.ServerRequest;
|
||||||
import org.springframework.web.reactive.function.server.ServerResponse;
|
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||||
import reactor.core.publisher.Mono;
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
@Tag(name="团课管理",description = "团课相关操作")
|
@Tag(name="团课管理",description = "团课相关操作")
|
||||||
public class GroupCourseHandler {
|
public class GroupCourseHandler {
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(GroupCourseHandler.class);
|
||||||
private final IGroupCourseService groupCourseService;
|
private final IGroupCourseService groupCourseService;
|
||||||
private final Validator validator;
|
private final Validator validator;
|
||||||
private final RedisUtil redisUtil;
|
private final RedisUtil redisUtil;
|
||||||
@@ -37,11 +43,12 @@ public class GroupCourseHandler {
|
|||||||
this.objectMapper = objectMapper;
|
this.objectMapper = objectMapper;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Operation(summary = "获取所有团课", description = "获取系统中所有团课列表")
|
@Operation(summary = "获取所有团课", description = "获取系统中所有团课列表(含教练昵称)")
|
||||||
public Mono<ServerResponse> getAllGroupCourse(ServerRequest request){
|
public Mono<ServerResponse> getAllGroupCourse(ServerRequest request){
|
||||||
boolean includeDeleted = Boolean.valueOf(request.queryParam("includeDeleted").orElse("false"));
|
boolean includeDeleted = Boolean.valueOf(request.queryParam("includeDeleted").orElse("false"));
|
||||||
return ServerResponse.ok()
|
return groupCourseService.findAllAsVO(includeDeleted)
|
||||||
.body(groupCourseService.findAll(includeDeleted), GroupCourse.class);
|
.collectList()
|
||||||
|
.flatMap(list -> ServerResponse.ok().bodyValue(list));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Operation(summary = "分页获取团课", description = "根据分页参数获取团课列表")
|
@Operation(summary = "分页获取团课", description = "根据分页参数获取团课列表")
|
||||||
@@ -65,7 +72,7 @@ public class GroupCourseHandler {
|
|||||||
pageRequest.setOrder("asc");
|
pageRequest.setOrder("asc");
|
||||||
}
|
}
|
||||||
|
|
||||||
return groupCourseService.findByPage(pageRequest, includeDeleted)
|
return groupCourseService.findByPageAsVO(pageRequest, includeDeleted)
|
||||||
.flatMap(response -> ServerResponse.ok().bodyValue(response));
|
.flatMap(response -> ServerResponse.ok().bodyValue(response));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -289,4 +296,50 @@ public class GroupCourseHandler {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "检查教练时间冲突", description = "检查教练在指定时间段内是否有时间冲突的团课")
|
||||||
|
public Mono<ServerResponse> checkCoachConflict(ServerRequest request) {
|
||||||
|
return request.bodyToMono(Map.class)
|
||||||
|
.flatMap(body -> {
|
||||||
|
if (body == null) {
|
||||||
|
Map<String, Object> error = new HashMap<>();
|
||||||
|
error.put("success", false);
|
||||||
|
error.put("message", "请求体不能为空");
|
||||||
|
return ServerResponse.badRequest().bodyValue(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
Object coachIdObj = body.get("coachId");
|
||||||
|
Object startTimeStr = body.get("startTime");
|
||||||
|
Object endTimeStr = body.get("endTime");
|
||||||
|
Object excludeCourseIdObj = body.get("excludeCourseId");
|
||||||
|
|
||||||
|
if (coachIdObj == null || startTimeStr == null || endTimeStr == null) {
|
||||||
|
Map<String, Object> error = new HashMap<>();
|
||||||
|
error.put("success", false);
|
||||||
|
error.put("message", "coachId、startTime、endTime 不能为空");
|
||||||
|
return ServerResponse.badRequest().bodyValue(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
Long coachId = Long.valueOf(String.valueOf(coachIdObj));
|
||||||
|
LocalDateTime startTime = LocalDateTime.parse(String.valueOf(startTimeStr));
|
||||||
|
LocalDateTime endTime = LocalDateTime.parse(String.valueOf(endTimeStr));
|
||||||
|
Long excludeCourseId = excludeCourseIdObj != null ? Long.valueOf(String.valueOf(excludeCourseIdObj)) : null;
|
||||||
|
|
||||||
|
return groupCourseService.checkCoachConflict(coachId, startTime, endTime, excludeCourseId)
|
||||||
|
.flatMap(conflicts -> {
|
||||||
|
Map<String, Object> response = new HashMap<>();
|
||||||
|
response.put("success", true);
|
||||||
|
response.put("hasConflict", !conflicts.isEmpty());
|
||||||
|
response.put("conflicts", conflicts);
|
||||||
|
return ServerResponse.ok().bodyValue(response);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.onErrorResume(error -> {
|
||||||
|
logger.warn("检查教练时间冲突失败: {}", error.getMessage());
|
||||||
|
Map<String, Object> response = new HashMap<>();
|
||||||
|
response.put("success", false);
|
||||||
|
response.put("message", "检查失败: " + error.getMessage());
|
||||||
|
return ServerResponse.badRequest().bodyValue(response);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+7
-2
@@ -27,9 +27,14 @@ public interface IGroupCourseRepository {
|
|||||||
|
|
||||||
Mono<Void> deleteById(Long id);
|
Mono<Void> deleteById(Long id);
|
||||||
|
|
||||||
Mono<GroupCourse> updateCurrentMembers(Long id, Integer delta);
|
|
||||||
|
|
||||||
Flux<GroupCourse> findByCourseType(Long courseType);
|
Flux<GroupCourse> findByCourseType(Long courseType);
|
||||||
|
|
||||||
Mono<PageResponse<GroupCourse>> searchGroupCourses(GroupCourseQueryDto query);
|
Mono<PageResponse<GroupCourse>> searchGroupCourses(GroupCourseQueryDto query);
|
||||||
|
|
||||||
|
// ==================== 教练相关 ====================
|
||||||
|
|
||||||
|
Flux<GroupCourse> findByCoachId(Long coachId);
|
||||||
|
Flux<GroupCourse> findByCoachId(Long coachId, Sort sort);
|
||||||
|
Mono<Long> countByCoachIdAndStatus(Long coachId, Long status);
|
||||||
|
Mono<Integer> cancelCoursesByCoachIdExceptStatus(Long coachId, Long excludeStatus);
|
||||||
}
|
}
|
||||||
|
|||||||
+24
-12
@@ -119,7 +119,6 @@ public class GroupCourseRepository implements IGroupCourseRepository {
|
|||||||
entity.setCreatedAt(LocalDateTime.now());
|
entity.setCreatedAt(LocalDateTime.now());
|
||||||
entity.setUpdatedAt(LocalDateTime.now());
|
entity.setUpdatedAt(LocalDateTime.now());
|
||||||
entity.setStatus(0L);
|
entity.setStatus(0L);
|
||||||
entity.setCurrentMembers(0);
|
|
||||||
|
|
||||||
return groupCourseDao.save(entity)
|
return groupCourseDao.save(entity)
|
||||||
.map(groupCourseConverter::toDomain);
|
.map(groupCourseConverter::toDomain);
|
||||||
@@ -151,17 +150,6 @@ public class GroupCourseRepository implements IGroupCourseRepository {
|
|||||||
.then();
|
.then();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public Mono<GroupCourse> updateCurrentMembers(Long id, Integer delta) {
|
|
||||||
return groupCourseDao.updateCurrentMembers(id, delta, LocalDateTime.now())
|
|
||||||
.flatMap(updated -> {
|
|
||||||
if (updated > 0) {
|
|
||||||
return findByIdAndDeletedAtIsNull(id);
|
|
||||||
}
|
|
||||||
return Mono.empty();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Flux<GroupCourse> findByCourseType(Long courseType) {
|
public Flux<GroupCourse> findByCourseType(Long courseType) {
|
||||||
return groupCourseDao.findByCourseTypeAndDeletedAtIsNull(courseType)
|
return groupCourseDao.findByCourseTypeAndDeletedAtIsNull(courseType)
|
||||||
@@ -190,4 +178,28 @@ public class GroupCourseRepository implements IGroupCourseRepository {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==================== 教练相关 ====================
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Flux<GroupCourse> findByCoachId(Long coachId) {
|
||||||
|
return groupCourseDao.findByCoachIdAndDeletedAtIsNull(coachId)
|
||||||
|
.map(groupCourseConverter::toDomain);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Flux<GroupCourse> findByCoachId(Long coachId, Sort sort) {
|
||||||
|
return groupCourseDao.findByCoachIdAndDeletedAtIsNull(coachId, sort)
|
||||||
|
.map(groupCourseConverter::toDomain);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Mono<Long> countByCoachIdAndStatus(Long coachId, Long status) {
|
||||||
|
return groupCourseDao.countByCoachIdAndStatus(coachId, String.valueOf(status));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Mono<Integer> cancelCoursesByCoachIdExceptStatus(Long coachId, Long excludeStatus) {
|
||||||
|
return groupCourseDao.cancelCoursesByCoachIdExceptStatus(coachId, String.valueOf(excludeStatus), LocalDateTime.now());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
-46
@@ -1,46 +0,0 @@
|
|||||||
package cn.novalon.gym.manage.groupcourse.scheduler;
|
|
||||||
|
|
||||||
import cn.novalon.gym.manage.groupcourse.dao.GroupCourseDao;
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
import org.springframework.scheduling.annotation.Scheduled;
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 团课过期状态自动更新定时任务
|
|
||||||
*
|
|
||||||
* 功能:定期检查已过期的团课(end_time <= NOW()),自动将 status 从 0 更新为 2(已结束)
|
|
||||||
*
|
|
||||||
* @date 2026-06-24
|
|
||||||
*/
|
|
||||||
@Component
|
|
||||||
public class GroupCourseExpireScheduler {
|
|
||||||
|
|
||||||
private static final Logger logger = LoggerFactory.getLogger(GroupCourseExpireScheduler.class);
|
|
||||||
|
|
||||||
private final GroupCourseDao groupCourseDao;
|
|
||||||
|
|
||||||
public GroupCourseExpireScheduler(GroupCourseDao groupCourseDao) {
|
|
||||||
this.groupCourseDao = groupCourseDao;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 每分钟检查一次,将已过期但状态仍为 0 的团课标记为已结束(status = 2)
|
|
||||||
*/
|
|
||||||
@Scheduled(fixedRate = 60000)
|
|
||||||
public void completeExpiredCourses() {
|
|
||||||
logger.debug("定时任务开始检查已过期团课,更新状态为已结束");
|
|
||||||
|
|
||||||
groupCourseDao.completeExpiredCourses(LocalDateTime.now())
|
|
||||||
.subscribe(
|
|
||||||
count -> {
|
|
||||||
if (count > 0) {
|
|
||||||
logger.info("定时任务完成,更新了 {} 条过期团课状态为已结束", count);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
error -> logger.error("过期团课状态更新定时任务执行失败:{}", error.getMessage(), error)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+16
@@ -6,14 +6,20 @@ import cn.novalon.gym.manage.common.dto.PageResponse;
|
|||||||
import cn.novalon.gym.manage.groupcourse.domain.GroupCourse;
|
import cn.novalon.gym.manage.groupcourse.domain.GroupCourse;
|
||||||
import cn.novalon.gym.manage.groupcourse.domain.GroupCourseDetail;
|
import cn.novalon.gym.manage.groupcourse.domain.GroupCourseDetail;
|
||||||
import cn.novalon.gym.manage.groupcourse.dto.GroupCourseQueryDto;
|
import cn.novalon.gym.manage.groupcourse.dto.GroupCourseQueryDto;
|
||||||
|
import cn.novalon.gym.manage.groupcourse.vo.GroupCourseVO;
|
||||||
import reactor.core.publisher.Flux;
|
import reactor.core.publisher.Flux;
|
||||||
import reactor.core.publisher.Mono;
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
public interface IGroupCourseService {
|
public interface IGroupCourseService {
|
||||||
Mono<GroupCourse> findById(Long id);
|
Mono<GroupCourse> findById(Long id);
|
||||||
Mono<GroupCourseDetail> findDetailById(Long id);
|
Mono<GroupCourseDetail> findDetailById(Long id);
|
||||||
Flux<GroupCourse> findAll();
|
Flux<GroupCourse> findAll();
|
||||||
Flux<GroupCourse> findAll(boolean includeDeleted);
|
Flux<GroupCourse> findAll(boolean includeDeleted);
|
||||||
|
Flux<GroupCourseVO> findAllAsVO(boolean includeDeleted);
|
||||||
|
Mono<PageResponse<GroupCourseVO>> findByPageAsVO(PageRequest pageRequest, boolean includeDeleted);
|
||||||
|
|
||||||
Mono<PageResponse<GroupCourse>> findByPage(PageRequest pageRequest, boolean includeDeleted);
|
Mono<PageResponse<GroupCourse>> findByPage(PageRequest pageRequest, boolean includeDeleted);
|
||||||
|
|
||||||
@@ -28,4 +34,14 @@ public interface IGroupCourseService {
|
|||||||
Mono<Void> delete(Long id);
|
Mono<Void> delete(Long id);
|
||||||
|
|
||||||
Mono<PageResponse<GroupCourse>> searchGroupCourses(GroupCourseQueryDto query);
|
Mono<PageResponse<GroupCourse>> searchGroupCourses(GroupCourseQueryDto query);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查教练在指定时间段内是否有时间冲突的团课
|
||||||
|
* @param coachId 教练ID
|
||||||
|
* @param startTime 开始时间
|
||||||
|
* @param endTime 结束时间
|
||||||
|
* @param excludeCourseId 排除的课程ID(编辑时排除自身)
|
||||||
|
* @return 冲突的课程列表
|
||||||
|
*/
|
||||||
|
Mono<List<GroupCourse>> checkCoachConflict(Long coachId, LocalDateTime startTime, LocalDateTime endTime, Long excludeCourseId);
|
||||||
}
|
}
|
||||||
|
|||||||
+16
-31
@@ -72,8 +72,16 @@ public class GroupCourseBookingService implements IGroupCourseBookingService {
|
|||||||
return Mono.error(new RuntimeException("系统繁忙,请稍后重试"));
|
return Mono.error(new RuntimeException("系统繁忙,请稍后重试"));
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. 尝试从缓存获取课程信息,如果缓存不存在则从数据库获取
|
// 2. 从缓存或数据库获取课程信息,并用实时预约数覆盖currentMembers
|
||||||
return getCourseWithCache(courseId)
|
return getCourseWithCache(courseId)
|
||||||
|
.flatMap(course ->
|
||||||
|
bookingRepository.countValidBookings(courseId)
|
||||||
|
.map(count -> {
|
||||||
|
course.setCurrentMembers(count.intValue());
|
||||||
|
return course;
|
||||||
|
})
|
||||||
|
.defaultIfEmpty(course)
|
||||||
|
)
|
||||||
.flatMap(course -> {
|
.flatMap(course -> {
|
||||||
// 3. 验证课程状态
|
// 3. 验证课程状态
|
||||||
Long courseStatus = course.getStatus();
|
Long courseStatus = course.getStatus();
|
||||||
@@ -150,35 +158,20 @@ public class GroupCourseBookingService implements IGroupCourseBookingService {
|
|||||||
if (saved.getId() == null) {
|
if (saved.getId() == null) {
|
||||||
return Mono.error(new RuntimeException("保存预约记录失败"));
|
return Mono.error(new RuntimeException("保存预约记录失败"));
|
||||||
}
|
}
|
||||||
// 10. 更新课程当前人数
|
// 10. 释放锁
|
||||||
return courseRepository.updateCurrentMembers(courseId, 1)
|
|
||||||
.flatMap(updatedCourse -> {
|
|
||||||
// 11. 更新Redis预约计数
|
|
||||||
return redisService.incrementBookingCount(courseId)
|
|
||||||
.flatMap(count -> {
|
|
||||||
// 12. 释放锁
|
|
||||||
return redisService.releaseLock(courseId, requestId)
|
return redisService.releaseLock(courseId, requestId)
|
||||||
.then(Mono.just(saved));
|
.then(Mono.just(saved));
|
||||||
});
|
|
||||||
})
|
})
|
||||||
.doOnSuccess(savedBooking -> {
|
.doOnSuccess(savedBooking -> {
|
||||||
logger.info("预约成功:bookingId={}, courseId={}, memberId={}",
|
logger.info("预约成功:bookingId={}, courseId={}, memberId={}",
|
||||||
saved.getId(), courseId, memberId);
|
savedBooking.getId(), courseId, memberId);
|
||||||
// 发布预约成功事件
|
// 发布预约成功事件
|
||||||
bookingReminderEventPublisher.publishBookingSuccessEvent(
|
bookingReminderEventPublisher.publishBookingSuccessEvent(
|
||||||
saved.getId(),
|
savedBooking.getId(),
|
||||||
saved.getMemberId(),
|
savedBooking.getMemberId(),
|
||||||
saved.getCourseName(),
|
savedBooking.getCourseName(),
|
||||||
saved.getCourseStartTime().toString()
|
savedBooking.getCourseStartTime().toString()
|
||||||
);
|
);
|
||||||
})
|
|
||||||
.doOnError(error -> {
|
|
||||||
// 失败时回滚Redis计数和课程人数
|
|
||||||
redisService.decrementBookingCount(courseId).subscribe();
|
|
||||||
courseRepository.updateCurrentMembers(courseId, -1).subscribe();
|
|
||||||
logger.error("预约失败:courseId={}, memberId={}, error={}",
|
|
||||||
courseId, memberId, error.getMessage());
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@@ -273,16 +266,9 @@ public class GroupCourseBookingService implements IGroupCourseBookingService {
|
|||||||
if (rows == 0) {
|
if (rows == 0) {
|
||||||
return Mono.error(new RuntimeException("更新预约状态失败"));
|
return Mono.error(new RuntimeException("更新预约状态失败"));
|
||||||
}
|
}
|
||||||
// 6. 减少课程当前人数
|
// 6. 释放锁
|
||||||
return courseRepository.updateCurrentMembers(booking.getCourseId(), -1)
|
|
||||||
.flatMap(updatedCourse -> {
|
|
||||||
// 7. 更新Redis预约计数
|
|
||||||
return redisService.decrementBookingCount(booking.getCourseId())
|
|
||||||
.flatMap(count -> {
|
|
||||||
// 8. 释放锁
|
|
||||||
return redisService.releaseLock(bookingId, requestId)
|
return redisService.releaseLock(bookingId, requestId)
|
||||||
.then(bookingRepository.findById(bookingId));
|
.then(bookingRepository.findById(bookingId));
|
||||||
});
|
|
||||||
})
|
})
|
||||||
.doOnSuccess(updatedBooking -> {
|
.doOnSuccess(updatedBooking -> {
|
||||||
logger.info("取消预约成功:bookingId={}, memberId={}", bookingId, memberId);
|
logger.info("取消预约成功:bookingId={}, memberId={}", bookingId, memberId);
|
||||||
@@ -297,7 +283,6 @@ public class GroupCourseBookingService implements IGroupCourseBookingService {
|
|||||||
logger.error("取消预约失败:bookingId={}, memberId={}, error={}",
|
logger.error("取消预约失败:bookingId={}, memberId={}, error={}",
|
||||||
bookingId, memberId, error.getMessage());
|
bookingId, memberId, error.getMessage());
|
||||||
});
|
});
|
||||||
});
|
|
||||||
})
|
})
|
||||||
.onErrorResume(error -> {
|
.onErrorResume(error -> {
|
||||||
redisService.releaseLock(bookingId, requestId).subscribe();
|
redisService.releaseLock(bookingId, requestId).subscribe();
|
||||||
|
|||||||
-42
@@ -149,46 +149,4 @@ public class GroupCourseRedisService {
|
|||||||
})
|
})
|
||||||
.defaultIfEmpty(false);
|
.defaultIfEmpty(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取课程预约人数(缓存)
|
|
||||||
*/
|
|
||||||
public Mono<Integer> getBookingCount(Long courseId) {
|
|
||||||
String key = "booking_count:" + courseId;
|
|
||||||
return reactiveRedisTemplate.opsForValue()
|
|
||||||
.get(key)
|
|
||||||
.map(obj -> (Integer) obj)
|
|
||||||
.defaultIfEmpty(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 增加课程预约人数
|
|
||||||
*/
|
|
||||||
public Mono<Long> incrementBookingCount(Long courseId) {
|
|
||||||
String key = "booking_count:" + courseId;
|
|
||||||
return reactiveRedisTemplate.opsForValue()
|
|
||||||
.increment(key)
|
|
||||||
.doOnSuccess(count -> logger.debug("预约人数增加:courseId={}, count={}", courseId, count));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 减少课程预约人数
|
|
||||||
*/
|
|
||||||
public Mono<Long> decrementBookingCount(Long courseId) {
|
|
||||||
String key = "booking_count:" + courseId;
|
|
||||||
return reactiveRedisTemplate.opsForValue()
|
|
||||||
.decrement(key)
|
|
||||||
.doOnSuccess(count -> logger.debug("预约人数减少:courseId={}, count={}", courseId, count));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 设置课程预约人数(用于从数据库同步)
|
|
||||||
*/
|
|
||||||
public Mono<Void> setBookingCount(Long courseId, Integer count) {
|
|
||||||
String key = "booking_count:" + courseId;
|
|
||||||
return reactiveRedisTemplate.opsForValue()
|
|
||||||
.set(key, count)
|
|
||||||
.doOnSuccess(result -> logger.debug("预约人数已设置:courseId={}, count={}", courseId, count))
|
|
||||||
.then();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
+222
-16
@@ -19,12 +19,15 @@ import cn.novalon.gym.manage.groupcourse.repository.IGroupCourseRepository;
|
|||||||
import cn.novalon.gym.manage.groupcourse.repository.IGroupCourseTypeRepository;
|
import cn.novalon.gym.manage.groupcourse.repository.IGroupCourseTypeRepository;
|
||||||
import cn.novalon.gym.manage.groupcourse.service.IGroupCourseService;
|
import cn.novalon.gym.manage.groupcourse.service.IGroupCourseService;
|
||||||
import cn.novalon.gym.manage.groupcourse.util.QRCodeUtil;
|
import cn.novalon.gym.manage.groupcourse.util.QRCodeUtil;
|
||||||
|
import cn.novalon.gym.manage.groupcourse.vo.GroupCourseVO;
|
||||||
import cn.novalon.gym.manage.file.core.service.ISysFileService;
|
import cn.novalon.gym.manage.file.core.service.ISysFileService;
|
||||||
import cn.novalon.gym.manage.member.entity.MemberCard;
|
import cn.novalon.gym.manage.member.entity.MemberCard;
|
||||||
import cn.novalon.gym.manage.member.entity.MemberCardRecord;
|
import cn.novalon.gym.manage.member.entity.MemberCardRecord;
|
||||||
import cn.novalon.gym.manage.member.enums.MemberCardType;
|
import cn.novalon.gym.manage.member.enums.MemberCardType;
|
||||||
import cn.novalon.gym.manage.member.repository.MemberCardRepository;
|
import cn.novalon.gym.manage.member.repository.MemberCardRepository;
|
||||||
import cn.novalon.gym.manage.member.service.IMemberCardRecordService;
|
import cn.novalon.gym.manage.member.service.IMemberCardRecordService;
|
||||||
|
import cn.novalon.gym.manage.sys.core.domain.SysUser;
|
||||||
|
import cn.novalon.gym.manage.sys.core.repository.ISysUserRepository;
|
||||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
@@ -35,7 +38,9 @@ import reactor.core.publisher.Flux;
|
|||||||
import reactor.core.publisher.Mono;
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
@@ -53,6 +58,7 @@ public class GroupCourseService implements IGroupCourseService {
|
|||||||
private final GroupCourseStateMachine stateMachine;
|
private final GroupCourseStateMachine stateMachine;
|
||||||
private final DatabaseClient databaseClient;
|
private final DatabaseClient databaseClient;
|
||||||
private final ISysFileService fileService;
|
private final ISysFileService fileService;
|
||||||
|
private final ISysUserRepository sysUserRepository;
|
||||||
|
|
||||||
private static final String CACHE_KEY_PREFIX = "group_course:page:";
|
private static final String CACHE_KEY_PREFIX = "group_course:page:";
|
||||||
private static final String CACHE_KEY_ID_PREFIX = "group_course:id:";
|
private static final String CACHE_KEY_ID_PREFIX = "group_course:id:";
|
||||||
@@ -71,7 +77,8 @@ public class GroupCourseService implements IGroupCourseService {
|
|||||||
ObjectMapper objectMapper,
|
ObjectMapper objectMapper,
|
||||||
GroupCourseStateMachine stateMachine,
|
GroupCourseStateMachine stateMachine,
|
||||||
DatabaseClient databaseClient,
|
DatabaseClient databaseClient,
|
||||||
ISysFileService fileService){
|
ISysFileService fileService,
|
||||||
|
ISysUserRepository sysUserRepository){
|
||||||
this.groupCourseRepository = groupCourseRepository;
|
this.groupCourseRepository = groupCourseRepository;
|
||||||
this.bookingRepository = bookingRepository;
|
this.bookingRepository = bookingRepository;
|
||||||
this.groupCourseTypeRepository = groupCourseTypeRepository;
|
this.groupCourseTypeRepository = groupCourseTypeRepository;
|
||||||
@@ -83,19 +90,21 @@ public class GroupCourseService implements IGroupCourseService {
|
|||||||
this.stateMachine = stateMachine;
|
this.stateMachine = stateMachine;
|
||||||
this.databaseClient = databaseClient;
|
this.databaseClient = databaseClient;
|
||||||
this.fileService = fileService;
|
this.fileService = fileService;
|
||||||
|
this.sysUserRepository = sysUserRepository;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Mono<GroupCourseDetail> findDetailById(Long id) {
|
public Mono<GroupCourseDetail> findDetailById(Long id) {
|
||||||
String cacheKey = CACHE_KEY_DETAIL_PREFIX + id;
|
String cacheKey = CACHE_KEY_DETAIL_PREFIX + id;
|
||||||
|
|
||||||
return redisUtil.get(cacheKey, String.class)
|
Mono<String> cachedMono = redisUtil.get(cacheKey, String.class);
|
||||||
|
return cachedMono
|
||||||
.flatMap(cachedJson -> {
|
.flatMap(cachedJson -> {
|
||||||
if (cachedJson != null) {
|
if (cachedJson != null && !cachedJson.isEmpty()) {
|
||||||
try {
|
try {
|
||||||
GroupCourseDetail detail = objectMapper.readValue(cachedJson, GroupCourseDetail.class);
|
GroupCourseDetail detail = objectMapper.readValue(cachedJson, GroupCourseDetail.class);
|
||||||
logger.info("缓存命中 - findDetailById: id={}", id);
|
logger.info("缓存命中 - findDetailById: id={}", id);
|
||||||
return Mono.just(detail);
|
return Mono.<GroupCourseDetail>just(detail);
|
||||||
} catch (JsonProcessingException e) {
|
} catch (JsonProcessingException e) {
|
||||||
logger.warn("缓存解析失败,删除缓存 - id: {}, error: {}", id, e.getMessage());
|
logger.warn("缓存解析失败,删除缓存 - id: {}, error: {}", id, e.getMessage());
|
||||||
return redisUtil.delete(cacheKey).then(Mono.empty());
|
return redisUtil.delete(cacheKey).then(Mono.empty());
|
||||||
@@ -127,6 +136,8 @@ public class GroupCourseService implements IGroupCourseService {
|
|||||||
})
|
})
|
||||||
.switchIfEmpty(Mono.just(buildDetail(course, null)));
|
.switchIfEmpty(Mono.just(buildDetail(course, null)));
|
||||||
})
|
})
|
||||||
|
.flatMap(this::enrichCoachName)
|
||||||
|
.flatMap(this::enrichCurrentMembers)
|
||||||
.flatMap(detail -> {
|
.flatMap(detail -> {
|
||||||
try {
|
try {
|
||||||
String jsonData = objectMapper.writeValueAsString(detail);
|
String jsonData = objectMapper.writeValueAsString(detail);
|
||||||
@@ -172,17 +183,64 @@ public class GroupCourseService implements IGroupCourseService {
|
|||||||
return detail;
|
return detail;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 为团课详情对象填充教练名称
|
||||||
|
*/
|
||||||
|
private Mono<GroupCourseDetail> enrichCoachName(GroupCourseDetail detail) {
|
||||||
|
Long coachId = detail.getCoachId();
|
||||||
|
if (coachId == null) {
|
||||||
|
return Mono.just(detail);
|
||||||
|
}
|
||||||
|
return sysUserRepository.findByIdIncludingDeleted(coachId)
|
||||||
|
.map(user -> {
|
||||||
|
String name = user.getNickname() != null ? user.getNickname() : user.getUsername();
|
||||||
|
detail.setCoachName(name);
|
||||||
|
logger.debug("enrichCoachName: courseId={}, coachId={}, coachName={}", detail.getId(), coachId, name);
|
||||||
|
return detail;
|
||||||
|
})
|
||||||
|
.doOnNext(d -> {}) // 避免空doOnNext告警
|
||||||
|
.defaultIfEmpty(detail)
|
||||||
|
.doOnDiscard(GroupCourseDetail.class, d ->
|
||||||
|
logger.warn("enrichCoachName: coach not found, coachId={}, courseId={}", coachId, detail.getId())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 为团课详情对象填充实时预约人数
|
||||||
|
*/
|
||||||
|
private Mono<GroupCourseDetail> enrichCurrentMembers(GroupCourseDetail detail) {
|
||||||
|
return bookingRepository.countValidBookings(detail.getId())
|
||||||
|
.map(count -> {
|
||||||
|
detail.setCurrentMembers(count.intValue());
|
||||||
|
return detail;
|
||||||
|
})
|
||||||
|
.defaultIfEmpty(detail);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 为团课对象填充实时预约人数
|
||||||
|
*/
|
||||||
|
private Mono<GroupCourse> enrichCurrentMembers(GroupCourse course) {
|
||||||
|
return bookingRepository.countValidBookings(course.getId())
|
||||||
|
.map(count -> {
|
||||||
|
course.setCurrentMembers(count.intValue());
|
||||||
|
return course;
|
||||||
|
})
|
||||||
|
.defaultIfEmpty(course);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Mono<GroupCourse> findById(Long id) {
|
public Mono<GroupCourse> findById(Long id) {
|
||||||
String cacheKey = CACHE_KEY_ID_PREFIX + id;
|
String cacheKey = CACHE_KEY_ID_PREFIX + id;
|
||||||
|
|
||||||
return redisUtil.get(cacheKey, String.class)
|
Mono<String> cachedMono = redisUtil.get(cacheKey, String.class);
|
||||||
|
return cachedMono
|
||||||
.flatMap(cachedJson -> {
|
.flatMap(cachedJson -> {
|
||||||
if (cachedJson != null) {
|
if (cachedJson != null && !cachedJson.isEmpty()) {
|
||||||
try {
|
try {
|
||||||
GroupCourse groupCourse = objectMapper.readValue(cachedJson, GroupCourse.class);
|
GroupCourse groupCourse = objectMapper.readValue(cachedJson, GroupCourse.class);
|
||||||
logger.info("缓存命中 - findById: id={}", id);
|
logger.info("缓存命中 - findById: id={}", id);
|
||||||
return Mono.just(groupCourse);
|
return Mono.<GroupCourse>just(groupCourse);
|
||||||
} catch (JsonProcessingException e) {
|
} catch (JsonProcessingException e) {
|
||||||
logger.warn("缓存解析失败,删除缓存 - id: {}, error: {}", id, e.getMessage());
|
logger.warn("缓存解析失败,删除缓存 - id: {}, error: {}", id, e.getMessage());
|
||||||
return redisUtil.delete(cacheKey).then(Mono.empty());
|
return redisUtil.delete(cacheKey).then(Mono.empty());
|
||||||
@@ -204,7 +262,8 @@ public class GroupCourseService implements IGroupCourseService {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
.doOnSubscribe(sub -> logger.debug("缓存未命中,查询数据库 - findById: id={}", id))
|
.doOnSubscribe(sub -> logger.debug("缓存未命中,查询数据库 - findById: id={}", id))
|
||||||
);
|
)
|
||||||
|
.flatMap(this::enrichCurrentMembers);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -221,6 +280,47 @@ public class GroupCourseService implements IGroupCourseService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Flux<GroupCourseVO> findAllAsVO(boolean includeDeleted) {
|
||||||
|
return findAll(includeDeleted)
|
||||||
|
.flatMap(this::enrichCurrentMembers)
|
||||||
|
.collectList()
|
||||||
|
.flatMapMany(courses -> {
|
||||||
|
if (courses.isEmpty()) {
|
||||||
|
logger.info("findAllAsVO: no courses found");
|
||||||
|
return Flux.empty();
|
||||||
|
}
|
||||||
|
var coachIds = courses.stream()
|
||||||
|
.map(GroupCourse::getCoachId)
|
||||||
|
.filter(id -> id != null)
|
||||||
|
.distinct()
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
logger.info("findAllAsVO: {} courses, {} unique coach IDs: {}", courses.size(), coachIds.size(), coachIds);
|
||||||
|
|
||||||
|
if (coachIds.isEmpty()) {
|
||||||
|
logger.info("findAllAsVO: no coach IDs, returning VOs with null coachName");
|
||||||
|
return Flux.fromIterable(courses.stream()
|
||||||
|
.map(c -> GroupCourseVO.from(c, null))
|
||||||
|
.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
return Flux.fromIterable(coachIds)
|
||||||
|
.flatMap(sysUserRepository::findByIdIncludingDeleted)
|
||||||
|
.doOnNext(u -> logger.info("findAllAsVO: found coach id={}, nickname={}, username={}", u.getId(), u.getNickname(), u.getUsername()))
|
||||||
|
.collectMap(SysUser::getId, u -> u.getNickname() != null ? u.getNickname() : u.getUsername())
|
||||||
|
.doOnNext(m -> logger.info("findAllAsVO: coachNameMap size={}, keys={}", m.size(), m.keySet()))
|
||||||
|
.flatMapMany(coachNameMap ->
|
||||||
|
Flux.fromIterable(courses.stream()
|
||||||
|
.map(c -> {
|
||||||
|
String name = coachNameMap.get(c.getCoachId());
|
||||||
|
logger.debug("findAllAsVO: courseId={}, coachId={}, coachName={}", c.getId(), c.getCoachId(), name);
|
||||||
|
return GroupCourseVO.from(c, name);
|
||||||
|
})
|
||||||
|
.toList()));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Mono<PageResponse<GroupCourse>> findByPage(PageRequest pageRequest, boolean includeDeleted) {
|
public Mono<PageResponse<GroupCourse>> findByPage(PageRequest pageRequest, boolean includeDeleted) {
|
||||||
int page = pageRequest.getPage();
|
int page = pageRequest.getPage();
|
||||||
@@ -232,14 +332,15 @@ public class GroupCourseService implements IGroupCourseService {
|
|||||||
|
|
||||||
String cacheKey = CACHE_KEY_PREFIX + page + ":" + size + ":" + includeDeleted + ":" + sort + ":" + order + ":" + keyword + ":" + status;
|
String cacheKey = CACHE_KEY_PREFIX + page + ":" + size + ":" + includeDeleted + ":" + sort + ":" + order + ":" + keyword + ":" + status;
|
||||||
|
|
||||||
return redisUtil.get(cacheKey, String.class)
|
Mono<String> cachedMono = redisUtil.get(cacheKey, String.class);
|
||||||
|
return cachedMono
|
||||||
.flatMap(cachedJson -> {
|
.flatMap(cachedJson -> {
|
||||||
if (cachedJson != null) {
|
if (cachedJson != null && !cachedJson.isEmpty()) {
|
||||||
try {
|
try {
|
||||||
PageResponse<GroupCourse> pageResponse = objectMapper.readValue(cachedJson,
|
PageResponse<GroupCourse> pageResponse = objectMapper.readValue(cachedJson,
|
||||||
objectMapper.getTypeFactory().constructParametricType(PageResponse.class, GroupCourse.class));
|
objectMapper.getTypeFactory().constructParametricType(PageResponse.class, GroupCourse.class));
|
||||||
logger.info("缓存命中 - findByPage: key={}", cacheKey);
|
logger.info("缓存命中 - findByPage: key={}", cacheKey);
|
||||||
return Mono.just(pageResponse);
|
return Mono.<PageResponse<GroupCourse>>just(pageResponse);
|
||||||
} catch (JsonProcessingException e) {
|
} catch (JsonProcessingException e) {
|
||||||
logger.warn("缓存解析失败,删除缓存 - key: {}, error: {}", cacheKey, e.getMessage());
|
logger.warn("缓存解析失败,删除缓存 - key: {}, error: {}", cacheKey, e.getMessage());
|
||||||
return redisUtil.delete(cacheKey).then(Mono.empty());
|
return redisUtil.delete(cacheKey).then(Mono.empty());
|
||||||
@@ -272,6 +373,73 @@ public class GroupCourseService implements IGroupCourseService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Mono<PageResponse<GroupCourseVO>> findByPageAsVO(PageRequest pageRequest, boolean includeDeleted) {
|
||||||
|
return findByPage(pageRequest, includeDeleted)
|
||||||
|
.flatMap(pageResponse -> {
|
||||||
|
List<GroupCourse> courses = pageResponse.getContent();
|
||||||
|
if (courses.isEmpty()) {
|
||||||
|
PageResponse<GroupCourseVO> voResponse = new PageResponse<>(
|
||||||
|
Collections.emptyList(),
|
||||||
|
pageResponse.getTotalPages(),
|
||||||
|
pageResponse.getTotalElements(),
|
||||||
|
pageResponse.getCurrentPage(),
|
||||||
|
pageResponse.getPageSize()
|
||||||
|
);
|
||||||
|
return Mono.just(voResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 用实时预约人数覆盖缓存/DB中的currentMembers
|
||||||
|
return Flux.fromIterable(courses)
|
||||||
|
.flatMap(this::enrichCurrentMembers)
|
||||||
|
.collectList()
|
||||||
|
.flatMap(enrichedCourses -> {
|
||||||
|
var coachIds = enrichedCourses.stream()
|
||||||
|
.map(GroupCourse::getCoachId)
|
||||||
|
.filter(id -> id != null)
|
||||||
|
.distinct()
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
logger.info("findByPageAsVO: {} courses, {} unique coach IDs: {}", enrichedCourses.size(), coachIds.size(), coachIds);
|
||||||
|
|
||||||
|
if (coachIds.isEmpty()) {
|
||||||
|
List<GroupCourseVO> voList = enrichedCourses.stream()
|
||||||
|
.map(c -> GroupCourseVO.from(c, null))
|
||||||
|
.toList();
|
||||||
|
PageResponse<GroupCourseVO> voResponse = new PageResponse<>(
|
||||||
|
voList,
|
||||||
|
pageResponse.getTotalPages(),
|
||||||
|
pageResponse.getTotalElements(),
|
||||||
|
pageResponse.getCurrentPage(),
|
||||||
|
pageResponse.getPageSize()
|
||||||
|
);
|
||||||
|
return Mono.just(voResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Flux.fromIterable(coachIds)
|
||||||
|
.flatMap(sysUserRepository::findByIdIncludingDeleted)
|
||||||
|
.doOnNext(u -> logger.info("findByPageAsVO: found coach id={}, nickname={}", u.getId(), u.getNickname()))
|
||||||
|
.collectMap(SysUser::getId, u -> u.getNickname() != null ? u.getNickname() : u.getUsername())
|
||||||
|
.map(coachNameMap -> {
|
||||||
|
List<GroupCourseVO> voList = enrichedCourses.stream()
|
||||||
|
.map(c -> {
|
||||||
|
String name = coachNameMap.get(c.getCoachId());
|
||||||
|
logger.debug("findByPageAsVO: courseId={}, coachId={}, coachName={}", c.getId(), c.getCoachId(), name);
|
||||||
|
return GroupCourseVO.from(c, name);
|
||||||
|
})
|
||||||
|
.toList();
|
||||||
|
return new PageResponse<>(
|
||||||
|
voList,
|
||||||
|
pageResponse.getTotalPages(),
|
||||||
|
pageResponse.getTotalElements(),
|
||||||
|
pageResponse.getCurrentPage(),
|
||||||
|
pageResponse.getPageSize()
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Mono<GroupCourse> create(GroupCourse groupCourse) {
|
public Mono<GroupCourse> create(GroupCourse groupCourse) {
|
||||||
return groupCourseRepository.save(groupCourse)
|
return groupCourseRepository.save(groupCourse)
|
||||||
@@ -488,6 +656,15 @@ public class GroupCourseService implements IGroupCourseService {
|
|||||||
public Mono<GroupCourse> signIn(Long courseId, Long memberId) {
|
public Mono<GroupCourse> signIn(Long courseId, Long memberId) {
|
||||||
return groupCourseRepository.findByIdAndDeletedAtIsNull(courseId)
|
return groupCourseRepository.findByIdAndDeletedAtIsNull(courseId)
|
||||||
.switchIfEmpty(Mono.error(new RuntimeException("团课不存在或已删除")))
|
.switchIfEmpty(Mono.error(new RuntimeException("团课不存在或已删除")))
|
||||||
|
.flatMap(course ->
|
||||||
|
// 用实时预约人数覆盖currentMembers
|
||||||
|
bookingRepository.countValidBookings(courseId)
|
||||||
|
.map(count -> {
|
||||||
|
course.setCurrentMembers(count.intValue());
|
||||||
|
return course;
|
||||||
|
})
|
||||||
|
.defaultIfEmpty(course)
|
||||||
|
)
|
||||||
.flatMap(course -> {
|
.flatMap(course -> {
|
||||||
// 校验1:团课已取消
|
// 校验1:团课已取消
|
||||||
if (course.getStatus().equals(CourseStatus.CANCELLED.getValue())) {
|
if (course.getStatus().equals(CourseStatus.CANCELLED.getValue())) {
|
||||||
@@ -518,11 +695,9 @@ public class GroupCourseService implements IGroupCourseService {
|
|||||||
return bookingRepository.findValidBooking(courseId, memberId)
|
return bookingRepository.findValidBooking(courseId, memberId)
|
||||||
.switchIfEmpty(Mono.error(new RuntimeException("您未预约此团课")))
|
.switchIfEmpty(Mono.error(new RuntimeException("您未预约此团课")))
|
||||||
.flatMap(booking -> {
|
.flatMap(booking -> {
|
||||||
return groupCourseRepository.updateCurrentMembers(courseId, 1)
|
// 更新预约状态为已出席(2),不再手动计数
|
||||||
.flatMap(updatedCourse -> {
|
|
||||||
return bookingRepository.updateStatus(booking.getId(), "2")
|
return bookingRepository.updateStatus(booking.getId(), "2")
|
||||||
.thenReturn(updatedCourse);
|
.thenReturn(course);
|
||||||
});
|
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.doOnSuccess(course -> logger.info("团课签到成功 - courseId={}, memberId={}", courseId, memberId))
|
.doOnSuccess(course -> logger.info("团课签到成功 - courseId={}, memberId={}", courseId, memberId))
|
||||||
@@ -566,6 +741,37 @@ public class GroupCourseService implements IGroupCourseService {
|
|||||||
private Mono<Void> clearCache() {
|
private Mono<Void> clearCache() {
|
||||||
return redisUtil.deleteByPattern(CACHE_KEY_PREFIX + "*")
|
return redisUtil.deleteByPattern(CACHE_KEY_PREFIX + "*")
|
||||||
.then(redisUtil.deleteByPattern(CACHE_KEY_ID_PREFIX + "*"))
|
.then(redisUtil.deleteByPattern(CACHE_KEY_ID_PREFIX + "*"))
|
||||||
.then(redisUtil.deleteByPattern(CACHE_KEY_DETAIL_PREFIX + "*")).then();
|
.then(redisUtil.deleteByPattern(CACHE_KEY_DETAIL_PREFIX + "*"))
|
||||||
|
.then(redisUtil.deleteByPattern("datacount:statistics:*"))
|
||||||
|
.then();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Mono<List<GroupCourse>> checkCoachConflict(Long coachId, LocalDateTime startTime, LocalDateTime endTime, Long excludeCourseId) {
|
||||||
|
if (coachId == null || startTime == null || endTime == null) {
|
||||||
|
return Mono.just(Collections.emptyList());
|
||||||
|
}
|
||||||
|
return groupCourseRepository.findByCoachId(coachId)
|
||||||
|
.filter(course -> {
|
||||||
|
// 排除已取消的课程
|
||||||
|
if (course.getStatus() != null && course.getStatus().equals(CourseStatus.CANCELLED.getValue())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// 排除自身(编辑时)
|
||||||
|
if (excludeCourseId != null && course.getId() != null && course.getId().equals(excludeCourseId)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// 检查时间是否重叠:startTime < course.endTime AND course.startTime < endTime
|
||||||
|
if (course.getStartTime() == null || course.getEndTime() == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return startTime.isBefore(course.getEndTime()) && course.getStartTime().isBefore(endTime);
|
||||||
|
})
|
||||||
|
.collectList()
|
||||||
|
.doOnNext(conflicts -> {
|
||||||
|
if (!conflicts.isEmpty()) {
|
||||||
|
logger.info("checkCoachConflict: coachId={}, conflicts={} courses", coachId, conflicts.size());
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+134
@@ -0,0 +1,134 @@
|
|||||||
|
package cn.novalon.gym.manage.groupcourse.vo;
|
||||||
|
|
||||||
|
import cn.novalon.gym.manage.groupcourse.domain.GroupCourse;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 团课视图对象(含教练昵称)
|
||||||
|
*
|
||||||
|
* @author 张翔
|
||||||
|
* @date 2026-07-19
|
||||||
|
*/
|
||||||
|
public class GroupCourseVO {
|
||||||
|
|
||||||
|
private Long id;
|
||||||
|
private String courseName;
|
||||||
|
private Long coachId;
|
||||||
|
private String coachName;
|
||||||
|
private Long courseType;
|
||||||
|
private LocalDateTime startTime;
|
||||||
|
private LocalDateTime endTime;
|
||||||
|
private LocalDateTime actualStartTime;
|
||||||
|
private LocalDateTime actualEndTime;
|
||||||
|
private Integer maxMembers;
|
||||||
|
private Integer currentMembers;
|
||||||
|
private Long status;
|
||||||
|
private String location;
|
||||||
|
private String coverImage;
|
||||||
|
private String description;
|
||||||
|
private BigDecimal storedValueAmount;
|
||||||
|
private String qrCodePath;
|
||||||
|
private String createBy;
|
||||||
|
private String updateBy;
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
private LocalDateTime updatedAt;
|
||||||
|
|
||||||
|
public GroupCourseVO() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从领域对象和教练名称构建 VO
|
||||||
|
*/
|
||||||
|
public static GroupCourseVO from(GroupCourse course, String coachName) {
|
||||||
|
GroupCourseVO vo = new GroupCourseVO();
|
||||||
|
vo.setId(course.getId());
|
||||||
|
vo.setCourseName(course.getCourseName());
|
||||||
|
vo.setCoachId(course.getCoachId());
|
||||||
|
vo.setCoachName(coachName);
|
||||||
|
vo.setCourseType(course.getCourseType());
|
||||||
|
vo.setStartTime(course.getStartTime());
|
||||||
|
vo.setEndTime(course.getEndTime());
|
||||||
|
vo.setActualStartTime(course.getActualStartTime());
|
||||||
|
vo.setActualEndTime(course.getActualEndTime());
|
||||||
|
vo.setMaxMembers(course.getMaxMembers());
|
||||||
|
vo.setCurrentMembers(course.getCurrentMembers());
|
||||||
|
vo.setStatus(course.getStatus());
|
||||||
|
vo.setLocation(course.getLocation());
|
||||||
|
vo.setCoverImage(course.getCoverImage());
|
||||||
|
vo.setDescription(course.getDescription());
|
||||||
|
vo.setStoredValueAmount(course.getStoredValueAmount());
|
||||||
|
vo.setQrCodePath(course.getQrCodePath());
|
||||||
|
vo.setCreateBy(course.getCreateBy());
|
||||||
|
vo.setUpdateBy(course.getUpdateBy());
|
||||||
|
vo.setCreatedAt(course.getCreatedAt());
|
||||||
|
vo.setUpdatedAt(course.getUpdatedAt());
|
||||||
|
return vo;
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- getters / setters --
|
||||||
|
|
||||||
|
public Long getId() { return id; }
|
||||||
|
public void setId(Long id) { this.id = id; }
|
||||||
|
|
||||||
|
public String getCourseName() { return courseName; }
|
||||||
|
public void setCourseName(String courseName) { this.courseName = courseName; }
|
||||||
|
|
||||||
|
public Long getCoachId() { return coachId; }
|
||||||
|
public void setCoachId(Long coachId) { this.coachId = coachId; }
|
||||||
|
|
||||||
|
public String getCoachName() { return coachName; }
|
||||||
|
public void setCoachName(String coachName) { this.coachName = coachName; }
|
||||||
|
|
||||||
|
public Long getCourseType() { return courseType; }
|
||||||
|
public void setCourseType(Long courseType) { this.courseType = courseType; }
|
||||||
|
|
||||||
|
public LocalDateTime getStartTime() { return startTime; }
|
||||||
|
public void setStartTime(LocalDateTime startTime) { this.startTime = startTime; }
|
||||||
|
|
||||||
|
public LocalDateTime getEndTime() { return endTime; }
|
||||||
|
public void setEndTime(LocalDateTime endTime) { this.endTime = endTime; }
|
||||||
|
|
||||||
|
public LocalDateTime getActualStartTime() { return actualStartTime; }
|
||||||
|
public void setActualStartTime(LocalDateTime actualStartTime) { this.actualStartTime = actualStartTime; }
|
||||||
|
|
||||||
|
public LocalDateTime getActualEndTime() { return actualEndTime; }
|
||||||
|
public void setActualEndTime(LocalDateTime actualEndTime) { this.actualEndTime = actualEndTime; }
|
||||||
|
|
||||||
|
public Integer getMaxMembers() { return maxMembers; }
|
||||||
|
public void setMaxMembers(Integer maxMembers) { this.maxMembers = maxMembers; }
|
||||||
|
|
||||||
|
public Integer getCurrentMembers() { return currentMembers; }
|
||||||
|
public void setCurrentMembers(Integer currentMembers) { this.currentMembers = currentMembers; }
|
||||||
|
|
||||||
|
public Long getStatus() { return status; }
|
||||||
|
public void setStatus(Long status) { this.status = status; }
|
||||||
|
|
||||||
|
public String getLocation() { return location; }
|
||||||
|
public void setLocation(String location) { this.location = location; }
|
||||||
|
|
||||||
|
public String getCoverImage() { return coverImage; }
|
||||||
|
public void setCoverImage(String coverImage) { this.coverImage = coverImage; }
|
||||||
|
|
||||||
|
public String getDescription() { return description; }
|
||||||
|
public void setDescription(String description) { this.description = description; }
|
||||||
|
|
||||||
|
public BigDecimal getStoredValueAmount() { return storedValueAmount; }
|
||||||
|
public void setStoredValueAmount(BigDecimal storedValueAmount) { this.storedValueAmount = storedValueAmount; }
|
||||||
|
|
||||||
|
public String getQrCodePath() { return qrCodePath; }
|
||||||
|
public void setQrCodePath(String qrCodePath) { this.qrCodePath = qrCodePath; }
|
||||||
|
|
||||||
|
public String getCreateBy() { return createBy; }
|
||||||
|
public void setCreateBy(String createBy) { this.createBy = createBy; }
|
||||||
|
|
||||||
|
public String getUpdateBy() { return updateBy; }
|
||||||
|
public void setUpdateBy(String updateBy) { this.updateBy = updateBy; }
|
||||||
|
|
||||||
|
public LocalDateTime getCreatedAt() { return createdAt; }
|
||||||
|
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
|
||||||
|
|
||||||
|
public LocalDateTime getUpdatedAt() { return updatedAt; }
|
||||||
|
public void setUpdatedAt(LocalDateTime updatedAt) { this.updatedAt = updatedAt; }
|
||||||
|
}
|
||||||
+2
@@ -158,6 +158,7 @@ public class MemberServiceImpl implements MemberService {
|
|||||||
|
|
||||||
return MemberInfoVO.builder()
|
return MemberInfoVO.builder()
|
||||||
.id(member.getId())
|
.id(member.getId())
|
||||||
|
.memberNo(member.getMemberNo())
|
||||||
.nickname(member.getNickname())
|
.nickname(member.getNickname())
|
||||||
.phone(maskedPhone)
|
.phone(maskedPhone)
|
||||||
.gender(genderEnum)
|
.gender(genderEnum)
|
||||||
@@ -166,6 +167,7 @@ public class MemberServiceImpl implements MemberService {
|
|||||||
.avatar(member.getAvatar())
|
.avatar(member.getAvatar())
|
||||||
.hasPhone(phone != null)
|
.hasPhone(phone != null)
|
||||||
.isSubscribed(member.getSubscribed() != null && member.getSubscribed())
|
.isSubscribed(member.getSubscribed() != null && member.getSubscribed())
|
||||||
|
.lastLoginAt(member.getLastLoginAt())
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+7
@@ -7,6 +7,7 @@ import lombok.Data;
|
|||||||
import lombok.NoArgsConstructor;
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -25,6 +26,9 @@ public class MemberInfoVO {
|
|||||||
// 会员 ID
|
// 会员 ID
|
||||||
private Long id;
|
private Long id;
|
||||||
|
|
||||||
|
// 会员编号
|
||||||
|
private String memberNo;
|
||||||
|
|
||||||
// 昵称
|
// 昵称
|
||||||
private String nickname;
|
private String nickname;
|
||||||
|
|
||||||
@@ -48,4 +52,7 @@ public class MemberInfoVO {
|
|||||||
|
|
||||||
// 是否已关注公众号
|
// 是否已关注公众号
|
||||||
private Boolean isSubscribed;
|
private Boolean isSubscribed;
|
||||||
|
|
||||||
|
// 最后登录时间
|
||||||
|
private LocalDateTime lastLoginAt;
|
||||||
}
|
}
|
||||||
@@ -169,6 +169,12 @@
|
|||||||
<version>1.0.0</version>
|
<version>1.0.0</version>
|
||||||
<scope>compile</scope>
|
<scope>compile</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>cn.novalon.gym.manage</groupId>
|
||||||
|
<artifactId>gym-coach</artifactId>
|
||||||
|
<version>1.0.0</version>
|
||||||
|
<scope>compile</scope>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
<build>
|
<build>
|
||||||
|
|||||||
+2
-1
@@ -25,7 +25,8 @@ import java.util.List;
|
|||||||
"cn.novalon.gym.manage.member.repository",
|
"cn.novalon.gym.manage.member.repository",
|
||||||
"cn.novalon.gym.manage.groupcourse.dao",
|
"cn.novalon.gym.manage.groupcourse.dao",
|
||||||
"cn.novalon.gym.manage.checkIn.repository",
|
"cn.novalon.gym.manage.checkIn.repository",
|
||||||
"cn.novalon.gym.manage.payment.repository"
|
"cn.novalon.gym.manage.payment.repository",
|
||||||
|
"cn.novalon.gym.manage.coach.dao"
|
||||||
})
|
})
|
||||||
@EnableReactiveElasticsearchRepositories(basePackages = "cn.novalon.gym.manage.member.es.repository")
|
@EnableReactiveElasticsearchRepositories(basePackages = "cn.novalon.gym.manage.member.es.repository")
|
||||||
public class ManageApplication {
|
public class ManageApplication {
|
||||||
|
|||||||
+27
-1
@@ -16,9 +16,12 @@ import cn.novalon.gym.manage.member.handler.MemberCardTransactionHandler;
|
|||||||
import cn.novalon.gym.manage.member.handler.MemberHandler;
|
import cn.novalon.gym.manage.member.handler.MemberHandler;
|
||||||
import cn.novalon.gym.manage.member.handler.MemberStoredCardHandler;
|
import cn.novalon.gym.manage.member.handler.MemberStoredCardHandler;
|
||||||
import cn.novalon.gym.manage.member.handler.WechatAuthHandler;
|
import cn.novalon.gym.manage.member.handler.WechatAuthHandler;
|
||||||
|
import cn.novalon.gym.manage.notify.handler.BannerHandler;
|
||||||
import cn.novalon.gym.manage.notify.handler.SysNoticeHandler;
|
import cn.novalon.gym.manage.notify.handler.SysNoticeHandler;
|
||||||
import cn.novalon.gym.manage.notify.handler.SysUserMessageHandler;
|
import cn.novalon.gym.manage.notify.handler.SysUserMessageHandler;
|
||||||
import cn.novalon.gym.manage.payment.handler.PaymentHandler;
|
import cn.novalon.gym.manage.payment.handler.PaymentHandler;
|
||||||
|
import cn.novalon.gym.manage.coach.handler.CoachCourseHandler;
|
||||||
|
import cn.novalon.gym.manage.coach.handler.CoachHandler;
|
||||||
import cn.novalon.gym.manage.sys.handler.auth.PasswordDiagnosticHandler;
|
import cn.novalon.gym.manage.sys.handler.auth.PasswordDiagnosticHandler;
|
||||||
import cn.novalon.gym.manage.sys.handler.auth.SysAuthHandler;
|
import cn.novalon.gym.manage.sys.handler.auth.SysAuthHandler;
|
||||||
import cn.novalon.gym.manage.sys.handler.config.SysConfigHandler;
|
import cn.novalon.gym.manage.sys.handler.config.SysConfigHandler;
|
||||||
@@ -63,6 +66,7 @@ public class SystemRouter {
|
|||||||
SysAuthHandler authHandler,
|
SysAuthHandler authHandler,
|
||||||
StatsHandler statsHandler,
|
StatsHandler statsHandler,
|
||||||
SysDictHandler dictHandler,
|
SysDictHandler dictHandler,
|
||||||
|
BannerHandler bannerHandler,
|
||||||
SysNoticeHandler noticeHandler,
|
SysNoticeHandler noticeHandler,
|
||||||
SysUserMessageHandler messageHandler,
|
SysUserMessageHandler messageHandler,
|
||||||
SysFileHandler fileHandler,
|
SysFileHandler fileHandler,
|
||||||
@@ -82,7 +86,9 @@ public class SystemRouter {
|
|||||||
CheckInHandler checkInHandler,
|
CheckInHandler checkInHandler,
|
||||||
DataStatisticsHandler dataStatisticsHandler,
|
DataStatisticsHandler dataStatisticsHandler,
|
||||||
PhoneAuthHandler phoneAuthHandler,
|
PhoneAuthHandler phoneAuthHandler,
|
||||||
PaymentHandler paymentHandler) {
|
PaymentHandler paymentHandler,
|
||||||
|
CoachHandler coachHandler,
|
||||||
|
CoachCourseHandler coachCourseHandler) {
|
||||||
|
|
||||||
return route()
|
return route()
|
||||||
// ========== 诊断路由 ==========
|
// ========== 诊断路由 ==========
|
||||||
@@ -116,6 +122,17 @@ public class SystemRouter {
|
|||||||
.GET("/api/users/{id}/roles", userHandler::getUserRoles)
|
.GET("/api/users/{id}/roles", userHandler::getUserRoles)
|
||||||
.POST("/api/users/{id}/roles", userHandler::assignRoles)
|
.POST("/api/users/{id}/roles", userHandler::assignRoles)
|
||||||
|
|
||||||
|
// ========== 教练路由 ==========
|
||||||
|
.GET("/api/coach/list", coachHandler::getAllCoaches)
|
||||||
|
.POST("/api/coach", coachHandler::createCoach)
|
||||||
|
.PUT("/api/coach/{id}", coachHandler::updateCoach)
|
||||||
|
.POST("/api/coach/{id}/disable", coachHandler::disableCoach)
|
||||||
|
.GET("/api/coach/{id}/courses", coachHandler::getCoachCourses)
|
||||||
|
.GET("/api/coach/violation-counts", coachHandler::getViolationCounts)
|
||||||
|
.GET("/api/coach/{id}/violations", coachHandler::getCoachViolations)
|
||||||
|
.POST("/api/coach/courses/{courseId}/start", coachCourseHandler::startCourse)
|
||||||
|
.POST("/api/coach/courses/{courseId}/end", coachCourseHandler::endCourse)
|
||||||
|
|
||||||
// ========== 菜单路由 ==========
|
// ========== 菜单路由 ==========
|
||||||
.GET("/api/menus", menuHandler::getAllMenus)
|
.GET("/api/menus", menuHandler::getAllMenus)
|
||||||
.GET("/api/menus/tree", menuHandler::getMenuTree)
|
.GET("/api/menus/tree", menuHandler::getMenuTree)
|
||||||
@@ -188,6 +205,14 @@ public class SystemRouter {
|
|||||||
.PUT("/api/dict/data/{id}", dictHandler::updateDictData)
|
.PUT("/api/dict/data/{id}", dictHandler::updateDictData)
|
||||||
.DELETE("/api/dict/data/{id}", dictHandler::deleteDictData)
|
.DELETE("/api/dict/data/{id}", dictHandler::deleteDictData)
|
||||||
|
|
||||||
|
// ========== 轮播图路由 ==========
|
||||||
|
.GET("/api/banners", bannerHandler::getAllBanners)
|
||||||
|
.GET("/api/banners/active", bannerHandler::getActiveBanners)
|
||||||
|
.GET("/api/banners/{id}", bannerHandler::getBannerById)
|
||||||
|
.POST("/api/banners", bannerHandler::createBanner)
|
||||||
|
.PUT("/api/banners/{id}", bannerHandler::updateBanner)
|
||||||
|
.DELETE("/api/banners/{id}", bannerHandler::deleteBanner)
|
||||||
|
|
||||||
// ========== 公告路由 ==========
|
// ========== 公告路由 ==========
|
||||||
.GET("/api/notices", noticeHandler::getAllNotices)
|
.GET("/api/notices", noticeHandler::getAllNotices)
|
||||||
.GET("/api/notices/{id}", noticeHandler::getNoticeById)
|
.GET("/api/notices/{id}", noticeHandler::getNoticeById)
|
||||||
@@ -352,6 +377,7 @@ public class SystemRouter {
|
|||||||
.POST("/api/groupCourse/{id}/cancel", groupCourseHandler::cancelGroupCourse)
|
.POST("/api/groupCourse/{id}/cancel", groupCourseHandler::cancelGroupCourse)
|
||||||
.POST("/api/groupCourse/signin/{memberId}", groupCourseHandler::signIn)
|
.POST("/api/groupCourse/signin/{memberId}", groupCourseHandler::signIn)
|
||||||
.POST("/api/groupCourse/search", groupCourseHandler::searchGroupCourses)
|
.POST("/api/groupCourse/search", groupCourseHandler::searchGroupCourses)
|
||||||
|
.POST("/api/groupCourse/check-coach-conflict", groupCourseHandler::checkCoachConflict)
|
||||||
|
|
||||||
// ========= 签到模块路由 ==========
|
// ========= 签到模块路由 ==========
|
||||||
// ===== 签到核心功能 =====
|
// ===== 签到核心功能 =====
|
||||||
|
|||||||
+1
-1
@@ -43,7 +43,7 @@ public class RedisUtil {
|
|||||||
public <T> Mono<T> get(String key, Class<T> clazz) {
|
public <T> Mono<T> get(String key, Class<T> clazz) {
|
||||||
return reactiveRedisTemplate.opsForValue().get(key)
|
return reactiveRedisTemplate.opsForValue().get(key)
|
||||||
.filter(clazz::isInstance)
|
.filter(clazz::isInstance)
|
||||||
.cast(clazz)
|
.map(clazz::cast)
|
||||||
.onErrorResume(e -> {
|
.onErrorResume(e -> {
|
||||||
// 旧缓存数据格式不兼容时静默跳过,后续逻辑会 fallback 到 DB 查询
|
// 旧缓存数据格式不兼容时静默跳过,后续逻辑会 fallback 到 DB 查询
|
||||||
log.warn("读取 Redis 缓存失败(可能为旧格式): key={}, error={}", key, e.getMessage());
|
log.warn("读取 Redis 缓存失败(可能为旧格式): key={}, error={}", key, e.getMessage());
|
||||||
|
|||||||
+73
@@ -0,0 +1,73 @@
|
|||||||
|
package cn.novalon.gym.manage.db.converter;
|
||||||
|
|
||||||
|
import cn.novalon.gym.manage.notify.core.domain.Banner;
|
||||||
|
import cn.novalon.gym.manage.db.entity.BannerEntity;
|
||||||
|
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 轮播图实体转换器
|
||||||
|
*
|
||||||
|
* @author 张翔
|
||||||
|
* @date 2026-07-18
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
public class BannerConverter {
|
||||||
|
|
||||||
|
public Banner toDomain(BannerEntity entity) {
|
||||||
|
if (entity == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
Banner domain = new Banner();
|
||||||
|
domain.setId(entity.getId());
|
||||||
|
domain.setImageUrl(entity.getImageUrl());
|
||||||
|
domain.setTitle(entity.getTitle());
|
||||||
|
domain.setSubtitle(entity.getSubtitle());
|
||||||
|
domain.setSortOrder(entity.getSortOrder());
|
||||||
|
domain.setIsActive(entity.getIsActive());
|
||||||
|
domain.setLinkUrl(entity.getLinkUrl());
|
||||||
|
domain.setCreatedAt(entity.getCreatedAt());
|
||||||
|
domain.setUpdatedAt(entity.getUpdatedAt());
|
||||||
|
domain.setDeletedAt(entity.getDeletedAt());
|
||||||
|
return domain;
|
||||||
|
}
|
||||||
|
|
||||||
|
public BannerEntity toEntity(Banner domain) {
|
||||||
|
if (domain == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
BannerEntity entity = new BannerEntity();
|
||||||
|
entity.setId(domain.getId());
|
||||||
|
entity.setImageUrl(domain.getImageUrl());
|
||||||
|
entity.setTitle(domain.getTitle());
|
||||||
|
entity.setSubtitle(domain.getSubtitle());
|
||||||
|
entity.setSortOrder(domain.getSortOrder());
|
||||||
|
entity.setIsActive(domain.getIsActive());
|
||||||
|
entity.setLinkUrl(domain.getLinkUrl());
|
||||||
|
entity.setCreatedAt(domain.getCreatedAt());
|
||||||
|
entity.setUpdatedAt(domain.getUpdatedAt());
|
||||||
|
entity.setDeletedAt(domain.getDeletedAt());
|
||||||
|
return entity;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Banner> toDomainList(List<BannerEntity> entities) {
|
||||||
|
if (entities == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return entities.stream()
|
||||||
|
.map(this::toDomain)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<BannerEntity> toEntityList(List<Banner> domains) {
|
||||||
|
if (domains == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return domains.stream()
|
||||||
|
.map(this::toEntity)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package cn.novalon.gym.manage.db.dao;
|
||||||
|
|
||||||
|
import cn.novalon.gym.manage.db.entity.BannerEntity;
|
||||||
|
import org.springframework.data.domain.Sort;
|
||||||
|
import org.springframework.data.r2dbc.repository.Query;
|
||||||
|
import org.springframework.data.r2dbc.repository.R2dbcRepository;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
import reactor.core.publisher.Flux;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
public interface BannerDao extends R2dbcRepository<BannerEntity, Long> {
|
||||||
|
|
||||||
|
Flux<BannerEntity> findByDeletedAtIsNull();
|
||||||
|
|
||||||
|
Flux<BannerEntity> findByDeletedAtIsNull(Sort sort);
|
||||||
|
|
||||||
|
@Query("SELECT * FROM banner WHERE deleted_at IS NULL ORDER BY sort_order ASC")
|
||||||
|
Flux<BannerEntity> findActiveBanners();
|
||||||
|
|
||||||
|
Mono<Void> deleteByIdAndDeletedAtIsNull(Long id);
|
||||||
|
}
|
||||||
+127
@@ -0,0 +1,127 @@
|
|||||||
|
package cn.novalon.gym.manage.db.entity;
|
||||||
|
|
||||||
|
import org.springframework.data.annotation.Id;
|
||||||
|
import org.springframework.data.relational.core.mapping.Column;
|
||||||
|
import org.springframework.data.relational.core.mapping.Table;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 轮播图数据库实体类
|
||||||
|
*
|
||||||
|
* @author 张翔
|
||||||
|
* @date 2026-07-18
|
||||||
|
*/
|
||||||
|
@Table("banner")
|
||||||
|
public class BannerEntity {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@Column("image_url")
|
||||||
|
private String imageUrl;
|
||||||
|
|
||||||
|
@Column("title")
|
||||||
|
private String title;
|
||||||
|
|
||||||
|
@Column("subtitle")
|
||||||
|
private String subtitle;
|
||||||
|
|
||||||
|
@Column("sort_order")
|
||||||
|
private Integer sortOrder;
|
||||||
|
|
||||||
|
@Column("is_active")
|
||||||
|
private String isActive;
|
||||||
|
|
||||||
|
@Column("link_url")
|
||||||
|
private String linkUrl;
|
||||||
|
|
||||||
|
@Column("created_at")
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
|
||||||
|
@Column("updated_at")
|
||||||
|
private LocalDateTime updatedAt;
|
||||||
|
|
||||||
|
@Column("deleted_at")
|
||||||
|
private LocalDateTime deletedAt;
|
||||||
|
|
||||||
|
public Long getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setId(Long id) {
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getImageUrl() {
|
||||||
|
return imageUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setImageUrl(String imageUrl) {
|
||||||
|
this.imageUrl = imageUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getTitle() {
|
||||||
|
return title;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTitle(String title) {
|
||||||
|
this.title = title;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getSubtitle() {
|
||||||
|
return subtitle;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSubtitle(String subtitle) {
|
||||||
|
this.subtitle = subtitle;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getSortOrder() {
|
||||||
|
return sortOrder;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSortOrder(Integer sortOrder) {
|
||||||
|
this.sortOrder = sortOrder;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getIsActive() {
|
||||||
|
return isActive;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setIsActive(String isActive) {
|
||||||
|
this.isActive = isActive;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getLinkUrl() {
|
||||||
|
return linkUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setLinkUrl(String linkUrl) {
|
||||||
|
this.linkUrl = linkUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LocalDateTime getCreatedAt() {
|
||||||
|
return createdAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCreatedAt(LocalDateTime createdAt) {
|
||||||
|
this.createdAt = createdAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LocalDateTime getUpdatedAt() {
|
||||||
|
return updatedAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUpdatedAt(LocalDateTime updatedAt) {
|
||||||
|
this.updatedAt = updatedAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LocalDateTime getDeletedAt() {
|
||||||
|
return deletedAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDeletedAt(LocalDateTime deletedAt) {
|
||||||
|
this.deletedAt = deletedAt;
|
||||||
|
}
|
||||||
|
}
|
||||||
+59
@@ -0,0 +1,59 @@
|
|||||||
|
package cn.novalon.gym.manage.db.repository;
|
||||||
|
|
||||||
|
import cn.novalon.gym.manage.notify.core.domain.Banner;
|
||||||
|
import cn.novalon.gym.manage.notify.core.repository.IBannerRepository;
|
||||||
|
import cn.novalon.gym.manage.db.converter.BannerConverter;
|
||||||
|
import cn.novalon.gym.manage.db.dao.BannerDao;
|
||||||
|
import cn.novalon.gym.manage.db.entity.BannerEntity;
|
||||||
|
import org.springframework.data.domain.Sort;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
import reactor.core.publisher.Flux;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 轮播图仓储实现类
|
||||||
|
*
|
||||||
|
* @author 张翔
|
||||||
|
* @date 2026-07-18
|
||||||
|
*/
|
||||||
|
@Repository
|
||||||
|
public class BannerRepository implements IBannerRepository {
|
||||||
|
|
||||||
|
private final BannerDao bannerDao;
|
||||||
|
private final BannerConverter bannerConverter;
|
||||||
|
|
||||||
|
public BannerRepository(BannerDao bannerDao, BannerConverter bannerConverter) {
|
||||||
|
this.bannerDao = bannerDao;
|
||||||
|
this.bannerConverter = bannerConverter;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Flux<Banner> findByDeletedAtIsNull() {
|
||||||
|
return bannerDao.findByDeletedAtIsNull(Sort.by(Sort.Direction.ASC, "sortOrder"))
|
||||||
|
.map(bannerConverter::toDomain);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Flux<Banner> findActiveBanners() {
|
||||||
|
return bannerDao.findActiveBanners()
|
||||||
|
.map(bannerConverter::toDomain);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Mono<Banner> findById(Long id) {
|
||||||
|
return bannerDao.findById(id)
|
||||||
|
.map(bannerConverter::toDomain);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Mono<Banner> save(Banner banner) {
|
||||||
|
BannerEntity entity = bannerConverter.toEntity(banner);
|
||||||
|
return bannerDao.save(entity)
|
||||||
|
.map(bannerConverter::toDomain);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Mono<Void> deleteByIdAndDeletedAtIsNull(Long id) {
|
||||||
|
return bannerDao.deleteByIdAndDeletedAtIsNull(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
+38
@@ -0,0 +1,38 @@
|
|||||||
|
-- ============================================
|
||||||
|
-- 团课推荐表
|
||||||
|
-- 版本: V10
|
||||||
|
-- 描述: 创建团课推荐表,用于首页推荐团课展示
|
||||||
|
-- ============================================
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS group_course_recommend (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
course_id BIGINT NOT NULL,
|
||||||
|
recommend_title VARCHAR(200),
|
||||||
|
recommend_content TEXT,
|
||||||
|
recommend_reason VARCHAR(500),
|
||||||
|
priority INTEGER DEFAULT 0,
|
||||||
|
is_active BOOLEAN DEFAULT TRUE,
|
||||||
|
create_by VARCHAR(50),
|
||||||
|
update_by VARCHAR(50),
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
deleted_at TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_group_course_recommend_course_id ON group_course_recommend(course_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_group_course_recommend_priority ON group_course_recommend(priority);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_group_course_recommend_is_active ON group_course_recommend(is_active);
|
||||||
|
|
||||||
|
COMMENT ON TABLE group_course_recommend IS '团课推荐表';
|
||||||
|
COMMENT ON COLUMN group_course_recommend.id IS '主键ID';
|
||||||
|
COMMENT ON COLUMN group_course_recommend.course_id IS '团课ID(关联group_course.id)';
|
||||||
|
COMMENT ON COLUMN group_course_recommend.recommend_title IS '推荐标题';
|
||||||
|
COMMENT ON COLUMN group_course_recommend.recommend_content IS '推荐内容';
|
||||||
|
COMMENT ON COLUMN group_course_recommend.recommend_reason IS '推荐理由';
|
||||||
|
COMMENT ON COLUMN group_course_recommend.priority IS '优先级(数字越大优先级越高)';
|
||||||
|
COMMENT ON COLUMN group_course_recommend.is_active IS '是否启用';
|
||||||
|
COMMENT ON COLUMN group_course_recommend.create_by IS '创建人';
|
||||||
|
COMMENT ON COLUMN group_course_recommend.update_by IS '更新人';
|
||||||
|
COMMENT ON COLUMN group_course_recommend.created_at IS '创建时间';
|
||||||
|
COMMENT ON COLUMN group_course_recommend.updated_at IS '更新时间';
|
||||||
|
COMMENT ON COLUMN group_course_recommend.deleted_at IS '删除时间(软删除)';
|
||||||
+9
@@ -0,0 +1,9 @@
|
|||||||
|
-- ============================================
|
||||||
|
-- 为团课表添加二维码路径字段
|
||||||
|
-- 版本: V11
|
||||||
|
-- 描述: 添加团课二维码路径,用于扫码签到
|
||||||
|
-- ============================================
|
||||||
|
|
||||||
|
ALTER TABLE group_course ADD COLUMN IF NOT EXISTS qr_code_path VARCHAR(500);
|
||||||
|
|
||||||
|
COMMENT ON COLUMN group_course.qr_code_path IS '二维码图片路径';
|
||||||
+10
@@ -0,0 +1,10 @@
|
|||||||
|
-- ============================================
|
||||||
|
-- 为 member_user 表添加 deleted_at 字段
|
||||||
|
-- 版本: V12
|
||||||
|
-- 描述: 补充软删除时间字段,与BaseEntity映射对齐
|
||||||
|
-- ============================================
|
||||||
|
|
||||||
|
ALTER TABLE member_user
|
||||||
|
ADD COLUMN IF NOT EXISTS deleted_at TIMESTAMP;
|
||||||
|
|
||||||
|
COMMENT ON COLUMN member_user.deleted_at IS '软删除时间';
|
||||||
+10
@@ -0,0 +1,10 @@
|
|||||||
|
-- ============================================
|
||||||
|
-- 为 member_card_transactions 表添加 updated_at 字段
|
||||||
|
-- 版本: V13
|
||||||
|
-- 描述: 补充更新时间字段,与BaseEntity映射对齐
|
||||||
|
-- ============================================
|
||||||
|
|
||||||
|
ALTER TABLE member_card_transactions
|
||||||
|
ADD COLUMN IF NOT EXISTS updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP;
|
||||||
|
|
||||||
|
COMMENT ON COLUMN member_card_transactions.updated_at IS '更新时间';
|
||||||
+63
@@ -0,0 +1,63 @@
|
|||||||
|
-- ============================================
|
||||||
|
-- 支付订单表
|
||||||
|
-- 版本: V14
|
||||||
|
-- 描述: 创建支付订单表,用于记录支付交易信息
|
||||||
|
-- ============================================
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS payment_order (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
order_no VARCHAR(64) NOT NULL UNIQUE,
|
||||||
|
member_id BIGINT,
|
||||||
|
order_type VARCHAR(32),
|
||||||
|
goods_desc VARCHAR(256),
|
||||||
|
trans_amt DECIMAL(12, 2),
|
||||||
|
trade_type VARCHAR(32),
|
||||||
|
pay_status VARCHAR(16) DEFAULT 'PENDING',
|
||||||
|
huifu_id VARCHAR(64),
|
||||||
|
req_seq_id VARCHAR(64),
|
||||||
|
req_date VARCHAR(16),
|
||||||
|
hf_seq_id VARCHAR(64),
|
||||||
|
pay_info TEXT,
|
||||||
|
qr_code TEXT,
|
||||||
|
pay_url TEXT,
|
||||||
|
remark VARCHAR(512),
|
||||||
|
notify_url VARCHAR(512),
|
||||||
|
pay_time TIMESTAMP,
|
||||||
|
expire_time TIMESTAMP,
|
||||||
|
error_code VARCHAR(64),
|
||||||
|
error_msg VARCHAR(512),
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
deleted_at TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_payment_order_order_no ON payment_order(order_no);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_payment_order_member_id ON payment_order(member_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_payment_order_req_seq_id ON payment_order(req_seq_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_payment_order_status ON payment_order(pay_status);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_payment_order_expire_time ON payment_order(expire_time);
|
||||||
|
|
||||||
|
COMMENT ON TABLE payment_order IS '支付订单表';
|
||||||
|
COMMENT ON COLUMN payment_order.order_no IS '订单号';
|
||||||
|
COMMENT ON COLUMN payment_order.member_id IS '会员ID';
|
||||||
|
COMMENT ON COLUMN payment_order.order_type IS '订单类型';
|
||||||
|
COMMENT ON COLUMN payment_order.goods_desc IS '商品描述';
|
||||||
|
COMMENT ON COLUMN payment_order.trans_amt IS '交易金额';
|
||||||
|
COMMENT ON COLUMN payment_order.trade_type IS '交易类型';
|
||||||
|
COMMENT ON COLUMN payment_order.pay_status IS '支付状态:PENDING-待支付,SUCCESS-成功,FAILED-失败';
|
||||||
|
COMMENT ON COLUMN payment_order.huifu_id IS '汇付商户ID';
|
||||||
|
COMMENT ON COLUMN payment_order.req_seq_id IS '请求流水号';
|
||||||
|
COMMENT ON COLUMN payment_order.req_date IS '请求日期';
|
||||||
|
COMMENT ON COLUMN payment_order.hf_seq_id IS '汇付流水号';
|
||||||
|
COMMENT ON COLUMN payment_order.pay_info IS '支付信息';
|
||||||
|
COMMENT ON COLUMN payment_order.qr_code IS '二维码';
|
||||||
|
COMMENT ON COLUMN payment_order.pay_url IS '支付链接';
|
||||||
|
COMMENT ON COLUMN payment_order.remark IS '备注';
|
||||||
|
COMMENT ON COLUMN payment_order.notify_url IS '通知地址';
|
||||||
|
COMMENT ON COLUMN payment_order.pay_time IS '支付时间';
|
||||||
|
COMMENT ON COLUMN payment_order.expire_time IS '过期时间';
|
||||||
|
COMMENT ON COLUMN payment_order.error_code IS '错误码';
|
||||||
|
COMMENT ON COLUMN payment_order.error_msg IS '错误信息';
|
||||||
|
COMMENT ON COLUMN payment_order.created_at IS '创建时间';
|
||||||
|
COMMENT ON COLUMN payment_order.updated_at IS '更新时间';
|
||||||
|
COMMENT ON COLUMN payment_order.deleted_at IS '删除时间';
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
-- ============================================
|
||||||
|
-- 支付订单表添加 party_order_id 字段
|
||||||
|
-- 版本: V15
|
||||||
|
-- 描述: 添加汇付支付返回的商户订单号
|
||||||
|
-- ============================================
|
||||||
|
|
||||||
|
ALTER TABLE payment_order ADD COLUMN IF NOT EXISTS party_order_id VARCHAR(64);
|
||||||
|
|
||||||
|
COMMENT ON COLUMN payment_order.party_order_id IS '汇付商户订单号';
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
-- ============================================
|
||||||
|
-- 储值卡相关表
|
||||||
|
-- 版本: V16
|
||||||
|
-- 描述: 创建储值卡表和充值记录表,每个会员只有一张储值卡
|
||||||
|
-- ============================================
|
||||||
|
|
||||||
|
-- 储值卡表(包含支付密码)
|
||||||
|
CREATE TABLE IF NOT EXISTS member_stored_card (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
member_id BIGINT NOT NULL,
|
||||||
|
balance DECIMAL(10, 2) DEFAULT 0.00,
|
||||||
|
total_recharge DECIMAL(10, 2) DEFAULT 0.00,
|
||||||
|
total_consume DECIMAL(10, 2) DEFAULT 0.00,
|
||||||
|
pay_password VARCHAR(255),
|
||||||
|
is_pay_password_set BOOLEAN DEFAULT FALSE,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
deleted_at TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_member_stored_card_member_id ON member_stored_card(member_id) WHERE deleted_at IS NULL;
|
||||||
|
|
||||||
|
COMMENT ON TABLE member_stored_card IS '储值卡表,每个会员只有一张储值卡,包含支付密码';
|
||||||
|
COMMENT ON COLUMN member_stored_card.member_id IS '会员用户ID';
|
||||||
|
COMMENT ON COLUMN member_stored_card.balance IS '储值卡余额';
|
||||||
|
COMMENT ON COLUMN member_stored_card.total_recharge IS '累计充值金额';
|
||||||
|
COMMENT ON COLUMN member_stored_card.total_consume IS '累计消费金额';
|
||||||
|
COMMENT ON COLUMN member_stored_card.pay_password IS '支付密码(BCrypt加密)';
|
||||||
|
COMMENT ON COLUMN member_stored_card.is_pay_password_set IS '是否已设置支付密码';
|
||||||
|
|
||||||
|
-- 储值卡充值记录表
|
||||||
|
CREATE TABLE IF NOT EXISTS member_stored_card_recharge (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
member_id BIGINT NOT NULL,
|
||||||
|
order_no VARCHAR(64) NOT NULL,
|
||||||
|
recharge_amount DECIMAL(10, 2) DEFAULT 0.00,
|
||||||
|
bonus_amount DECIMAL(10, 2) DEFAULT 0.00,
|
||||||
|
total_amount DECIMAL(10, 2) DEFAULT 0.00,
|
||||||
|
pay_amount DECIMAL(10, 2) DEFAULT 0.00,
|
||||||
|
status VARCHAR(32) DEFAULT 'PENDING',
|
||||||
|
pay_time TIMESTAMP,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
deleted_at TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_stored_card_recharge_member_id ON member_stored_card_recharge(member_id) WHERE deleted_at IS NULL;
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_stored_card_recharge_order_no ON member_stored_card_recharge(order_no) WHERE deleted_at IS NULL;
|
||||||
|
|
||||||
|
COMMENT ON TABLE member_stored_card_recharge IS '储值卡充值记录表';
|
||||||
|
COMMENT ON COLUMN member_stored_card_recharge.member_id IS '会员用户ID';
|
||||||
|
COMMENT ON COLUMN member_stored_card_recharge.order_no IS '订单号';
|
||||||
|
COMMENT ON COLUMN member_stored_card_recharge.recharge_amount IS '充值金额';
|
||||||
|
COMMENT ON COLUMN member_stored_card_recharge.bonus_amount IS '赠送金额';
|
||||||
|
COMMENT ON COLUMN member_stored_card_recharge.total_amount IS '到账总金额(充值+赠送)';
|
||||||
|
COMMENT ON COLUMN member_stored_card_recharge.pay_amount IS '实际支付金额';
|
||||||
|
COMMENT ON COLUMN member_stored_card_recharge.status IS '充值状态:PENDING-待支付,SUCCESS-成功,FAILED-失败';
|
||||||
|
COMMENT ON COLUMN member_stored_card_recharge.pay_time IS '支付时间';
|
||||||
+8
@@ -0,0 +1,8 @@
|
|||||||
|
-- ============================================
|
||||||
|
-- 将 group_course_booking 表的 member_card_id 改为可空
|
||||||
|
-- 版本: V17
|
||||||
|
-- 描述: 预约团课不再需要会员卡,member_card_id 允许为空
|
||||||
|
-- ============================================
|
||||||
|
|
||||||
|
ALTER TABLE group_course_booking
|
||||||
|
ALTER COLUMN member_card_id DROP NOT NULL;
|
||||||
+209
@@ -0,0 +1,209 @@
|
|||||||
|
-- ============================================
|
||||||
|
-- 团课测试数据
|
||||||
|
-- 版本: V18
|
||||||
|
-- 描述: 插入团课标签、类型、关联关系及课程数据
|
||||||
|
-- 5个团课类型,14个标签,每类型3个标签,每类型5个团课
|
||||||
|
-- ============================================
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- 1. 团课标签数据(14个)
|
||||||
|
-- ============================================
|
||||||
|
|
||||||
|
INSERT INTO course_label (label_name, color, description) VALUES
|
||||||
|
('适合新手', '#52c41a', '适合健身初学者,动作简单易学'),
|
||||||
|
('中级过渡', '#faad14', '适合有一定基础的学员,逐步提升难度'),
|
||||||
|
('高级进阶', '#f5222d', '适合高级学员,强度和技巧要求较高'),
|
||||||
|
('减脂塑形', '#722ed1', '高效燃烧卡路里,助力减脂和体型塑造'),
|
||||||
|
('增肌强化', '#13c2c2', '注重肌肉力量和围度增长'),
|
||||||
|
('柔韧性训练', '#eb2f96', '提升身体柔韧性和关节活动度'),
|
||||||
|
('核心训练', '#1890ff', '强化核心肌群,改善身体稳定性'),
|
||||||
|
('心肺训练', '#fa8c16', '提升心肺功能和耐力水平'),
|
||||||
|
('低冲击', '#52c41a', '低冲击运动,对关节友好'),
|
||||||
|
('高强度', '#f5222d', '高强度训练,短时间内最大化训练效果'),
|
||||||
|
('团体互动', '#722ed1', '注重团队协作和互动氛围'),
|
||||||
|
('私教推荐', '#13c2c2', '私教推荐的高效训练课程'),
|
||||||
|
('热门课程', '#ff1493', '人气较高,广受学员欢迎'),
|
||||||
|
('新课上线', '#00ced1', '新上线课程,欢迎尝鲜体验')
|
||||||
|
ON CONFLICT DO NOTHING;
|
||||||
|
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- 2. 团课类型数据(5个)
|
||||||
|
-- ============================================
|
||||||
|
|
||||||
|
INSERT INTO group_course_type (type_name, base_difficulty, description, category) VALUES
|
||||||
|
('瑜伽', 3, '通过呼吸调控与体式练习,提升身体柔韧性、力量与心灵平静,适合各年龄段人群', '柔韧与平衡类'),
|
||||||
|
('动感单车', 5, '伴随动感音乐节奏进行室内骑行,高效燃烧卡路里,释放压力,体验极速快感', '基础有氧与热身'),
|
||||||
|
('HIIT训练', 8, '高强度间歇训练,通过短时爆发动作与间歇休息交替,短时间内达到最大燃脂效果', '高强度与爆发力'),
|
||||||
|
('普拉提', 4, '注重核心肌群控制、脊柱对齐和呼吸配合,有效改善体态,预防运动损伤', '柔韧与平衡类'),
|
||||||
|
('搏击操', 6, '融合拳击、踢腿、格斗动作的有氧运动,宣泄压力同时高效塑形,增强协调性', '高强度与爆发力')
|
||||||
|
ON CONFLICT DO NOTHING;
|
||||||
|
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- 3. 类型-标签关联数据(每类型3个标签,共15条)
|
||||||
|
-- ============================================
|
||||||
|
|
||||||
|
INSERT INTO course_type_label (type_id, label_id)
|
||||||
|
SELECT gt.id, cl.id FROM group_course_type gt, course_label cl
|
||||||
|
WHERE gt.type_name = '瑜伽' AND cl.label_name = '柔韧性训练'
|
||||||
|
OR gt.type_name = '瑜伽' AND cl.label_name = '低冲击'
|
||||||
|
OR gt.type_name = '瑜伽' AND cl.label_name = '适合新手'
|
||||||
|
OR gt.type_name = '动感单车' AND cl.label_name = '心肺训练'
|
||||||
|
OR gt.type_name = '动感单车' AND cl.label_name = '热门课程'
|
||||||
|
OR gt.type_name = '动感单车' AND cl.label_name = '中级过渡'
|
||||||
|
OR gt.type_name = 'HIIT训练' AND cl.label_name = '高强度'
|
||||||
|
OR gt.type_name = 'HIIT训练' AND cl.label_name = '减脂塑形'
|
||||||
|
OR gt.type_name = 'HIIT训练' AND cl.label_name = '高级进阶'
|
||||||
|
OR gt.type_name = '普拉提' AND cl.label_name = '核心训练'
|
||||||
|
OR gt.type_name = '普拉提' AND cl.label_name = '柔韧性训练'
|
||||||
|
OR gt.type_name = '普拉提' AND cl.label_name = '适合新手'
|
||||||
|
OR gt.type_name = '搏击操' AND cl.label_name = '高强度'
|
||||||
|
OR gt.type_name = '搏击操' AND cl.label_name = '减脂塑形'
|
||||||
|
OR gt.type_name = '搏击操' AND cl.label_name = '心肺训练'
|
||||||
|
ON CONFLICT DO NOTHING;
|
||||||
|
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- 4. 团课课程数据(5个类型 × 5个课程 = 25个)
|
||||||
|
-- start_time / end_time 分布在 2026-07-22 至 2026-07-28
|
||||||
|
-- ============================================
|
||||||
|
|
||||||
|
-- ---------- 瑜伽课程(5个)----------
|
||||||
|
|
||||||
|
INSERT INTO group_course (course_name, course_type, start_time, end_time, max_members, current_members, status, location, cover_image, description, stored_value_amount, create_by)
|
||||||
|
SELECT '哈他瑜伽入门', gt.id, '2026-07-22 08:00:00'::TIMESTAMP, '2026-07-22 09:00:00'::TIMESTAMP, 25, 12, '0', '瑜伽室A', '/static/course/yoga_hatha.jpg', '经典哈他瑜伽,从基础体式入手,配合呼吸引导,帮助初学者建立正确的瑜伽练习基础,感受身心的和谐统一。', 88.00, 'system'
|
||||||
|
FROM group_course_type gt WHERE gt.type_name = '瑜伽';
|
||||||
|
|
||||||
|
INSERT INTO group_course (course_name, course_type, start_time, end_time, max_members, current_members, status, location, cover_image, description, stored_value_amount, create_by)
|
||||||
|
SELECT '流瑜伽', gt.id, '2026-07-24 10:00:00'::TIMESTAMP, '2026-07-24 11:00:00'::TIMESTAMP, 20, 18, '0', '瑜伽室A', '/static/course/yoga_flow.jpg', '体式之间流畅串联,如行云流水般一气呵成。以拜日式为根基,结合站姿、平衡和扭转,提升力量与柔韧的整合能力。', 98.00, 'system'
|
||||||
|
FROM group_course_type gt WHERE gt.type_name = '瑜伽';
|
||||||
|
|
||||||
|
INSERT INTO group_course (course_name, course_type, start_time, end_time, max_members, current_members, status, location, cover_image, description, stored_value_amount, create_by)
|
||||||
|
SELECT '阴瑜伽', gt.id, '2026-07-26 18:00:00'::TIMESTAMP, '2026-07-26 19:15:00'::TIMESTAMP, 20, 8, '0', '瑜伽室B', '/static/course/yoga_yin.jpg', '阴瑜伽以长时间保持体式为特点,深度伸展筋膜和结缔组织,帮助你释放身体深层紧张,缓解一周的工作疲劳。适合所有级别。', 88.00, 'system'
|
||||||
|
FROM group_course_type gt WHERE gt.type_name = '瑜伽';
|
||||||
|
|
||||||
|
INSERT INTO group_course (course_name, course_type, start_time, end_time, max_members, current_members, status, location, cover_image, description, stored_value_amount, create_by)
|
||||||
|
SELECT '阿斯汤加瑜伽', gt.id, '2026-07-23 07:00:00'::TIMESTAMP, '2026-07-23 08:30:00'::TIMESTAMP, 15, 10, '0', '瑜伽室A', '/static/course/yoga_ashtanga.jpg', '阿斯汤加瑜伽以固定的体式序列和独特的呼吸法为核心,节奏紧凑,强度较高,能有效提升力量、柔韧和专注力。建议有一定瑜伽基础者参加。', 128.00, 'system'
|
||||||
|
FROM group_course_type gt WHERE gt.type_name = '瑜伽';
|
||||||
|
|
||||||
|
INSERT INTO group_course (course_name, course_type, start_time, end_time, max_members, current_members, status, location, cover_image, description, stored_value_amount, create_by)
|
||||||
|
SELECT '空中瑜伽', gt.id, '2026-07-25 19:00:00'::TIMESTAMP, '2026-07-25 20:00:00'::TIMESTAMP, 12, 12, '0', '瑜伽室B', '/static/course/yoga_aerial.jpg', '利用悬挂的瑜伽吊床完成各种体式,借助重力让脊柱自然延展,体验"飞翔"般的自由感。深受女性学员喜爱,名额有限请提前预约。', 128.00, 'system'
|
||||||
|
FROM group_course_type gt WHERE gt.type_name = '瑜伽';
|
||||||
|
|
||||||
|
|
||||||
|
-- ---------- 动感单车课程(5个)----------
|
||||||
|
|
||||||
|
INSERT INTO group_course (course_name, course_type, start_time, end_time, max_members, current_members, status, location, cover_image, description, stored_value_amount, create_by)
|
||||||
|
SELECT '初级燃脂骑行', gt.id, '2026-07-22 19:00:00'::TIMESTAMP, '2026-07-22 19:45:00'::TIMESTAMP, 30, 22, '0', '单车房', '/static/course/spin_beginner.jpg', '适合新手的入门级骑行课程,教练将带领你掌握正确的骑行姿势和阻力调节技巧。在动感音乐中轻松燃烧300-400卡路里。', 58.00, 'system'
|
||||||
|
FROM group_course_type gt WHERE gt.type_name = '动感单车';
|
||||||
|
|
||||||
|
INSERT INTO group_course (course_name, course_type, start_time, end_time, max_members, current_members, status, location, cover_image, description, stored_value_amount, create_by)
|
||||||
|
SELECT '节奏爬坡', gt.id, '2026-07-24 18:30:00'::TIMESTAMP, '2026-07-24 19:15:00'::TIMESTAMP, 30, 15, '0', '单车房', '/static/course/spin_climb.jpg', '模拟山路爬坡骑行,通过逐渐增加阻力来挑战你的耐力和意志力。每一段爬坡后都有短暂的平路冲刺,节奏感十足,大汗淋漓的畅快体验。', 68.00, 'system'
|
||||||
|
FROM group_course_type gt WHERE gt.type_name = '动感单车';
|
||||||
|
|
||||||
|
INSERT INTO group_course (course_name, course_type, start_time, end_time, max_members, current_members, status, location, cover_image, description, stored_value_amount, create_by)
|
||||||
|
SELECT '极速冲刺', gt.id, '2026-07-26 20:00:00'::TIMESTAMP, '2026-07-26 20:45:00'::TIMESTAMP, 25, 25, '0', '单车房', '/static/course/spin_sprint.jpg', '高强度冲刺为主题的骑行课,多组30秒极限冲刺搭配短暂恢复,彻底引爆你的心肺极限。周五夜间的爆款课程,约满速度极快!', 68.00, 'system'
|
||||||
|
FROM group_course_type gt WHERE gt.type_name = '动感单车';
|
||||||
|
|
||||||
|
INSERT INTO group_course (course_name, course_type, start_time, end_time, max_members, current_members, status, location, cover_image, description, stored_value_amount, create_by)
|
||||||
|
SELECT '耐力骑行', gt.id, '2026-07-27 10:00:00'::TIMESTAMP, '2026-07-27 11:00:00'::TIMESTAMP, 30, 18, '0', '单车房', '/static/course/spin_endurance.jpg', '60分钟中低强度耐力骑行,适合周末早晨唤醒身体。在稳定的节奏中持续燃脂,配以舒缓的收尾拉伸,开启元气满满的周末。', 58.00, 'system'
|
||||||
|
FROM group_course_type gt WHERE gt.type_name = '动感单车';
|
||||||
|
|
||||||
|
INSERT INTO group_course (course_name, course_type, start_time, end_time, max_members, current_members, status, location, cover_image, description, stored_value_amount, create_by)
|
||||||
|
SELECT '音乐主题骑行', gt.id, '2026-07-23 19:30:00'::TIMESTAMP, '2026-07-23 20:15:00'::TIMESTAMP, 30, 20, '0', '单车房', '/static/course/spin_music.jpg', '以热门流行音乐为主题的骑行派对!每首歌曲对应一套骑行组合,跟着节拍加速、爬坡、冲刺,在音乐中忘记疲惫。', 68.00, 'system'
|
||||||
|
FROM group_course_type gt WHERE gt.type_name = '动感单车';
|
||||||
|
|
||||||
|
|
||||||
|
-- ---------- HIIT训练课程(5个)----------
|
||||||
|
|
||||||
|
INSERT INTO group_course (course_name, course_type, start_time, end_time, max_members, current_members, status, location, cover_image, description, stored_value_amount, create_by)
|
||||||
|
SELECT 'Tabata燃脂', gt.id, '2026-07-22 07:00:00'::TIMESTAMP, '2026-07-22 07:30:00'::TIMESTAMP, 15, 8, '0', '多功能训练区', '/static/course/hiit_tabata.jpg', '经典的Tabata模式:20秒全力运动 + 10秒休息,循环8轮共4分钟,搭配热身和拉伸,30分钟全身燃爆。早晨空腹训练燃脂效果加倍!', 98.00, 'system'
|
||||||
|
FROM group_course_type gt WHERE gt.type_name = 'HIIT训练';
|
||||||
|
|
||||||
|
INSERT INTO group_course (course_name, course_type, start_time, end_time, max_members, current_members, status, location, cover_image, description, stored_value_amount, create_by)
|
||||||
|
SELECT '全身循环训练', gt.id, '2026-07-24 12:00:00'::TIMESTAMP, '2026-07-24 12:45:00'::TIMESTAMP, 15, 12, '0', '多功能训练区', '/static/course/hiit_circuit.jpg', '午间燃脂利器!8个动作站点轮流进行,涵盖深蹲跳、波比跳、壶铃摇摆、登山跑等,每个动作45秒,休息15秒,3轮循环让你全身湿透。', 98.00, 'system'
|
||||||
|
FROM group_course_type gt WHERE gt.type_name = 'HIIT训练';
|
||||||
|
|
||||||
|
INSERT INTO group_course (course_name, course_type, start_time, end_time, max_members, current_members, status, location, cover_image, description, stored_value_amount, create_by)
|
||||||
|
SELECT '核心爆发力', gt.id, '2026-07-26 17:30:00'::TIMESTAMP, '2026-07-26 18:15:00'::TIMESTAMP, 12, 5, '0', '多功能训练区', '/static/course/hiit_core.jpg', '专注于核心肌群的HIIT训练,结合药球砸地、悬垂举腿、俄罗斯转体和平板支撑变式,打造钢铁般的核心力量,提升所有运动表现。', 108.00, 'system'
|
||||||
|
FROM group_course_type gt WHERE gt.type_name = 'HIIT训练';
|
||||||
|
|
||||||
|
INSERT INTO group_course (course_name, course_type, start_time, end_time, max_members, current_members, status, location, cover_image, description, stored_value_amount, create_by)
|
||||||
|
SELECT '战绳挑战', gt.id, '2026-07-27 08:00:00'::TIMESTAMP, '2026-07-27 08:45:00'::TIMESTAMP, 10, 10, '0', '户外训练区', '/static/course/hiit_battle.jpg', '风靡全球的战绳训练!通过波浪、猛击、旋转等动作模式激活全身肌群,30秒一组高强度间歇,燃脂同时雕刻上肢线条。名额有限,已约满!', 128.00, 'system'
|
||||||
|
FROM group_course_type gt WHERE gt.type_name = 'HIIT训练';
|
||||||
|
|
||||||
|
INSERT INTO group_course (course_name, course_type, start_time, end_time, max_members, current_members, status, location, cover_image, description, stored_value_amount, create_by)
|
||||||
|
SELECT '药球HIIT', gt.id, '2026-07-28 10:00:00'::TIMESTAMP, '2026-07-28 10:45:00'::TIMESTAMP, 12, 6, '0', '多功能训练区', '/static/course/hiit_medball.jpg', '以药球为主要工具的HIIT训练。药球砸地、旋转投掷、过头抛接等动作兼具力量和爆发力训练,动作多样不枯燥,周日上午的爆汗之选。', 108.00, 'system'
|
||||||
|
FROM group_course_type gt WHERE gt.type_name = 'HIIT训练';
|
||||||
|
|
||||||
|
|
||||||
|
-- ---------- 普拉提课程(5个)----------
|
||||||
|
|
||||||
|
INSERT INTO group_course (course_name, course_type, start_time, end_time, max_members, current_members, status, location, cover_image, description, stored_value_amount, create_by)
|
||||||
|
SELECT '垫上普拉提', gt.id, '2026-07-23 10:00:00'::TIMESTAMP, '2026-07-23 11:00:00'::TIMESTAMP, 20, 14, '0', '普拉提室', '/static/course/pilates_mat.jpg', '经典垫上普拉提,通过精准的动作控制激活深层核心肌群。从呼吸到骨盆稳定,从脊柱逐节卷动到四肢协调,一节课改善你的身体感知。', 108.00, 'system'
|
||||||
|
FROM group_course_type gt WHERE gt.type_name = '普拉提';
|
||||||
|
|
||||||
|
INSERT INTO group_course (course_name, course_type, start_time, end_time, max_members, current_members, status, location, cover_image, description, stored_value_amount, create_by)
|
||||||
|
SELECT '器械普拉提', gt.id, '2026-07-25 09:00:00'::TIMESTAMP, '2026-07-25 10:00:00'::TIMESTAMP, 10, 8, '0', '普拉提室', '/static/course/pilates_reformer.jpg', '使用专业普拉提核心床(Reformer)进行训练,弹簧阻力提供精准的负荷控制,适合需要针对性改善体态、康复训练或追求高效塑形的学员。', 158.00, 'system'
|
||||||
|
FROM group_course_type gt WHERE gt.type_name = '普拉提';
|
||||||
|
|
||||||
|
INSERT INTO group_course (course_name, course_type, start_time, end_time, max_members, current_members, status, location, cover_image, description, stored_value_amount, create_by)
|
||||||
|
SELECT '核心塑形', gt.id, '2026-07-27 14:00:00'::TIMESTAMP, '2026-07-27 15:00:00'::TIMESTAMP, 15, 9, '0', '普拉提室', '/static/course/pilates_sculpt.jpg', '专注于腹部、腰部和臀部的普拉提塑形课程。百次拍击、剪刀腿、肩桥等经典动作持续刺激目标肌群,打造紧致核心线条。', 128.00, 'system'
|
||||||
|
FROM group_course_type gt WHERE gt.type_name = '普拉提';
|
||||||
|
|
||||||
|
INSERT INTO group_course (course_name, course_type, start_time, end_time, max_members, current_members, status, location, cover_image, description, stored_value_amount, create_by)
|
||||||
|
SELECT '脊柱健康', gt.id, '2026-07-22 16:00:00'::TIMESTAMP, '2026-07-22 17:00:00'::TIMESTAMP, 15, 6, '0', '普拉提室', '/static/course/pilates_spine.jpg', '专为久坐办公人群设计,通过普拉提脊柱逐节运动改善驼背、圆肩等不良体态。融合猫牛式、脊柱旋转和游泳式,给你的脊柱一次深度保养。', 108.00, 'system'
|
||||||
|
FROM group_course_type gt WHERE gt.type_name = '普拉提';
|
||||||
|
|
||||||
|
INSERT INTO group_course (course_name, course_type, start_time, end_time, max_members, current_members, status, location, cover_image, description, stored_value_amount, create_by)
|
||||||
|
SELECT '产后恢复普拉提', gt.id, '2026-07-24 11:00:00'::TIMESTAMP, '2026-07-24 12:00:00'::TIMESTAMP, 10, 4, '0', '普拉提室', '/static/course/pilates_postnatal.jpg', '专为产后妈妈设计的温和修复课程,重点激活盆底肌和腹横肌,循序渐进恢复核心力量。小班教学,教练一对一关注每位学员的姿势质量。', 158.00, 'system'
|
||||||
|
FROM group_course_type gt WHERE gt.type_name = '普拉提';
|
||||||
|
|
||||||
|
|
||||||
|
-- ---------- 搏击操课程(5个)----------
|
||||||
|
|
||||||
|
INSERT INTO group_course (course_name, course_type, start_time, end_time, max_members, current_members, status, location, cover_image, description, stored_value_amount, create_by)
|
||||||
|
SELECT '有氧搏击基础', gt.id, '2026-07-23 18:00:00'::TIMESTAMP, '2026-07-23 19:00:00'::TIMESTAMP, 25, 20, '0', '操厅A', '/static/course/boxing_basic.jpg', '零基础友好的搏击操入门课。从直拳、摆拳、勾拳到前踢、侧踹,教练分步讲解每个动作的要领,配合动感音乐串联成完整的搏击组合。', 68.00, 'system'
|
||||||
|
FROM group_course_type gt WHERE gt.type_name = '搏击操';
|
||||||
|
|
||||||
|
INSERT INTO group_course (course_name, course_type, start_time, end_time, max_members, current_members, status, location, cover_image, description, stored_value_amount, create_by)
|
||||||
|
SELECT 'Kickboxing进阶', gt.id, '2026-07-25 19:00:00'::TIMESTAMP, '2026-07-25 20:00:00'::TIMESTAMP, 20, 15, '0', '操厅A', '/static/course/boxing_kickboxing.jpg', '融合踢拳技术的进阶搏击课程,增加连击组合、闪躲步法和膝肘技术。强度较基础课明显提升,汗如雨下的同时释放工作压力,宣泄效果一流。', 88.00, 'system'
|
||||||
|
FROM group_course_type gt WHERE gt.type_name = '搏击操';
|
||||||
|
|
||||||
|
INSERT INTO group_course (course_name, course_type, start_time, end_time, max_members, current_members, status, location, cover_image, description, stored_value_amount, create_by)
|
||||||
|
SELECT '综合格斗体能', gt.id, '2026-07-27 16:00:00'::TIMESTAMP, '2026-07-27 17:00:00'::TIMESTAMP, 20, 12, '0', '操厅A', '/static/course/boxing_mma.jpg', '融合拳击、泰拳、摔跤元素的综合体能训练。沙袋击打、地面对抗和爆发力训练交替进行,全面提升力量、速度和耐力。周六下午的硬核之选!', 98.00, 'system'
|
||||||
|
FROM group_course_type gt WHERE gt.type_name = '搏击操';
|
||||||
|
|
||||||
|
INSERT INTO group_course (course_name, course_type, start_time, end_time, max_members, current_members, status, location, cover_image, description, stored_value_amount, create_by)
|
||||||
|
SELECT '燃脂拳击', gt.id, '2026-07-22 12:00:00'::TIMESTAMP, '2026-07-22 13:00:00'::TIMESTAMP, 25, 16, '0', '操厅A', '/static/course/boxing_fatburn.jpg', '午间燃脂拳击课。空击组合与沙袋击打间歇进行,心率维持在高燃脂区间。一节课可消耗500-700卡路里,中午来打拳比喝咖啡更提神!', 68.00, 'system'
|
||||||
|
FROM group_course_type gt WHERE gt.type_name = '搏击操';
|
||||||
|
|
||||||
|
INSERT INTO group_course (course_name, course_type, start_time, end_time, max_members, current_members, status, location, cover_image, description, stored_value_amount, create_by)
|
||||||
|
SELECT '泰拳基础', gt.id, '2026-07-28 15:00:00'::TIMESTAMP, '2026-07-28 16:00:00'::TIMESTAMP, 15, 7, '0', '操厅A', '/static/course/boxing_muaythai.jpg', '学习泰拳的八肢艺术(双拳、双腿、双膝、双肘),从基本站架到肘膝组合,感受泰拳的刚猛魅力。小班教学保证动作纠正质量。', 98.00, 'system'
|
||||||
|
FROM group_course_type gt WHERE gt.type_name = '搏击操';
|
||||||
|
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- 5. 团课推荐数据(每类型推荐1个热门课程,共5条)
|
||||||
|
-- ============================================
|
||||||
|
|
||||||
|
INSERT INTO group_course_recommend (course_id, recommend_title, recommend_content, recommend_reason, priority, is_active)
|
||||||
|
SELECT gc.id, '清晨之选 - 哈他瑜伽入门', '周一早晨8点,用温和的瑜伽唤醒身体,开启高效一周', '最受欢迎的入门瑜伽课,教练耐心细致', 5, TRUE
|
||||||
|
FROM group_course gc WHERE gc.course_name = '哈他瑜伽入门';
|
||||||
|
|
||||||
|
INSERT INTO group_course_recommend (course_id, recommend_title, recommend_content, recommend_reason, priority, is_active)
|
||||||
|
SELECT gc.id, '周五狂欢 - 极速冲刺', '周五晚8点,用极速骑行释放一周的疲惫与压力', '单车房王牌课程,周周爆满,燃烧500+卡路里', 4, TRUE
|
||||||
|
FROM group_course gc WHERE gc.course_name = '极速冲刺';
|
||||||
|
|
||||||
|
INSERT INTO group_course_recommend (course_id, recommend_title, recommend_content, recommend_reason, priority, is_active)
|
||||||
|
SELECT gc.id, '午间爆汗 - 全身循环训练', '午休45分钟,8个站点循环,高效利用碎片时间燃脂', '午间最受欢迎的HIIT课,适合忙碌的上班族', 3, TRUE
|
||||||
|
FROM group_course gc WHERE gc.course_name = '全身循环训练';
|
||||||
|
|
||||||
|
INSERT INTO group_course_recommend (course_id, recommend_title, recommend_content, recommend_reason, priority, is_active)
|
||||||
|
SELECT gc.id, '久坐救星 - 脊柱健康普拉提', '专为久坐族设计的脊柱保养课,改善驼背圆肩', '全网好评,学员反馈"一节课腰就不疼了"', 2, TRUE
|
||||||
|
FROM group_course gc WHERE gc.course_name = '脊柱健康';
|
||||||
|
|
||||||
|
INSERT INTO group_course_recommend (course_id, recommend_title, recommend_content, recommend_reason, priority, is_active)
|
||||||
|
SELECT gc.id, '释压首选 - 有氧搏击基础', '零基础也能打!在拳拳到肉的快感中忘记烦恼', '新手友好,发泄压力效果拔群,超过200名学员推荐', 1, TRUE
|
||||||
|
FROM group_course gc WHERE gc.course_name = '有氧搏击基础';
|
||||||
@@ -0,0 +1,315 @@
|
|||||||
|
-- ============================================
|
||||||
|
-- V19: 系统初始数据(角色/用户/权限/菜单/字典/配置)
|
||||||
|
-- 来源:合并原 V2 + V22 + V28 + V29 + V30 + V31
|
||||||
|
-- ============================================
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- 1. 角色数据
|
||||||
|
-- ============================================
|
||||||
|
INSERT INTO sys_role (id, role_name, role_key, role_sort, status, create_by, update_by, created_at, updated_at)
|
||||||
|
VALUES
|
||||||
|
(1, '超级管理员', 'admin', 1, 1, 'system', 'system', NOW(), NOW()),
|
||||||
|
(2, '测试管理员', 'test_admin', 2, 1, 'system', 'system', NOW(), NOW()),
|
||||||
|
(3, '普通用户', 'normal_user', 3, 1, 'system', 'system', NOW(), NOW()),
|
||||||
|
(4, '访客', 'guest', 4, 1, 'system', 'system', NOW(), NOW());
|
||||||
|
|
||||||
|
SELECT setval('sys_role_id_seq', 4);
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- 2. 用户数据(密码均为 Test@123)
|
||||||
|
-- ============================================
|
||||||
|
INSERT INTO sys_user (id, username, password, email, phone, nickname, status, create_by, update_by, created_at, updated_at)
|
||||||
|
VALUES
|
||||||
|
(1, 'admin', '$2a$12$nZ1EMUpZQljbnEdIKzH72eHlDJKUmHmHppnTTVth/SlHs5VpSAr8C', 'admin@novalon.com', '13800138000', '超级管理员', 1, 'system', 'system', NOW(), NOW()),
|
||||||
|
(2, 'user', '$2a$12$nZ1EMUpZQljbnEdIKzH72eHlDJKUmHmHppnTTVth/SlHs5VpSAr8C', 'user@novalon.com', '13800138001', '普通用户', 1, 'system', 'system', NOW(), NOW()),
|
||||||
|
(10,'e2e_test_user', '$2a$12$nZ1EMUpZQljbnEdIKzH72eHlDJKUmHmHppnTTVth/SlHs5VpSAr8C', 'e2e@test.com', '13900139000', 'E2E测试用户', 1, 'system', 'system', NOW(), NOW())
|
||||||
|
ON CONFLICT (username) DO UPDATE SET
|
||||||
|
password = EXCLUDED.password,
|
||||||
|
status = EXCLUDED.status;
|
||||||
|
|
||||||
|
SELECT setval('sys_user_id_seq', 10);
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- 3. 用户角色关联
|
||||||
|
-- ============================================
|
||||||
|
INSERT INTO user_role (user_id, role_id, created_by, created_at)
|
||||||
|
VALUES
|
||||||
|
(1, 1, 'system', NOW()),
|
||||||
|
(2, 3, 'system', NOW()),
|
||||||
|
(10, 1, 'system', NOW())
|
||||||
|
ON CONFLICT (user_id, role_id) DO NOTHING;
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- 4. 权限数据
|
||||||
|
-- ============================================
|
||||||
|
|
||||||
|
-- 4.1 系统管理权限
|
||||||
|
INSERT INTO sys_permission (permission_name, permission_code, resource, action, description, status, create_by, update_by, created_at, updated_at) VALUES
|
||||||
|
('用户查看', 'system:user:view', '/api/users', 'GET', '查看用户列表', 1, 'system', 'system', NOW(), NOW()),
|
||||||
|
('用户创建', 'system:user:create', '/api/users', 'POST', '创建用户', 1, 'system', 'system', NOW(), NOW()),
|
||||||
|
('用户编辑', 'system:user:edit', '/api/users', 'PUT', '编辑用户', 1, 'system', 'system', NOW(), NOW()),
|
||||||
|
('用户删除', 'system:user:delete', '/api/users', 'DELETE', '删除用户', 1, 'system', 'system', NOW(), NOW()),
|
||||||
|
('角色查看', 'system:role:view', '/api/roles', 'GET', '查看角色列表', 1, 'system', 'system', NOW(), NOW()),
|
||||||
|
('角色创建', 'system:role:create', '/api/roles', 'POST', '创建角色', 1, 'system', 'system', NOW(), NOW()),
|
||||||
|
('角色编辑', 'system:role:edit', '/api/roles', 'PUT', '编辑角色', 1, 'system', 'system', NOW(), NOW()),
|
||||||
|
('角色删除', 'system:role:delete', '/api/roles', 'DELETE', '删除角色', 1, 'system', 'system', NOW(), NOW()),
|
||||||
|
('角色分配权限', 'system:role:assign', '/api/roles/*/permissions', 'POST', '为角色分配权限', 1, 'system', 'system', NOW(), NOW()),
|
||||||
|
('权限查看', 'system:permission:view', '/api/permissions', 'GET', '查看权限列表', 1, 'system', 'system', NOW(), NOW()),
|
||||||
|
('权限创建', 'system:permission:create', '/api/permissions', 'POST', '创建权限', 1, 'system', 'system', NOW(), NOW()),
|
||||||
|
('权限编辑', 'system:permission:edit', '/api/permissions', 'PUT', '编辑权限', 1, 'system', 'system', NOW(), NOW()),
|
||||||
|
('权限删除', 'system:permission:delete', '/api/permissions', 'DELETE', '删除权限', 1, 'system', 'system', NOW(), NOW()),
|
||||||
|
('菜单查看', 'system:menu:view', '/api/menus', 'GET', '查看菜单列表', 1, 'system', 'system', NOW(), NOW()),
|
||||||
|
('菜单创建', 'system:menu:create', '/api/menus', 'POST', '创建菜单', 1, 'system', 'system', NOW(), NOW()),
|
||||||
|
('菜单编辑', 'system:menu:edit', '/api/menus', 'PUT', '编辑菜单', 1, 'system', 'system', NOW(), NOW()),
|
||||||
|
('菜单删除', 'system:menu:delete', '/api/menus', 'DELETE', '删除菜单', 1, 'system', 'system', NOW(), NOW()),
|
||||||
|
('字典查看', 'system:dict:view', '/api/dict', 'GET', '查看字典列表', 1, 'system', 'system', NOW(), NOW()),
|
||||||
|
('字典创建', 'system:dict:create', '/api/dict', 'POST', '创建字典', 1, 'system', 'system', NOW(), NOW()),
|
||||||
|
('字典编辑', 'system:dict:edit', '/api/dict', 'PUT', '编辑字典', 1, 'system', 'system', NOW(), NOW()),
|
||||||
|
('字典删除', 'system:dict:delete', '/api/dict', 'DELETE', '删除字典', 1, 'system', 'system', NOW(), NOW()),
|
||||||
|
('配置查看', 'system:config:view', '/api/config', 'GET', '查看系统配置', 1, 'system', 'system', NOW(), NOW()),
|
||||||
|
('配置创建', 'system:config:create', '/api/config', 'POST', '创建系统配置', 1, 'system', 'system', NOW(), NOW()),
|
||||||
|
('配置编辑', 'system:config:edit', '/api/config', 'PUT', '编辑系统配置', 1, 'system', 'system', NOW(), NOW()),
|
||||||
|
('配置删除', 'system:config:delete', '/api/config', 'DELETE', '删除系统配置', 1, 'system', 'system', NOW(), NOW()),
|
||||||
|
('日志查看', 'system:log:view', '/api/logs', 'GET', '查看日志', 1, 'system', 'system', NOW(), NOW()),
|
||||||
|
('文件上传', 'system:file:upload', '/api/files/upload', 'POST', '上传文件', 1, 'system', 'system', NOW(), NOW()),
|
||||||
|
('文件下载', 'system:file:download', '/api/files/download', 'GET', '下载文件', 1, 'system', 'system', NOW(), NOW()),
|
||||||
|
('文件删除', 'system:file:delete', '/api/files', 'DELETE', '删除文件', 1, 'system', 'system', NOW(), NOW()),
|
||||||
|
('公告查看', 'system:notice:view', '/api/notices', 'GET', '查看公告', 1, 'system', 'system', NOW(), NOW()),
|
||||||
|
('公告创建', 'system:notice:create', '/api/notices', 'POST', '创建公告', 1, 'system', 'system', NOW(), NOW()),
|
||||||
|
('公告编辑', 'system:notice:edit', '/api/notices', 'PUT', '编辑公告', 1, 'system', 'system', NOW(), NOW()),
|
||||||
|
('公告删除', 'system:notice:delete', '/api/notices', 'DELETE', '删除公告', 1, 'system', 'system', NOW(), NOW());
|
||||||
|
|
||||||
|
-- 4.2 业务模块权限
|
||||||
|
INSERT INTO sys_permission (permission_name, permission_code, resource, action, description, status, create_by, update_by, created_at, updated_at) VALUES
|
||||||
|
-- 会员管理
|
||||||
|
('会员查看', 'business:member:view', '/api/admin/members', 'GET', '查看会员列表及详情', 1, 'system', 'system', NOW(), NOW()),
|
||||||
|
('会员编辑', 'business:member:edit', '/api/admin/member', 'PUT', '编辑会员信息', 1, 'system', 'system', NOW(), NOW()),
|
||||||
|
('会员手机修改', 'business:member:phone', '/api/admin/member/phone', 'POST', '管理员修改会员手机号', 1, 'system', 'system', NOW(), NOW()),
|
||||||
|
-- 会员卡管理
|
||||||
|
('会员卡查看', 'business:memberCard:view', '/api/member-cards', 'GET', '查看会员卡列表及详情', 1, 'system', 'system', NOW(), NOW()),
|
||||||
|
('会员卡创建', 'business:memberCard:create', '/api/member-cards', 'POST', '创建会员卡类型', 1, 'system', 'system', NOW(), NOW()),
|
||||||
|
('会员卡编辑', 'business:memberCard:edit', '/api/member-cards', 'PUT', '编辑会员卡类型', 1, 'system', 'system', NOW(), NOW()),
|
||||||
|
('会员卡删除', 'business:memberCard:delete', '/api/member-cards', 'DELETE', '删除会员卡类型', 1, 'system', 'system', NOW(), NOW()),
|
||||||
|
('会员卡购买', 'business:memberCard:purchase', '/api/member-card-records/purchase', 'POST', '为会员购买/绑定会员卡', 1, 'system', 'system', NOW(), NOW()),
|
||||||
|
('会员卡退卡', 'business:memberCard:refund', '/api/member-card-records/refund', 'POST', '会员卡退卡/解绑', 1, 'system', 'system', NOW(), NOW()),
|
||||||
|
-- 团课管理
|
||||||
|
('团课查看', 'business:groupCourse:view', '/api/groupCourse', 'GET', '查看团课列表及详情', 1, 'system', 'system', NOW(), NOW()),
|
||||||
|
('团课创建', 'business:groupCourse:create', '/api/groupCourse', 'POST', '创建团课', 1, 'system', 'system', NOW(), NOW()),
|
||||||
|
('团课编辑', 'business:groupCourse:edit', '/api/groupCourse', 'PUT', '编辑团课信息', 1, 'system', 'system', NOW(), NOW()),
|
||||||
|
('团课删除', 'business:groupCourse:delete', '/api/groupCourse', 'DELETE', '删除团课', 1, 'system', 'system', NOW(), NOW()),
|
||||||
|
('团课取消', 'business:groupCourse:cancel', '/api/groupCourse/cancel', 'POST', '取消团课', 1, 'system', 'system', NOW(), NOW()),
|
||||||
|
-- 团课类型管理
|
||||||
|
('团课类型查看', 'business:groupCourseType:view', '/api/groupCourse/types', 'GET', '查看团课类型列表', 1, 'system', 'system', NOW(), NOW()),
|
||||||
|
('团课类型创建', 'business:groupCourseType:create', '/api/groupCourse/types', 'POST', '创建团课类型', 1, 'system', 'system', NOW(), NOW()),
|
||||||
|
('团课类型编辑', 'business:groupCourseType:edit', '/api/groupCourse/types', 'PUT', '编辑团课类型', 1, 'system', 'system', NOW(), NOW()),
|
||||||
|
('团课类型删除', 'business:groupCourseType:delete', '/api/groupCourse/types', 'DELETE', '删除团课类型', 1, 'system', 'system', NOW(), NOW()),
|
||||||
|
-- 团课推荐管理
|
||||||
|
('团课推荐查看', 'business:groupCourseRecommend:view', '/api/groupCourse/recommend', 'GET', '查看推荐团课列表', 1, 'system', 'system', NOW(), NOW()),
|
||||||
|
('团课推荐创建', 'business:groupCourseRecommend:create','/api/groupCourse/recommend', 'POST', '创建推荐团课', 1, 'system', 'system', NOW(), NOW()),
|
||||||
|
('团课推荐编辑', 'business:groupCourseRecommend:edit', '/api/groupCourse/recommend', 'PUT', '编辑推荐团课', 1, 'system', 'system', NOW(), NOW()),
|
||||||
|
('团课推荐删除', 'business:groupCourseRecommend:delete','/api/groupCourse/recommend', 'DELETE', '删除推荐团课', 1, 'system', 'system', NOW(), NOW()),
|
||||||
|
-- 团课预约管理
|
||||||
|
('团课预约查看', 'business:groupCourseBooking:view', '/api/groupCourse/bookings', 'GET', '查看团课预约记录', 1, 'system', 'system', NOW(), NOW()),
|
||||||
|
('团课预约创建', 'business:groupCourseBooking:create', '/api/groupCourse/book', 'POST', '预约团课', 1, 'system', 'system', NOW(), NOW()),
|
||||||
|
('团课预约取消', 'business:groupCourseBooking:cancel', '/api/groupCourse/booking/cancel', 'POST', '取消团课预约', 1, 'system', 'system', NOW(), NOW()),
|
||||||
|
-- 签到管理
|
||||||
|
('签到查看', 'business:checkIn:view', '/api/checkIn/records', 'GET', '查看签到记录', 1, 'system', 'system', NOW(), NOW()),
|
||||||
|
('签到操作', 'business:checkIn:create', '/api/checkIn', 'POST', '执行签到操作', 1, 'system', 'system', NOW(), NOW()),
|
||||||
|
('签到导出', 'business:checkIn:export', '/api/checkIn/records/export', 'GET', '导出签到记录', 1, 'system', 'system', NOW(), NOW()),
|
||||||
|
-- 数据统计
|
||||||
|
('数据统计查看', 'business:dataCount:view', '/api/datacount', 'GET', '查看数据统计报表', 1, 'system', 'system', NOW(), NOW()),
|
||||||
|
('数据统计导出', 'business:dataCount:export', '/api/datacount/export', 'GET', '导出统计报表', 1, 'system', 'system', NOW(), NOW()),
|
||||||
|
-- 员工管理
|
||||||
|
('员工查看', 'business:employee:view', '/api/employees/page', 'GET', '查看员工列表', 1, 'system', 'system', NOW(), NOW()),
|
||||||
|
('员工创建', 'business:employee:create', '/api/users', 'POST', '创建员工账号', 1, 'system', 'system', NOW(), NOW()),
|
||||||
|
('员工编辑', 'business:employee:edit', '/api/users', 'PUT', '编辑员工信息', 1, 'system', 'system', NOW(), NOW()),
|
||||||
|
('员工删除', 'business:employee:delete', '/api/users', 'DELETE', '删除/禁用员工账号', 1, 'system', 'system', NOW(), NOW()),
|
||||||
|
('员工分配角色', 'business:employee:assignRole', '/api/users/roles', 'POST', '为员工分配角色', 1, 'system', 'system', NOW(), NOW());
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- 5. 为超级管理员角色分配所有权限
|
||||||
|
-- ============================================
|
||||||
|
INSERT INTO sys_role_permission (role_id, permission_id, create_by, update_by, created_at, updated_at)
|
||||||
|
SELECT 1, id, 'system', 'system', NOW(), NOW() FROM sys_permission WHERE status = 1
|
||||||
|
AND id NOT IN (SELECT permission_id FROM sys_role_permission WHERE role_id = 1);
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- 6. 菜单数据
|
||||||
|
-- ============================================
|
||||||
|
|
||||||
|
-- 6.1 一级菜单
|
||||||
|
INSERT INTO sys_menu (id, menu_name, parent_id, order_num, menu_type, perms, component, status, created_at, updated_at) VALUES
|
||||||
|
(1, '系统管理', 0, 1, 'M', NULL, NULL, 1, NOW(), NOW()),
|
||||||
|
(2, '审计日志', 0, 2, 'M', NULL, NULL, 1, NOW(), NOW()),
|
||||||
|
(3, '系统监控', 0, 3, 'M', NULL, NULL, 1, NOW(), NOW()),
|
||||||
|
(400, '团课管理', 0, 4, 'M', NULL, NULL, 1, NOW(), NOW()),
|
||||||
|
(500, '会员管理', 0, 5, 'M', NULL, NULL, 1, NOW(), NOW()),
|
||||||
|
(502, '会员卡管理', 0, 6, 'M', NULL, NULL, 1, NOW(), NOW()),
|
||||||
|
(504, '数据统计', 0, 7, 'M', NULL, NULL, 1, NOW(), NOW());
|
||||||
|
|
||||||
|
-- 6.2 系统管理 - 子菜单
|
||||||
|
INSERT INTO sys_menu (id, menu_name, parent_id, order_num, menu_type, perms, component, status, created_at, updated_at) VALUES
|
||||||
|
(11, '用户管理', 1, 1, 'C', 'system:user:list', 'system/user/index', 1, NOW(), NOW()),
|
||||||
|
(12, '角色管理', 1, 2, 'C', 'system:role:list', 'system/role/index', 1, NOW(), NOW()),
|
||||||
|
(13, '菜单管理', 1, 3, 'C', 'system:menu:list', 'system/menu/index', 1, NOW(), NOW()),
|
||||||
|
(14, '部门管理', 1, 4, 'C', 'system:dept:list', 'system/dept/index', 1, NOW(), NOW()),
|
||||||
|
(15, '字典管理', 1, 5, 'C', 'system:dict:list', 'system/dict/index', 1, NOW(), NOW()),
|
||||||
|
(16, '参数管理', 1, 6, 'C', 'system:config:list', 'system/config/index', 1, NOW(), NOW()),
|
||||||
|
(17, '通知公告', 1, 7, 'C', 'system:notice:list', 'system/notice/index', 1, NOW(), NOW()),
|
||||||
|
(18, '文件管理', 1, 8, 'C', 'system:file:list', 'system/file/index', 1, NOW(), NOW()),
|
||||||
|
(19, '轮播图管理', 0, 8, 'C', 'system:banner:list', 'banner/index', 1, NOW(), NOW());
|
||||||
|
|
||||||
|
-- 6.3 系统管理 - 按钮权限
|
||||||
|
INSERT INTO sys_menu (id, menu_name, parent_id, order_num, menu_type, perms, component, status, created_at, updated_at) VALUES
|
||||||
|
-- 用户管理按钮
|
||||||
|
(111, '用户查询', 11, 1, 'F', 'system:user:query', NULL, 1, NOW(), NOW()),
|
||||||
|
(112, '用户新增', 11, 2, 'F', 'system:user:add', NULL, 1, NOW(), NOW()),
|
||||||
|
(113, '用户修改', 11, 3, 'F', 'system:user:edit', NULL, 1, NOW(), NOW()),
|
||||||
|
(114, '用户删除', 11, 4, 'F', 'system:user:remove', NULL, 1, NOW(), NOW()),
|
||||||
|
(115, '用户导出', 11, 5, 'F', 'system:user:export', NULL, 1, NOW(), NOW()),
|
||||||
|
(116, '用户导入', 11, 6, 'F', 'system:user:import', NULL, 1, NOW(), NOW()),
|
||||||
|
(117, '重置密码', 11, 7, 'F', 'system:user:resetPwd', NULL, 1, NOW(), NOW()),
|
||||||
|
-- 角色管理按钮
|
||||||
|
(121, '角色查询', 12, 1, 'F', 'system:role:query', NULL, 1, NOW(), NOW()),
|
||||||
|
(122, '角色新增', 12, 2, 'F', 'system:role:add', NULL, 1, NOW(), NOW()),
|
||||||
|
(123, '角色修改', 12, 3, 'F', 'system:role:edit', NULL, 1, NOW(), NOW()),
|
||||||
|
(124, '角色删除', 12, 4, 'F', 'system:role:remove', NULL, 1, NOW(), NOW()),
|
||||||
|
(125, '角色导出', 12, 5, 'F', 'system:role:export', NULL, 1, NOW(), NOW()),
|
||||||
|
-- 菜单管理按钮
|
||||||
|
(131, '菜单查询', 13, 1, 'F', 'system:menu:query', NULL, 1, NOW(), NOW()),
|
||||||
|
(132, '菜单新增', 13, 2, 'F', 'system:menu:add', NULL, 1, NOW(), NOW()),
|
||||||
|
(133, '菜单修改', 13, 3, 'F', 'system:menu:edit', NULL, 1, NOW(), NOW()),
|
||||||
|
(134, '菜单删除', 13, 4, 'F', 'system:menu:remove', NULL, 1, NOW(), NOW());
|
||||||
|
|
||||||
|
-- 6.4 审计日志 - 子菜单
|
||||||
|
INSERT INTO sys_menu (id, menu_name, parent_id, order_num, menu_type, perms, component, status, created_at, updated_at) VALUES
|
||||||
|
(21, '操作日志', 2, 1, 'C', 'audit:operation:list', 'audit/operation/index', 1, NOW(), NOW()),
|
||||||
|
(22, '登录日志', 2, 2, 'C', 'audit:login:list', 'audit/login/index', 1, NOW(), NOW()),
|
||||||
|
(23, '异常日志', 2, 3, 'C', 'audit:exception:list', 'audit/exception/index', 1, NOW(), NOW());
|
||||||
|
|
||||||
|
-- 6.5 审计日志 - 按钮权限
|
||||||
|
INSERT INTO sys_menu (id, menu_name, parent_id, order_num, menu_type, perms, component, status, created_at, updated_at) VALUES
|
||||||
|
(211, '操作查询', 21, 1, 'F', 'audit:operation:query', NULL, 1, NOW(), NOW()),
|
||||||
|
(212, '操作删除', 21, 2, 'F', 'audit:operation:remove', NULL, 1, NOW(), NOW()),
|
||||||
|
(213, '操作导出', 21, 3, 'F', 'audit:operation:export', NULL, 1, NOW(), NOW()),
|
||||||
|
(221, '登录查询', 22, 1, 'F', 'audit:login:query', NULL, 1, NOW(), NOW()),
|
||||||
|
(222, '登录删除', 22, 2, 'F', 'audit:login:remove', NULL, 1, NOW(), NOW()),
|
||||||
|
(223, '登录导出', 22, 3, 'F', 'audit:login:export', NULL, 1, NOW(), NOW()),
|
||||||
|
(231, '异常查询', 23, 1, 'F', 'audit:exception:query', NULL, 1, NOW(), NOW()),
|
||||||
|
(232, '异常删除', 23, 2, 'F', 'audit:exception:remove', NULL, 1, NOW(), NOW()),
|
||||||
|
(233, '异常导出', 23, 3, 'F', 'audit:exception:export', NULL, 1, NOW(), NOW());
|
||||||
|
|
||||||
|
-- 6.6 系统监控 - 子菜单
|
||||||
|
INSERT INTO sys_menu (id, menu_name, parent_id, order_num, menu_type, perms, component, status, created_at, updated_at) VALUES
|
||||||
|
(31, '在线用户', 3, 1, 'C', 'monitor:online:list', 'monitor/online/index', 1, NOW(), NOW()),
|
||||||
|
(32, '定时任务', 3, 2, 'C', 'monitor:job:list', 'monitor/job/index', 1, NOW(), NOW()),
|
||||||
|
(33, '数据监控', 3, 3, 'C', 'monitor:data:list', 'monitor/data/index', 1, NOW(), NOW()),
|
||||||
|
(34, '服务监控', 3, 4, 'C', 'monitor:server:list', 'monitor/server/index', 1, NOW(), NOW()),
|
||||||
|
(35, '缓存监控', 3, 5, 'C', 'monitor:cache:list', 'monitor/cache/index', 1, NOW(), NOW());
|
||||||
|
|
||||||
|
-- 6.7 系统监控 - 按钮权限
|
||||||
|
INSERT INTO sys_menu (id, menu_name, parent_id, order_num, menu_type, perms, component, status, created_at, updated_at) VALUES
|
||||||
|
(311, '在线查询', 31, 1, 'F', 'monitor:online:query', NULL, 1, NOW(), NOW()),
|
||||||
|
(312, '在线强退', 31, 2, 'F', 'monitor:online:forceLogout', NULL, 1, NOW(), NOW()),
|
||||||
|
(321, '任务查询', 32, 1, 'F', 'monitor:job:query', NULL, 1, NOW(), NOW()),
|
||||||
|
(322, '任务新增', 32, 2, 'F', 'monitor:job:add', NULL, 1, NOW(), NOW()),
|
||||||
|
(323, '任务修改', 32, 3, 'F', 'monitor:job:edit', NULL, 1, NOW(), NOW()),
|
||||||
|
(324, '任务删除', 32, 4, 'F', 'monitor:job:remove', NULL, 1, NOW(), NOW()),
|
||||||
|
(325, '任务执行', 32, 5, 'F', 'monitor:job:execute', NULL, 1, NOW(), NOW());
|
||||||
|
|
||||||
|
-- 6.8 团课管理 - 子菜单
|
||||||
|
INSERT INTO sys_menu (id, menu_name, parent_id, order_num, menu_type, perms, component, status, created_at, updated_at) VALUES
|
||||||
|
(401, '团课管理', 400, 1, 'C', 'business:groupCourse:list', 'gymCourse/course/index', 1, NOW(), NOW()),
|
||||||
|
(402, '团课类型', 400, 2, 'C', 'business:groupCourseType:list', 'gymCourse/type/index', 1, NOW(), NOW()),
|
||||||
|
(403, '类型标签', 400, 3, 'C', 'business:groupCourseLabel:list', 'gymCourse/label/index', 1, NOW(), NOW()),
|
||||||
|
(404, '团课推荐', 400, 4, 'C', 'business:groupCourseRecommend:list', 'gymCourse/recommend/index', 1, NOW(), NOW());
|
||||||
|
|
||||||
|
-- 6.9 团课管理 - 按钮权限
|
||||||
|
INSERT INTO sys_menu (id, menu_name, parent_id, order_num, menu_type, perms, component, status, created_at, updated_at) VALUES
|
||||||
|
(4011, '团课查询', 401, 1, 'F', 'business:groupCourse:query', NULL, 1, NOW(), NOW()),
|
||||||
|
(4012, '团课新增', 401, 2, 'F', 'business:groupCourse:add', NULL, 1, NOW(), NOW()),
|
||||||
|
(4013, '团课修改', 401, 3, 'F', 'business:groupCourse:edit', NULL, 1, NOW(), NOW()),
|
||||||
|
(4014, '团课删除', 401, 4, 'F', 'business:groupCourse:remove', NULL, 1, NOW(), NOW()),
|
||||||
|
(4015, '团课取消', 401, 5, 'F', 'business:groupCourse:cancel', NULL, 1, NOW(), NOW()),
|
||||||
|
(4021, '类型查询', 402, 1, 'F', 'business:groupCourseType:query', NULL, 1, NOW(), NOW()),
|
||||||
|
(4022, '类型新增', 402, 2, 'F', 'business:groupCourseType:add', NULL, 1, NOW(), NOW()),
|
||||||
|
(4023, '类型修改', 402, 3, 'F', 'business:groupCourseType:edit', NULL, 1, NOW(), NOW()),
|
||||||
|
(4024, '类型删除', 402, 4, 'F', 'business:groupCourseType:remove', NULL, 1, NOW(), NOW()),
|
||||||
|
(4031, '标签查询', 403, 1, 'F', 'business:groupCourseLabel:query', NULL, 1, NOW(), NOW()),
|
||||||
|
(4032, '标签新增', 403, 2, 'F', 'business:groupCourseLabel:add', NULL, 1, NOW(), NOW()),
|
||||||
|
(4033, '标签修改', 403, 3, 'F', 'business:groupCourseLabel:edit', NULL, 1, NOW(), NOW()),
|
||||||
|
(4034, '标签删除', 403, 4, 'F', 'business:groupCourseLabel:remove', NULL, 1, NOW(), NOW()),
|
||||||
|
(4041, '推荐查询', 404, 1, 'F', 'business:groupCourseRecommend:query', NULL, 1, NOW(), NOW()),
|
||||||
|
(4042, '推荐新增', 404, 2, 'F', 'business:groupCourseRecommend:add', NULL, 1, NOW(), NOW()),
|
||||||
|
(4043, '推荐修改', 404, 3, 'F', 'business:groupCourseRecommend:edit', NULL, 1, NOW(), NOW()),
|
||||||
|
(4044, '推荐删除', 404, 4, 'F', 'business:groupCourseRecommend:remove', NULL, 1, NOW(), NOW());
|
||||||
|
|
||||||
|
-- 6.10 会员管理 - 子菜单
|
||||||
|
INSERT INTO sys_menu (id, menu_name, parent_id, order_num, menu_type, perms, component, status, created_at, updated_at) VALUES
|
||||||
|
(501, '会员管理', 500, 1, 'C', 'business:member:list', 'member/member/index', 1, NOW(), NOW()),
|
||||||
|
(503, '会员卡管理', 502, 1, 'C', 'business:memberCard:list', 'member/card/index', 1, NOW(), NOW()),
|
||||||
|
(505, '数据统计看板', 504, 1, 'C', 'business:statistics:view', 'statistics/dashboard/index', 1, NOW(), NOW());
|
||||||
|
|
||||||
|
-- 6.11 会员管理 - 按钮权限
|
||||||
|
INSERT INTO sys_menu (id, menu_name, parent_id, order_num, menu_type, perms, component, status, created_at, updated_at) VALUES
|
||||||
|
(5011, '会员查询', 501, 1, 'F', 'business:member:query', NULL, 1, NOW(), NOW()),
|
||||||
|
(5012, '会员新增', 501, 2, 'F', 'business:member:add', NULL, 1, NOW(), NOW()),
|
||||||
|
(5013, '会员修改', 501, 3, 'F', 'business:member:edit', NULL, 1, NOW(), NOW()),
|
||||||
|
(5014, '会员删除', 501, 4, 'F', 'business:member:remove', NULL, 1, NOW(), NOW()),
|
||||||
|
(5015, '会员详情', 501, 5, 'F', 'business:member:detail', NULL, 1, NOW(), NOW()),
|
||||||
|
(5031, '会员卡查询', 503, 1, 'F', 'business:memberCard:query', NULL, 1, NOW(), NOW()),
|
||||||
|
(5032, '会员卡新增', 503, 2, 'F', 'business:memberCard:add', NULL, 1, NOW(), NOW()),
|
||||||
|
(5033, '会员卡修改', 503, 3, 'F', 'business:memberCard:edit', NULL, 1, NOW(), NOW()),
|
||||||
|
(5034, '会员卡删除', 503, 4, 'F', 'business:memberCard:remove', NULL, 1, NOW(), NOW()),
|
||||||
|
(5051, '数据统计导出', 505, 1, 'F', 'business:statistics:export', NULL, 1, NOW(), NOW());
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- 7. 字典数据
|
||||||
|
-- ============================================
|
||||||
|
|
||||||
|
-- 7.1 字典类型
|
||||||
|
INSERT INTO sys_dict_type (dict_name, dict_type, status, remark, create_by, update_by, created_at, updated_at)
|
||||||
|
VALUES
|
||||||
|
('用户状态', 'user_status', '0', '用户状态列表', 'system', 'system', NOW(), NOW()),
|
||||||
|
('菜单状态', 'menu_status', '0', '菜单状态列表', 'system', 'system', NOW(), NOW()),
|
||||||
|
('角色状态', 'role_status', '0', '角色状态列表', 'system', 'system', NOW(), NOW()),
|
||||||
|
('系统开关', 'sys_normal_disable', '0', '系统开关列表', 'system', 'system', NOW(), NOW())
|
||||||
|
ON CONFLICT (dict_type) DO NOTHING;
|
||||||
|
|
||||||
|
-- 7.2 字典数据
|
||||||
|
INSERT INTO sys_dict_data (dict_sort, dict_label, dict_value, dict_type, css_class, list_class, is_default, status, create_by, update_by, created_at, updated_at)
|
||||||
|
VALUES
|
||||||
|
(1, '正常', '1', 'user_status', '', 'primary', 'Y', '0', 'system', 'system', NOW(), NOW()),
|
||||||
|
(2, '停用', '0', 'user_status', '', 'danger', 'N', '0', 'system', 'system', NOW(), NOW()),
|
||||||
|
(1, '正常', '0', 'menu_status', '', 'primary', 'Y', '0', 'system', 'system', NOW(), NOW()),
|
||||||
|
(2, '停用', '1', 'menu_status', '', 'danger', 'N', '0', 'system', 'system', NOW(), NOW()),
|
||||||
|
(1, '正常', '0', 'role_status', '', 'primary', 'Y', '0', 'system', 'system', NOW(), NOW()),
|
||||||
|
(2, '停用', '1', 'role_status', '', 'danger', 'N', '0', 'system', 'system', NOW(), NOW()),
|
||||||
|
(1, '正常', '0', 'sys_normal_disable', '', 'primary', 'Y', '0', 'system', 'system', NOW(), NOW()),
|
||||||
|
(2, '停用', '1', 'sys_normal_disable', '', 'danger', 'N', '0', 'system', 'system', NOW(), NOW());
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- 8. 系统配置
|
||||||
|
-- ============================================
|
||||||
|
INSERT INTO sys_config (config_name, config_key, config_value, config_type, create_by, update_by, created_at, updated_at)
|
||||||
|
VALUES
|
||||||
|
('用户管理-用户初始密码', 'sys.user.initPassword', '123456', 'Y', 'system', 'system', NOW(), NOW()),
|
||||||
|
('主框架页-默认皮肤样式名称', 'sys.index.skinName', 'skin-blue','Y', 'system', 'system', NOW(), NOW()),
|
||||||
|
('用户自助-验证码开关', 'sys.account.captchaEnabled', 'true', 'Y', 'system', 'system', NOW(), NOW()),
|
||||||
|
('用户自助-是否开启用户注册功能', 'sys.account.registerUser', 'false', 'Y', 'system', 'system', NOW(), NOW()),
|
||||||
|
('账号自助-密码验证码', 'sys.account.pwdCaptchaEnabled','true', 'Y', 'system', 'system', NOW(), NOW()),
|
||||||
|
('团课-取消预约免手续费次数', 'group_course.cancel_free_limit','3', 'Y', 'system', 'system', NOW(), NOW())
|
||||||
|
ON CONFLICT (config_key) DO NOTHING;
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- 9. 重置所有序列值
|
||||||
|
-- ============================================
|
||||||
|
SELECT setval('sys_user_id_seq', (SELECT COALESCE(MAX(id), 1) FROM sys_user));
|
||||||
|
SELECT setval('sys_role_id_seq', (SELECT COALESCE(MAX(id), 1) FROM sys_role));
|
||||||
|
SELECT setval('user_role_id_seq', (SELECT COALESCE(MAX(id), 1) FROM user_role));
|
||||||
|
SELECT setval('sys_permission_id_seq', (SELECT COALESCE(MAX(id), 1) FROM sys_permission));
|
||||||
|
SELECT setval('sys_role_permission_id_seq', (SELECT COALESCE(MAX(id), 1) FROM sys_role_permission));
|
||||||
|
SELECT setval('sys_menu_id_seq', (SELECT COALESCE(MAX(id), 1) FROM sys_menu));
|
||||||
|
SELECT setval('sys_dict_type_id_seq', (SELECT COALESCE(MAX(id), 1) FROM sys_dict_type));
|
||||||
|
SELECT setval('sys_dict_data_id_seq', (SELECT COALESCE(MAX(id), 1) FROM sys_dict_data));
|
||||||
|
SELECT setval('sys_config_id_seq', (SELECT COALESCE(MAX(id), 1) FROM sys_config));
|
||||||
+25
-14
@@ -1,6 +1,8 @@
|
|||||||
|
-- ============================================
|
||||||
-- Novalon管理系统数据库初始化脚本
|
-- Novalon管理系统数据库初始化脚本
|
||||||
-- 版本: V1
|
-- 版本: V1
|
||||||
-- 描述: 创建所有核心表结构(合并版)
|
-- 描述: 创建所有核心系统表结构
|
||||||
|
-- ============================================
|
||||||
|
|
||||||
-- ============================================
|
-- ============================================
|
||||||
-- 用户与角色相关表
|
-- 用户与角色相关表
|
||||||
@@ -140,7 +142,7 @@ CREATE TABLE IF NOT EXISTS sys_dict_data (
|
|||||||
deleted_at TIMESTAMP
|
deleted_at TIMESTAMP
|
||||||
);
|
);
|
||||||
|
|
||||||
-- 字典表(通用字典)
|
-- 通用字典表
|
||||||
CREATE TABLE IF NOT EXISTS sys_dictionary (
|
CREATE TABLE IF NOT EXISTS sys_dictionary (
|
||||||
id BIGSERIAL PRIMARY KEY,
|
id BIGSERIAL PRIMARY KEY,
|
||||||
type VARCHAR(100) NOT NULL,
|
type VARCHAR(100) NOT NULL,
|
||||||
@@ -165,7 +167,7 @@ CREATE TABLE IF NOT EXISTS sys_config (
|
|||||||
config_name VARCHAR(100) NOT NULL,
|
config_name VARCHAR(100) NOT NULL,
|
||||||
config_key VARCHAR(100) NOT NULL UNIQUE,
|
config_key VARCHAR(100) NOT NULL UNIQUE,
|
||||||
config_value VARCHAR(500) NOT NULL,
|
config_value VARCHAR(500) NOT NULL,
|
||||||
config_type VARCHAR(1) DEFAULT 'N',
|
config_type VARCHAR(1) DEFAULT 'Y',
|
||||||
create_by VARCHAR(50),
|
create_by VARCHAR(50),
|
||||||
update_by VARCHAR(50),
|
update_by VARCHAR(50),
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
@@ -355,15 +357,6 @@ COMMENT ON TABLE sys_dict_data IS '字典数据表';
|
|||||||
COMMENT ON TABLE sys_dictionary IS '通用字典表';
|
COMMENT ON TABLE sys_dictionary IS '通用字典表';
|
||||||
COMMENT ON TABLE sys_config IS '系统配置表';
|
COMMENT ON TABLE sys_config IS '系统配置表';
|
||||||
COMMENT ON TABLE sys_login_log IS '登录日志表';
|
COMMENT ON TABLE sys_login_log IS '登录日志表';
|
||||||
COMMENT ON TABLE sys_exception_log IS '异常日志表';
|
|
||||||
COMMENT ON TABLE operation_log IS '操作日志表';
|
|
||||||
COMMENT ON TABLE audit_log IS '审计日志表';
|
|
||||||
COMMENT ON TABLE audit_log_archive IS '审计日志归档表';
|
|
||||||
COMMENT ON TABLE sys_notice IS '系统公告表';
|
|
||||||
COMMENT ON TABLE sys_user_message IS '用户消息表';
|
|
||||||
COMMENT ON TABLE sys_file IS '文件管理表';
|
|
||||||
COMMENT ON TABLE oauth2_client IS 'OAuth2客户端表';
|
|
||||||
|
|
||||||
COMMENT ON TABLE sys_exception_log IS '异常日志表';
|
COMMENT ON TABLE sys_exception_log IS '异常日志表';
|
||||||
COMMENT ON COLUMN sys_exception_log.id IS '主键ID';
|
COMMENT ON COLUMN sys_exception_log.id IS '主键ID';
|
||||||
COMMENT ON COLUMN sys_exception_log.username IS '操作用户';
|
COMMENT ON COLUMN sys_exception_log.username IS '操作用户';
|
||||||
@@ -375,7 +368,22 @@ COMMENT ON COLUMN sys_exception_log.exception_msg IS '异常消息';
|
|||||||
COMMENT ON COLUMN sys_exception_log.exception_stack IS '异常堆栈';
|
COMMENT ON COLUMN sys_exception_log.exception_stack IS '异常堆栈';
|
||||||
COMMENT ON COLUMN sys_exception_log.ip IS 'IP地址';
|
COMMENT ON COLUMN sys_exception_log.ip IS 'IP地址';
|
||||||
COMMENT ON COLUMN sys_exception_log.create_time IS '创建时间';
|
COMMENT ON COLUMN sys_exception_log.create_time IS '创建时间';
|
||||||
|
COMMENT ON TABLE operation_log IS '操作日志表';
|
||||||
|
COMMENT ON COLUMN operation_log.id IS '主键ID';
|
||||||
|
COMMENT ON COLUMN operation_log.username IS '操作用户';
|
||||||
|
COMMENT ON COLUMN operation_log.operation IS '操作描述';
|
||||||
|
COMMENT ON COLUMN operation_log.method IS '请求方法';
|
||||||
|
COMMENT ON COLUMN operation_log.params IS '请求参数';
|
||||||
|
COMMENT ON COLUMN operation_log.result IS '操作结果';
|
||||||
|
COMMENT ON COLUMN operation_log.ip IS 'IP地址';
|
||||||
|
COMMENT ON COLUMN operation_log.duration IS '执行时长(毫秒)';
|
||||||
|
COMMENT ON COLUMN operation_log.status IS '操作状态(0成功 1失败)';
|
||||||
|
COMMENT ON COLUMN operation_log.error_msg IS '错误消息';
|
||||||
|
COMMENT ON COLUMN operation_log.create_by IS '创建人';
|
||||||
|
COMMENT ON COLUMN operation_log.update_by IS '更新人';
|
||||||
|
COMMENT ON COLUMN operation_log.created_at IS '创建时间';
|
||||||
|
COMMENT ON COLUMN operation_log.updated_at IS '更新时间';
|
||||||
|
COMMENT ON COLUMN operation_log.deleted_at IS '删除时间';
|
||||||
COMMENT ON TABLE audit_log IS '审计日志表';
|
COMMENT ON TABLE audit_log IS '审计日志表';
|
||||||
COMMENT ON COLUMN audit_log.id IS '主键ID';
|
COMMENT ON COLUMN audit_log.id IS '主键ID';
|
||||||
COMMENT ON COLUMN audit_log.entity_type IS '实体类型(如User, Role等)';
|
COMMENT ON COLUMN audit_log.entity_type IS '实体类型(如User, Role等)';
|
||||||
@@ -389,7 +397,6 @@ COMMENT ON COLUMN audit_log.changed_fields IS '变更字段列表';
|
|||||||
COMMENT ON COLUMN audit_log.ip_address IS 'IP地址';
|
COMMENT ON COLUMN audit_log.ip_address IS 'IP地址';
|
||||||
COMMENT ON COLUMN audit_log.description IS '操作描述';
|
COMMENT ON COLUMN audit_log.description IS '操作描述';
|
||||||
COMMENT ON COLUMN audit_log.created_at IS '记录创建时间';
|
COMMENT ON COLUMN audit_log.created_at IS '记录创建时间';
|
||||||
|
|
||||||
COMMENT ON TABLE audit_log_archive IS '审计日志归档表';
|
COMMENT ON TABLE audit_log_archive IS '审计日志归档表';
|
||||||
COMMENT ON COLUMN audit_log_archive.id IS '主键ID';
|
COMMENT ON COLUMN audit_log_archive.id IS '主键ID';
|
||||||
COMMENT ON COLUMN audit_log_archive.entity_type IS '实体类型(如User, Role等)';
|
COMMENT ON COLUMN audit_log_archive.entity_type IS '实体类型(如User, Role等)';
|
||||||
@@ -405,3 +412,7 @@ COMMENT ON COLUMN audit_log_archive.user_agent IS '用户代理';
|
|||||||
COMMENT ON COLUMN audit_log_archive.description IS '操作描述';
|
COMMENT ON COLUMN audit_log_archive.description IS '操作描述';
|
||||||
COMMENT ON COLUMN audit_log_archive.created_at IS '记录创建时间';
|
COMMENT ON COLUMN audit_log_archive.created_at IS '记录创建时间';
|
||||||
COMMENT ON COLUMN audit_log_archive.archived_at IS '归档时间';
|
COMMENT ON COLUMN audit_log_archive.archived_at IS '归档时间';
|
||||||
|
COMMENT ON TABLE sys_notice IS '系统公告表';
|
||||||
|
COMMENT ON TABLE sys_user_message IS '用户消息表';
|
||||||
|
COMMENT ON TABLE sys_file IS '文件管理表';
|
||||||
|
COMMENT ON TABLE oauth2_client IS 'OAuth2客户端表';
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
-- ============================================
|
||||||
|
-- V20: 新增轮播图管理菜单按钮权限
|
||||||
|
-- 注意: sys_menu(id=19, 轮播图管理) 已在 V19 中创建
|
||||||
|
-- ============================================
|
||||||
|
|
||||||
|
-- 轮播图管理按钮权限
|
||||||
|
INSERT INTO sys_menu (id, menu_name, parent_id, order_num, menu_type, perms, component, status, created_at, updated_at) VALUES
|
||||||
|
(191, '轮播图查询', 19, 1, 'F', 'system:banner:query', NULL, 1, NOW(), NOW()),
|
||||||
|
(192, '轮播图新增', 19, 2, 'F', 'system:banner:add', NULL, 1, NOW(), NOW()),
|
||||||
|
(193, '轮播图修改', 19, 3, 'F', 'system:banner:edit', NULL, 1, NOW(), NOW()),
|
||||||
|
(194, '轮播图删除', 19, 4, 'F', 'system:banner:remove', NULL, 1, NOW(), NOW())
|
||||||
|
ON CONFLICT (id) DO NOTHING;
|
||||||
|
|
||||||
|
-- 重置菜单序列
|
||||||
|
SELECT setval('sys_menu_id_seq', (SELECT COALESCE(MAX(id), 1) FROM sys_menu));
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
-- ============================================
|
||||||
|
-- V21: 创建轮播图表并更新菜单为顶级菜单
|
||||||
|
-- ============================================
|
||||||
|
|
||||||
|
-- 创建 banner 表
|
||||||
|
CREATE TABLE IF NOT EXISTS banner (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
image_url VARCHAR(512) NOT NULL,
|
||||||
|
title VARCHAR(100) NOT NULL,
|
||||||
|
subtitle TEXT,
|
||||||
|
sort_order INT DEFAULT 0,
|
||||||
|
is_active VARCHAR(1) DEFAULT '1',
|
||||||
|
link_url VARCHAR(512),
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
deleted_at TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 将轮播图管理菜单从系统管理子菜单调整为顶级菜单(parent_id=0, sort=8)
|
||||||
|
UPDATE sys_menu SET parent_id = 0, order_num = 8 WHERE id = 19;
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
-- ============================================
|
||||||
|
-- V22: 教练管理 - 角色/权限/菜单
|
||||||
|
-- ============================================
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- 1. 教练角色
|
||||||
|
-- ============================================
|
||||||
|
INSERT INTO sys_role (id, role_name, role_key, role_sort, status, create_by, update_by, created_at, updated_at)
|
||||||
|
VALUES (5, '教练', 'coach', 5, 1, 'system', 'system', NOW(), NOW())
|
||||||
|
ON CONFLICT (id) DO NOTHING;
|
||||||
|
|
||||||
|
SELECT setval('sys_role_id_seq', COALESCE((SELECT MAX(id) FROM sys_role), 1));
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- 2. 教练管理权限
|
||||||
|
-- ============================================
|
||||||
|
INSERT INTO sys_permission (permission_name, permission_code, resource, action, description, status, create_by, update_by, created_at, updated_at) VALUES
|
||||||
|
('教练查看', 'system:coach:view', '/api/coach', 'GET', '查看教练列表', 1, 'system', 'system', NOW(), NOW()),
|
||||||
|
('教练创建', 'system:coach:create', '/api/coach', 'POST', '创建教练', 1, 'system', 'system', NOW(), NOW()),
|
||||||
|
('教练编辑', 'system:coach:edit', '/api/coach', 'PUT', '编辑教练信息', 1, 'system', 'system', NOW(), NOW()),
|
||||||
|
('教练删除', 'system:coach:delete', '/api/coach', 'DELETE', '删除(禁用)教练', 1, 'system', 'system', NOW(), NOW());
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- 3. 为超级管理员角色分配教练权限
|
||||||
|
-- ============================================
|
||||||
|
INSERT INTO sys_role_permission (role_id, permission_id, create_by, update_by, created_at, updated_at)
|
||||||
|
SELECT 1, id, 'system', 'system', NOW(), NOW() FROM sys_permission
|
||||||
|
WHERE permission_code LIKE 'system:coach:%'
|
||||||
|
AND id NOT IN (SELECT permission_id FROM sys_role_permission WHERE role_id = 1);
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- 4. 教练管理菜单(顶级菜单)
|
||||||
|
-- ============================================
|
||||||
|
INSERT INTO sys_menu (id, menu_name, parent_id, order_num, menu_type, perms, component, status, created_at, updated_at) VALUES
|
||||||
|
(20, '教练管理', 0, 9, 'C', 'system:coach:list', 'system/coach/index', 1, NOW(), NOW());
|
||||||
|
|
||||||
|
-- 教练管理按钮权限
|
||||||
|
INSERT INTO sys_menu (id, menu_name, parent_id, order_num, menu_type, perms, component, status, created_at, updated_at) VALUES
|
||||||
|
(201, '教练查询', 20, 1, 'F', 'system:coach:query', NULL, 1, NOW(), NOW()),
|
||||||
|
(202, '教练新增', 20, 2, 'F', 'system:coach:add', NULL, 1, NOW(), NOW()),
|
||||||
|
(203, '教练修改', 20, 3, 'F', 'system:coach:edit', NULL, 1, NOW(), NOW()),
|
||||||
|
(204, '教练删除', 20, 4, 'F', 'system:coach:remove', NULL, 1, NOW(), NOW());
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- 5. 重置序列
|
||||||
|
-- ============================================
|
||||||
|
SELECT setval('sys_menu_id_seq', (SELECT COALESCE(MAX(id), 1) FROM sys_menu));
|
||||||
|
SELECT setval('sys_permission_id_seq', (SELECT COALESCE(MAX(id), 1) FROM sys_permission));
|
||||||
|
SELECT setval('sys_role_permission_id_seq',(SELECT COALESCE(MAX(id), 1) FROM sys_role_permission));
|
||||||
+30
@@ -0,0 +1,30 @@
|
|||||||
|
-- ============================================
|
||||||
|
-- 教练违规记录表
|
||||||
|
-- 版本: V23
|
||||||
|
-- 描述: 创建教练违规记录表
|
||||||
|
-- ============================================
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS coach_violation (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
coach_id BIGINT NOT NULL,
|
||||||
|
course_id BIGINT NOT NULL,
|
||||||
|
violation_time TIMESTAMP NOT NULL,
|
||||||
|
violation_reason VARCHAR(50) NOT NULL,
|
||||||
|
create_by VARCHAR(50),
|
||||||
|
update_by VARCHAR(50),
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
deleted_at TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
COMMENT ON TABLE coach_violation IS '教练违规记录表';
|
||||||
|
COMMENT ON COLUMN coach_violation.id IS '主键ID';
|
||||||
|
COMMENT ON COLUMN coach_violation.coach_id IS '教练ID(关联sys_user)';
|
||||||
|
COMMENT ON COLUMN coach_violation.course_id IS '团课ID(关联group_course)';
|
||||||
|
COMMENT ON COLUMN coach_violation.violation_time IS '违规时间';
|
||||||
|
COMMENT ON COLUMN coach_violation.violation_reason IS '违规原因(COACH_LATE-教练迟到, COACH_ABSENT-教练缺席, NOT_MANUAL_END-教练未手动结课)';
|
||||||
|
COMMENT ON COLUMN coach_violation.create_by IS '创建人';
|
||||||
|
COMMENT ON COLUMN coach_violation.update_by IS '更新人';
|
||||||
|
COMMENT ON COLUMN coach_violation.created_at IS '创建时间';
|
||||||
|
COMMENT ON COLUMN coach_violation.updated_at IS '更新时间';
|
||||||
|
COMMENT ON COLUMN coach_violation.deleted_at IS '删除时间(软删除)';
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
-- ============================================
|
||||||
|
-- V24: 教练测试数据
|
||||||
|
-- 描述: 创建5个教练用户并分配团课,每教练负责一种课型
|
||||||
|
-- 确保同教练的课程时间不冲突
|
||||||
|
-- ============================================
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- 1. 教练用户数据(密码均为 Test@123)
|
||||||
|
-- ============================================
|
||||||
|
INSERT INTO sys_user (id, username, password, email, phone, nickname, status, create_by, update_by, created_at, updated_at)
|
||||||
|
VALUES
|
||||||
|
(11, 'coach_zhang', '$2a$12$nZ1EMUpZQljbnEdIKzH72eHlDJKUmHmHppnTTVth/SlHs5VpSAr8C', 'coach_zhang@novalon.com', '13800138011', '张教练(瑜伽)', 1, 'system', 'system', NOW(), NOW()),
|
||||||
|
(12, 'coach_li', '$2a$12$nZ1EMUpZQljbnEdIKzH72eHlDJKUmHmHppnTTVth/SlHs5VpSAr8C', 'coach_li@novalon.com', '13800138012', '李教练(动感单车)', 1, 'system', 'system', NOW(), NOW()),
|
||||||
|
(13, 'coach_wang', '$2a$12$nZ1EMUpZQljbnEdIKzH72eHlDJKUmHmHppnTTVth/SlHs5VpSAr8C', 'coach_wang@novalon.com', '13800138013', '王教练(HIIT)', 1, 'system', 'system', NOW(), NOW()),
|
||||||
|
(14, 'coach_zhao', '$2a$12$nZ1EMUpZQljbnEdIKzH72eHlDJKUmHmHppnTTVth/SlHs5VpSAr8C', 'coach_zhao@novalon.com', '13800138014', '赵教练(普拉提)', 1, 'system', 'system', NOW(), NOW()),
|
||||||
|
(15, 'coach_chen', '$2a$12$nZ1EMUpZQljbnEdIKzH72eHlDJKUmHmHppnTTVth/SlHs5VpSAr8C', 'coach_chen@novalon.com', '13800138015', '陈教练(搏击操)', 1, 'system', 'system', NOW(), NOW())
|
||||||
|
ON CONFLICT (username) DO UPDATE SET
|
||||||
|
password = EXCLUDED.password,
|
||||||
|
status = EXCLUDED.status;
|
||||||
|
|
||||||
|
SELECT setval('sys_user_id_seq', 15);
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- 2. 分配教练角色(role_id=5)
|
||||||
|
-- ============================================
|
||||||
|
INSERT INTO user_role (user_id, role_id, created_by, created_at)
|
||||||
|
VALUES
|
||||||
|
(11, 5, 'system', NOW()),
|
||||||
|
(12, 5, 'system', NOW()),
|
||||||
|
(13, 5, 'system', NOW()),
|
||||||
|
(14, 5, 'system', NOW()),
|
||||||
|
(15, 5, 'system', NOW())
|
||||||
|
ON CONFLICT (user_id, role_id) DO NOTHING;
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- 3. 为团课分配教练(按课型,无时间冲突)
|
||||||
|
-- coach_id=11 张教练: 瑜伽 5门
|
||||||
|
-- coach_id=12 李教练: 动感单车 5门
|
||||||
|
-- coach_id=13 王教练: HIIT训练 5门
|
||||||
|
-- coach_id=14 赵教练: 普拉提 5门
|
||||||
|
-- coach_id=15 陈教练: 搏击操 5门
|
||||||
|
-- ============================================
|
||||||
|
|
||||||
|
-- 瑜伽课程 → 张教练(11)
|
||||||
|
UPDATE group_course SET coach_id = 11, updated_at = NOW()
|
||||||
|
WHERE course_type = (SELECT id FROM group_course_type WHERE type_name = '瑜伽')
|
||||||
|
AND deleted_at IS NULL;
|
||||||
|
|
||||||
|
-- 动感单车课程 → 李教练(12)
|
||||||
|
UPDATE group_course SET coach_id = 12, updated_at = NOW()
|
||||||
|
WHERE course_type = (SELECT id FROM group_course_type WHERE type_name = '动感单车')
|
||||||
|
AND deleted_at IS NULL;
|
||||||
|
|
||||||
|
-- HIIT训练课程 → 王教练(13)
|
||||||
|
UPDATE group_course SET coach_id = 13, updated_at = NOW()
|
||||||
|
WHERE course_type = (SELECT id FROM group_course_type WHERE type_name = 'HIIT训练')
|
||||||
|
AND deleted_at IS NULL;
|
||||||
|
|
||||||
|
-- 普拉提课程 → 赵教练(14)
|
||||||
|
UPDATE group_course SET coach_id = 14, updated_at = NOW()
|
||||||
|
WHERE course_type = (SELECT id FROM group_course_type WHERE type_name = '普拉提')
|
||||||
|
AND deleted_at IS NULL;
|
||||||
|
|
||||||
|
-- 搏击操课程 → 陈教练(15)
|
||||||
|
UPDATE group_course SET coach_id = 15, updated_at = NOW()
|
||||||
|
WHERE course_type = (SELECT id FROM group_course_type WHERE type_name = '搏击操')
|
||||||
|
AND deleted_at IS NULL;
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- 4. 验证:确认每个团课都有教练
|
||||||
|
-- ============================================
|
||||||
|
DO $$
|
||||||
|
DECLARE
|
||||||
|
null_count INTEGER;
|
||||||
|
BEGIN
|
||||||
|
SELECT COUNT(*) INTO null_count FROM group_course WHERE coach_id IS NULL AND deleted_at IS NULL;
|
||||||
|
IF null_count > 0 THEN
|
||||||
|
RAISE NOTICE '警告:仍有 % 门团课缺少教练', null_count;
|
||||||
|
ELSE
|
||||||
|
RAISE NOTICE '所有团课已成功分配教练';
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
@@ -0,0 +1,141 @@
|
|||||||
|
-- ============================================
|
||||||
|
-- Novalon管理系统索引优化脚本
|
||||||
|
-- 版本: V2
|
||||||
|
-- 描述: 为核心系统表创建必要的索引以提升查询性能
|
||||||
|
-- ============================================
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- 用户与角色表索引
|
||||||
|
-- ============================================
|
||||||
|
|
||||||
|
-- 用户表索引
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_users_username ON sys_user(username);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_users_email ON sys_user(email);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_users_status ON sys_user(status);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_users_deleted_at ON sys_user(deleted_at);
|
||||||
|
|
||||||
|
-- 角色表索引
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_roles_role_key ON sys_role(role_key);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_roles_status ON sys_role(status);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_roles_deleted_at ON sys_role(deleted_at);
|
||||||
|
|
||||||
|
-- 用户角色关联表索引
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_user_role_user_id ON user_role(user_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_user_role_role_id ON user_role(role_id);
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- 权限表索引
|
||||||
|
-- ============================================
|
||||||
|
|
||||||
|
-- 权限表索引
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_permission_code ON sys_permission(permission_code);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_permission_resource ON sys_permission(resource);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_permission_status ON sys_permission(status);
|
||||||
|
|
||||||
|
-- 角色权限关联表索引
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_role_permission_role_id ON sys_role_permission(role_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_role_permission_permission_id ON sys_role_permission(permission_id);
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- 菜单表索引
|
||||||
|
-- ============================================
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_sys_menu_parent_id ON sys_menu(parent_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_sys_menu_status ON sys_menu(status);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_sys_menu_deleted_at ON sys_menu(deleted_at);
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- 字典表索引
|
||||||
|
-- ============================================
|
||||||
|
|
||||||
|
-- 字典类型表索引
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_sys_dict_type_dict_type ON sys_dict_type(dict_type);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_sys_dict_type_status ON sys_dict_type(status);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_sys_dict_type_deleted_at ON sys_dict_type(deleted_at);
|
||||||
|
|
||||||
|
-- 字典数据表索引
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_sys_dict_data_dict_type ON sys_dict_data(dict_type);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_sys_dict_data_dict_value ON sys_dict_data(dict_value);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_sys_dict_data_status ON sys_dict_data(status);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_sys_dict_data_deleted_at ON sys_dict_data(deleted_at);
|
||||||
|
|
||||||
|
-- 通用字典表索引
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_sys_dictionary_type ON sys_dictionary(type);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_sys_dictionary_type_code ON sys_dictionary(type, code);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_sys_dictionary_deleted_at ON sys_dictionary(deleted_at);
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- 系统配置表索引
|
||||||
|
-- ============================================
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_sys_config_config_key ON sys_config(config_key);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_sys_config_config_type ON sys_config(config_type);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_sys_config_deleted_at ON sys_config(deleted_at);
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- 日志表索引
|
||||||
|
-- ============================================
|
||||||
|
|
||||||
|
-- 登录日志表索引
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_sys_login_log_username ON sys_login_log(username);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_sys_login_log_ip ON sys_login_log(ip);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_sys_login_log_status ON sys_login_log(status);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_sys_login_log_login_time ON sys_login_log(login_time);
|
||||||
|
|
||||||
|
-- 异常日志表索引
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_sys_exception_log_username ON sys_exception_log(username);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_sys_exception_log_exception_name ON sys_exception_log(exception_name);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_sys_exception_log_create_time ON sys_exception_log(create_time);
|
||||||
|
|
||||||
|
-- 操作日志表索引
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_operation_log_username ON operation_log(username);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_operation_log_operation ON operation_log(operation);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_operation_log_created_at ON operation_log(created_at);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_operation_log_status ON operation_log(status);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_operation_log_deleted_at ON operation_log(deleted_at);
|
||||||
|
|
||||||
|
-- 审计日志表索引
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_audit_log_entity_type ON audit_log(entity_type);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_audit_log_entity_id ON audit_log(entity_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_audit_log_operation_type ON audit_log(operation_type);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_audit_log_operator ON audit_log(operator);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_audit_log_operation_time ON audit_log(operation_time);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_audit_log_entity ON audit_log(entity_type, entity_id);
|
||||||
|
|
||||||
|
-- 审计日志归档表索引
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_audit_log_archive_entity_type ON audit_log_archive(entity_type);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_audit_log_archive_entity_id ON audit_log_archive(entity_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_audit_log_archive_operation_type ON audit_log_archive(operation_type);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_audit_log_archive_operator ON audit_log_archive(operator);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_audit_log_archive_operation_time ON audit_log_archive(operation_time);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_audit_log_archive_archived_at ON audit_log_archive(archived_at);
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- 通知与消息表索引
|
||||||
|
-- ============================================
|
||||||
|
|
||||||
|
-- 系统公告表索引
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_sys_notice_notice_type ON sys_notice(notice_type);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_sys_notice_status ON sys_notice(status);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_sys_notice_deleted_at ON sys_notice(deleted_at);
|
||||||
|
|
||||||
|
-- 用户消息表索引
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_sys_user_message_user_id ON sys_user_message(user_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_sys_user_message_notice_id ON sys_user_message(notice_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_sys_user_message_is_read ON sys_user_message(is_read);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_sys_user_message_deleted_at ON sys_user_message(deleted_at);
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- 文件管理表索引
|
||||||
|
-- ============================================
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_sys_file_file_type ON sys_file(file_type);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_sys_file_deleted_at ON sys_file(deleted_at);
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- OAuth2客户端表索引
|
||||||
|
-- ============================================
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_oauth2_client_client_id ON oauth2_client(client_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_oauth2_client_enabled ON oauth2_client(enabled);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_oauth2_client_deleted_at ON oauth2_client(deleted_at);
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
-- ============================================
|
||||||
|
-- Novalon管理系统权限授予脚本
|
||||||
|
-- 版本: V3
|
||||||
|
-- 描述: 为novalon用户授予所有表的访问权限
|
||||||
|
-- ============================================
|
||||||
|
|
||||||
|
-- 授予所有表的SELECT, INSERT, UPDATE, DELETE权限
|
||||||
|
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO novalon;
|
||||||
|
|
||||||
|
-- 授予所有序列的使用权限
|
||||||
|
GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO novalon;
|
||||||
|
|
||||||
|
-- 设置默认权限,使未来创建的表自动授予novalon用户权限
|
||||||
|
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO novalon;
|
||||||
|
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT USAGE, SELECT ON SEQUENCES TO novalon;
|
||||||
+238
@@ -0,0 +1,238 @@
|
|||||||
|
-- ============================================
|
||||||
|
-- 会员相关表
|
||||||
|
-- 版本: V4
|
||||||
|
-- 描述: 创建会员、微信用户、会员卡类型、会员卡记录、交易流水、退款申请等表
|
||||||
|
-- ============================================
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- 1. member_user 表(会员表)
|
||||||
|
-- ============================================
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS member_user (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
member_no VARCHAR(50) NOT NULL UNIQUE,
|
||||||
|
nickname VARCHAR(100),
|
||||||
|
phone VARCHAR(255),
|
||||||
|
gender INTEGER DEFAULT 0,
|
||||||
|
birthday DATE,
|
||||||
|
address VARCHAR(500),
|
||||||
|
avatar VARCHAR(500),
|
||||||
|
subscribed BOOLEAN DEFAULT FALSE,
|
||||||
|
last_login_at TIMESTAMP,
|
||||||
|
|
||||||
|
union_id VARCHAR(100),
|
||||||
|
miniapp_open_id VARCHAR(100),
|
||||||
|
official_open_id VARCHAR(100),
|
||||||
|
|
||||||
|
is_deleted BOOLEAN DEFAULT FALSE
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_member_user_member_no ON member_user(member_no);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_member_user_union_id ON member_user(union_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_member_user_miniapp_openid ON member_user(miniapp_open_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_member_user_official_openid ON member_user(official_open_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_member_user_phone ON member_user(phone);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_member_user_is_deleted ON member_user(is_deleted);
|
||||||
|
|
||||||
|
COMMENT ON TABLE member_user IS '会员表';
|
||||||
|
COMMENT ON COLUMN member_user.id IS '主键ID';
|
||||||
|
COMMENT ON COLUMN member_user.created_at IS '创建时间';
|
||||||
|
COMMENT ON COLUMN member_user.updated_at IS '更新时间';
|
||||||
|
COMMENT ON COLUMN member_user.member_no IS '会员编号(唯一,格式:MEM + 8位随机字符)';
|
||||||
|
COMMENT ON COLUMN member_user.nickname IS '昵称';
|
||||||
|
COMMENT ON COLUMN member_user.phone IS '手机号(AES-128-CBC加密存储)';
|
||||||
|
COMMENT ON COLUMN member_user.gender IS '性别:0-未知,1-男,2-女';
|
||||||
|
COMMENT ON COLUMN member_user.birthday IS '生日';
|
||||||
|
COMMENT ON COLUMN member_user.address IS '地址';
|
||||||
|
COMMENT ON COLUMN member_user.avatar IS '头像URL';
|
||||||
|
COMMENT ON COLUMN member_user.subscribed IS '是否关注服务号:true-已关注,false-未关注';
|
||||||
|
COMMENT ON COLUMN member_user.last_login_at IS '最后登录时间';
|
||||||
|
COMMENT ON COLUMN member_user.union_id IS '微信UnionID(用户在开放平台的唯一标识,跨应用相同)';
|
||||||
|
COMMENT ON COLUMN member_user.miniapp_open_id IS '小程序OpenID(用户在当前小程序的唯一标识)';
|
||||||
|
COMMENT ON COLUMN member_user.official_open_id IS '服务号OpenID(用户在当前服务号的唯一标识)';
|
||||||
|
COMMENT ON COLUMN member_user.is_deleted IS '是否删除(软删除标记):false-正常,true-已删除';
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- 2. wechat_user 表(微信用户表)
|
||||||
|
-- ============================================
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS wechat_user (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
member_id BIGINT NOT NULL,
|
||||||
|
|
||||||
|
union_id VARCHAR(100),
|
||||||
|
miniapp_openid VARCHAR(100),
|
||||||
|
mp_openid VARCHAR(100),
|
||||||
|
|
||||||
|
is_subscribed BOOLEAN DEFAULT FALSE,
|
||||||
|
subscribe_time TIMESTAMP,
|
||||||
|
unsubscribe_time TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
ALTER TABLE wechat_user
|
||||||
|
ADD CONSTRAINT fk_wechat_user_member
|
||||||
|
FOREIGN KEY (member_id) REFERENCES member_user(id) ON DELETE CASCADE;
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_wechat_user_union_id ON wechat_user(union_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_wechat_user_miniapp_openid ON wechat_user(miniapp_openid);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_wechat_user_mp_openid ON wechat_user(mp_openid);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_wechat_user_member_id ON wechat_user(member_id);
|
||||||
|
|
||||||
|
COMMENT ON TABLE wechat_user IS '微信用户表';
|
||||||
|
COMMENT ON COLUMN wechat_user.id IS '主键ID';
|
||||||
|
COMMENT ON COLUMN wechat_user.created_at IS '创建时间';
|
||||||
|
COMMENT ON COLUMN wechat_user.updated_at IS '更新时间';
|
||||||
|
COMMENT ON COLUMN wechat_user.member_id IS '会员ID(关联 member_user 表的 id 字段)';
|
||||||
|
COMMENT ON COLUMN wechat_user.union_id IS '微信UnionID(用户在开放平台的唯一标识)';
|
||||||
|
COMMENT ON COLUMN wechat_user.miniapp_openid IS '小程序OpenID(用户在当前小程序的唯一标识)';
|
||||||
|
COMMENT ON COLUMN wechat_user.mp_openid IS '服务号OpenID(用户在当前服务号的唯一标识)';
|
||||||
|
COMMENT ON COLUMN wechat_user.is_subscribed IS '是否关注服务号:true-已关注,false-未关注';
|
||||||
|
COMMENT ON COLUMN wechat_user.subscribe_time IS '首次关注时间';
|
||||||
|
COMMENT ON COLUMN wechat_user.unsubscribe_time IS '最后一次取消关注时间';
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- 3. member_card 表(会员卡类型表)
|
||||||
|
-- ============================================
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS member_card (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
deleted_at TIMESTAMP,
|
||||||
|
|
||||||
|
member_card_id BIGSERIAL,
|
||||||
|
member_card_name VARCHAR(100) NOT NULL,
|
||||||
|
member_card_type VARCHAR(20) NOT NULL,
|
||||||
|
member_card_price DECIMAL(10, 2) NOT NULL,
|
||||||
|
member_card_validity_days INTEGER,
|
||||||
|
member_card_total_times INTEGER,
|
||||||
|
member_card_amount DECIMAL(10, 2),
|
||||||
|
member_card_status INTEGER DEFAULT 1 NOT NULL,
|
||||||
|
extra_config TEXT DEFAULT '{}'
|
||||||
|
);
|
||||||
|
|
||||||
|
COMMENT ON TABLE member_card IS '会员卡类型表';
|
||||||
|
COMMENT ON COLUMN member_card.member_card_id IS '会员卡ID';
|
||||||
|
COMMENT ON COLUMN member_card.member_card_name IS '会员卡名称';
|
||||||
|
COMMENT ON COLUMN member_card.member_card_type IS '会员卡类型:TIME_CARD-时长卡, COUNT_CARD-次卡, STORED_VALUE_CARD-储值卡';
|
||||||
|
COMMENT ON COLUMN member_card.member_card_price IS '会员卡价格';
|
||||||
|
COMMENT ON COLUMN member_card.member_card_validity_days IS '有效天数(时长卡用)';
|
||||||
|
COMMENT ON COLUMN member_card.member_card_total_times IS '总次数(次卡用)';
|
||||||
|
COMMENT ON COLUMN member_card.member_card_amount IS '面额(储值卡用)';
|
||||||
|
COMMENT ON COLUMN member_card.member_card_status IS '状态:0-下架, 1-上架';
|
||||||
|
COMMENT ON COLUMN member_card.extra_config IS '扩展配置(JSON格式)';
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- 4. member_card_record 表(会员卡记录表)
|
||||||
|
-- ============================================
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS member_card_record (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
deleted_at TIMESTAMP,
|
||||||
|
|
||||||
|
member_card_record_id BIGSERIAL,
|
||||||
|
member_id BIGINT NOT NULL,
|
||||||
|
member_card_id BIGINT NOT NULL,
|
||||||
|
status VARCHAR(20) NOT NULL DEFAULT 'ACTIVE',
|
||||||
|
remaining_times INTEGER DEFAULT 0,
|
||||||
|
remaining_amount DECIMAL(10, 2) DEFAULT 0.00,
|
||||||
|
expire_time TIMESTAMP,
|
||||||
|
source_order_id BIGINT,
|
||||||
|
purchase_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
version INTEGER DEFAULT 0 NOT NULL,
|
||||||
|
card_composition TEXT
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_member_card_record_member_id ON member_card_record(member_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_member_card_record_status ON member_card_record(status);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_member_card_record_expire_time ON member_card_record(expire_time);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_member_card_record_member_status ON member_card_record(member_id, status);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_member_card_record_status_expire ON member_card_record(status, expire_time)
|
||||||
|
WHERE status = 'ACTIVE';
|
||||||
|
|
||||||
|
COMMENT ON TABLE member_card_record IS '会员卡记录表(会员持有的卡)';
|
||||||
|
COMMENT ON COLUMN member_card_record.member_card_record_id IS '会员卡记录ID';
|
||||||
|
COMMENT ON COLUMN member_card_record.member_id IS '会员ID';
|
||||||
|
COMMENT ON COLUMN member_card_record.member_card_id IS '会员卡类型ID';
|
||||||
|
COMMENT ON COLUMN member_card_record.status IS '状态:ACTIVE-有效, USED_UP-用完, EXPIRED-过期, REFUNDED-已退款';
|
||||||
|
COMMENT ON COLUMN member_card_record.remaining_times IS '剩余次数';
|
||||||
|
COMMENT ON COLUMN member_card_record.remaining_amount IS '剩余金额';
|
||||||
|
COMMENT ON COLUMN member_card_record.expire_time IS '到期时间';
|
||||||
|
COMMENT ON COLUMN member_card_record.source_order_id IS '来源订单ID';
|
||||||
|
COMMENT ON COLUMN member_card_record.purchase_time IS '购买时间';
|
||||||
|
COMMENT ON COLUMN member_card_record.version IS '乐观锁版本号';
|
||||||
|
COMMENT ON COLUMN member_card_record.card_composition IS '卡片组成(JSON格式,用于组合卡)';
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- 5. member_card_transactions 表(会员卡交易流水表)
|
||||||
|
-- ============================================
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS member_card_transactions (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
member_card_record_id BIGINT NOT NULL,
|
||||||
|
member_id BIGINT NOT NULL,
|
||||||
|
member_card_id BIGINT NOT NULL,
|
||||||
|
operation_type VARCHAR(20) NOT NULL,
|
||||||
|
change_amount INTEGER DEFAULT 0,
|
||||||
|
change_balance DECIMAL(10, 2) DEFAULT 0.00,
|
||||||
|
after_remaining_count INTEGER DEFAULT 0,
|
||||||
|
after_remaining_balance DECIMAL(10, 2) DEFAULT 0.00,
|
||||||
|
related_biz_type VARCHAR(20),
|
||||||
|
source_order_id BIGINT,
|
||||||
|
remark VARCHAR(500),
|
||||||
|
is_archived BOOLEAN DEFAULT FALSE,
|
||||||
|
archived_at TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_member_card_transactions_member_id ON member_card_transactions(member_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_member_card_transactions_record_id ON member_card_transactions(member_card_record_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_member_card_transactions_created_at ON member_card_transactions(created_at);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_member_card_transactions_member_type_time
|
||||||
|
ON member_card_transactions(member_id, operation_type, created_at);
|
||||||
|
|
||||||
|
COMMENT ON TABLE member_card_transactions IS '会员卡交易流水表';
|
||||||
|
COMMENT ON COLUMN member_card_transactions.operation_type IS '操作类型:PURCHASE-购买, DEDUCT-扣次/扣费, RENEW-续费, REFUND-退款, EXPIRE-过期';
|
||||||
|
COMMENT ON COLUMN member_card_transactions.change_amount IS '变动次数';
|
||||||
|
COMMENT ON COLUMN member_card_transactions.change_balance IS '变动金额';
|
||||||
|
COMMENT ON COLUMN member_card_transactions.after_remaining_count IS '变动后剩余次数';
|
||||||
|
COMMENT ON COLUMN member_card_transactions.after_remaining_balance IS '变动后剩余金额';
|
||||||
|
COMMENT ON COLUMN member_card_transactions.related_biz_type IS '关联业务类型:GROUP_CLASS-团课, PT_CLASS-私教, CHECK_IN-签到';
|
||||||
|
COMMENT ON COLUMN member_card_transactions.is_archived IS '是否已归档';
|
||||||
|
COMMENT ON COLUMN member_card_transactions.archived_at IS '归档时间';
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- 6. refund_application 表(退款申请表)
|
||||||
|
-- ============================================
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS refund_application (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
deleted_at TIMESTAMP,
|
||||||
|
|
||||||
|
record_id BIGINT NOT NULL,
|
||||||
|
member_id BIGINT NOT NULL,
|
||||||
|
status VARCHAR(20) NOT NULL DEFAULT 'PENDING',
|
||||||
|
reason VARCHAR(500),
|
||||||
|
apply_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
audit_time TIMESTAMP,
|
||||||
|
auditor_id BIGINT,
|
||||||
|
audit_remark VARCHAR(500),
|
||||||
|
refund_amount DECIMAL(10, 2)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_refund_application_record_id ON refund_application(record_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_refund_application_status ON refund_application(status);
|
||||||
|
|
||||||
|
COMMENT ON TABLE refund_application IS '退款申请表';
|
||||||
|
COMMENT ON COLUMN refund_application.status IS '状态:PENDING-待审核, APPROVED-已批准, REJECTED-已拒绝, PROCESSING-处理中, SUCCESS-成功, FAILED-失败';
|
||||||
+81
@@ -0,0 +1,81 @@
|
|||||||
|
-- ============================================
|
||||||
|
-- 团课相关表
|
||||||
|
-- 版本: V5
|
||||||
|
-- 描述: 创建团课课程表和团课预约记录表
|
||||||
|
-- ============================================
|
||||||
|
|
||||||
|
-- 团课课程表
|
||||||
|
CREATE TABLE IF NOT EXISTS group_course (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
course_name VARCHAR(100) NOT NULL,
|
||||||
|
coach_id BIGINT,
|
||||||
|
course_type BIGINT,
|
||||||
|
start_time TIMESTAMP NOT NULL,
|
||||||
|
end_time TIMESTAMP NOT NULL,
|
||||||
|
max_members INTEGER DEFAULT 20,
|
||||||
|
current_members INTEGER DEFAULT 0,
|
||||||
|
status VARCHAR(1) DEFAULT '0',
|
||||||
|
actual_start_time TIMESTAMP,
|
||||||
|
actual_end_time TIMESTAMP,
|
||||||
|
location VARCHAR(255),
|
||||||
|
cover_image VARCHAR(500),
|
||||||
|
description TEXT,
|
||||||
|
stored_value_amount DECIMAL(10,2) DEFAULT 0,
|
||||||
|
create_by VARCHAR(50),
|
||||||
|
update_by VARCHAR(50),
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
deleted_at TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 团课预约记录表
|
||||||
|
CREATE TABLE IF NOT EXISTS group_course_booking (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
course_id BIGINT NOT NULL,
|
||||||
|
member_id BIGINT NOT NULL,
|
||||||
|
member_card_id BIGINT NOT NULL,
|
||||||
|
booking_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
status VARCHAR(1) DEFAULT '0',
|
||||||
|
cancel_time TIMESTAMP,
|
||||||
|
create_by VARCHAR(50),
|
||||||
|
update_by VARCHAR(50),
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
deleted_at TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
COMMENT ON TABLE group_course IS '团课课程表';
|
||||||
|
COMMENT ON COLUMN group_course.id IS '主键ID';
|
||||||
|
COMMENT ON COLUMN group_course.course_name IS '课程名称';
|
||||||
|
COMMENT ON COLUMN group_course.coach_id IS '教练ID(关联sys_user)';
|
||||||
|
COMMENT ON COLUMN group_course.course_type IS '课程类型(如瑜伽/普拉提/动感单车)';
|
||||||
|
COMMENT ON COLUMN group_course.start_time IS '开始时间';
|
||||||
|
COMMENT ON COLUMN group_course.end_time IS '结束时间';
|
||||||
|
COMMENT ON COLUMN group_course.max_members IS '最大参与人数';
|
||||||
|
COMMENT ON COLUMN group_course.current_members IS '当前参与人数';
|
||||||
|
COMMENT ON COLUMN group_course.status IS '状态(0正常 1已取消 2已结束 3进行中 4超时 5教练缺席 6自动结束 7教练迟到)';
|
||||||
|
COMMENT ON COLUMN group_course.actual_start_time IS '实际开课时间(教练点击开课时记录)';
|
||||||
|
COMMENT ON COLUMN group_course.actual_end_time IS '实际结课时间(教练点击结课时记录)';
|
||||||
|
COMMENT ON COLUMN group_course.location IS '上课地点';
|
||||||
|
COMMENT ON COLUMN group_course.cover_image IS '封面图URL';
|
||||||
|
COMMENT ON COLUMN group_course.description IS '课程描述';
|
||||||
|
COMMENT ON COLUMN group_course.stored_value_amount IS '储值卡额度(消耗金额)';
|
||||||
|
COMMENT ON COLUMN group_course.create_by IS '创建人';
|
||||||
|
COMMENT ON COLUMN group_course.update_by IS '更新人';
|
||||||
|
COMMENT ON COLUMN group_course.created_at IS '创建时间';
|
||||||
|
COMMENT ON COLUMN group_course.updated_at IS '更新时间';
|
||||||
|
COMMENT ON COLUMN group_course.deleted_at IS '删除时间(软删除)';
|
||||||
|
|
||||||
|
COMMENT ON TABLE group_course_booking IS '团课预约记录表';
|
||||||
|
COMMENT ON COLUMN group_course_booking.id IS '主键ID';
|
||||||
|
COMMENT ON COLUMN group_course_booking.course_id IS '团课ID';
|
||||||
|
COMMENT ON COLUMN group_course_booking.member_id IS '用户ID';
|
||||||
|
COMMENT ON COLUMN group_course_booking.member_card_id IS '会员卡ID';
|
||||||
|
COMMENT ON COLUMN group_course_booking.booking_time IS '预约时间';
|
||||||
|
COMMENT ON COLUMN group_course_booking.status IS '状态(0已预约 1已取消 2已出席 3缺席 4教练缺席 5迟到)';
|
||||||
|
COMMENT ON COLUMN group_course_booking.cancel_time IS '取消时间';
|
||||||
|
COMMENT ON COLUMN group_course_booking.create_by IS '创建人';
|
||||||
|
COMMENT ON COLUMN group_course_booking.update_by IS '更新人';
|
||||||
|
COMMENT ON COLUMN group_course_booking.created_at IS '创建时间';
|
||||||
|
COMMENT ON COLUMN group_course_booking.updated_at IS '更新时间';
|
||||||
|
COMMENT ON COLUMN group_course_booking.deleted_at IS '删除时间(软删除)';
|
||||||
+16
@@ -0,0 +1,16 @@
|
|||||||
|
-- ============================================
|
||||||
|
-- 为团课预约记录表添加课程冗余字段
|
||||||
|
-- 版本: V6
|
||||||
|
-- 描述: 保存预约时的课程快照信息
|
||||||
|
-- ============================================
|
||||||
|
|
||||||
|
ALTER TABLE group_course_booking
|
||||||
|
ADD COLUMN IF NOT EXISTS course_name VARCHAR(100),
|
||||||
|
ADD COLUMN IF NOT EXISTS course_start_time TIMESTAMP,
|
||||||
|
ADD COLUMN IF NOT EXISTS course_end_time TIMESTAMP,
|
||||||
|
ADD COLUMN IF NOT EXISTS location VARCHAR(255);
|
||||||
|
|
||||||
|
COMMENT ON COLUMN group_course_booking.course_name IS '课程名称(冗余字段,保存预约时的课程快照)';
|
||||||
|
COMMENT ON COLUMN group_course_booking.course_start_time IS '课程开始时间(冗余字段,保存预约时的课程快照)';
|
||||||
|
COMMENT ON COLUMN group_course_booking.course_end_time IS '课程结束时间(冗余字段,保存预约时的课程快照)';
|
||||||
|
COMMENT ON COLUMN group_course_booking.location IS '上课地点(冗余字段,保存预约时的课程快照)';
|
||||||
+52
@@ -0,0 +1,52 @@
|
|||||||
|
-- ============================================
|
||||||
|
-- 会员到店签到记录表
|
||||||
|
-- 版本: V7
|
||||||
|
-- 描述: 创建sign_in_record表,用于记录会员签到信息
|
||||||
|
-- ============================================
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS sign_in_record (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
member_id BIGINT NOT NULL,
|
||||||
|
member_card_id BIGINT,
|
||||||
|
sign_in_time TIMESTAMP NOT NULL,
|
||||||
|
sign_in_type VARCHAR(20) NOT NULL,
|
||||||
|
sign_in_status VARCHAR(20) NOT NULL DEFAULT 'SUCCESS',
|
||||||
|
verification_details TEXT,
|
||||||
|
fail_reason VARCHAR(500),
|
||||||
|
operator_id BIGINT,
|
||||||
|
operator_name VARCHAR(100),
|
||||||
|
device_info VARCHAR(200),
|
||||||
|
ip_address VARCHAR(50),
|
||||||
|
source VARCHAR(20) NOT NULL,
|
||||||
|
is_delete BOOLEAN DEFAULT FALSE,
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_sign_in_record_member_id ON sign_in_record(member_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_sign_in_record_sign_in_time ON sign_in_record(sign_in_time);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_sign_in_record_sign_in_status ON sign_in_record(sign_in_status);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_sign_in_record_member_card_id ON sign_in_record(member_card_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_sign_in_record_operator_id ON sign_in_record(operator_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_sign_in_record_source ON sign_in_record(source);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_sign_in_record_is_delete ON sign_in_record(is_delete);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_sign_in_record_member_time ON sign_in_record(member_id, sign_in_time);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_sign_in_record_status_time ON sign_in_record(sign_in_status, sign_in_time);
|
||||||
|
|
||||||
|
COMMENT ON TABLE sign_in_record IS '会员到店签到记录表';
|
||||||
|
COMMENT ON COLUMN sign_in_record.id IS '自增主键';
|
||||||
|
COMMENT ON COLUMN sign_in_record.member_id IS '会员ID,关联member表';
|
||||||
|
COMMENT ON COLUMN sign_in_record.member_card_id IS '签到时使用的会员卡ID';
|
||||||
|
COMMENT ON COLUMN sign_in_record.sign_in_time IS '签到入场时间';
|
||||||
|
COMMENT ON COLUMN sign_in_record.sign_in_type IS '签到方式:QR_CODE-扫码签到,MANUAL-手动签到,FACE-人脸识别';
|
||||||
|
COMMENT ON COLUMN sign_in_record.sign_in_status IS '签到状态:SUCCESS-成功,FAILED-失败';
|
||||||
|
COMMENT ON COLUMN sign_in_record.verification_details IS 'JSON格式,存储会员卡验证时的快照数据';
|
||||||
|
COMMENT ON COLUMN sign_in_record.fail_reason IS '失败时的具体原因文案';
|
||||||
|
COMMENT ON COLUMN sign_in_record.operator_id IS '操作人ID(前台人员),自助签到时为NULL';
|
||||||
|
COMMENT ON COLUMN sign_in_record.operator_name IS '操作人姓名冗余';
|
||||||
|
COMMENT ON COLUMN sign_in_record.device_info IS '签到设备标识或型号';
|
||||||
|
COMMENT ON COLUMN sign_in_record.ip_address IS '客户端IP地址';
|
||||||
|
COMMENT ON COLUMN sign_in_record.source IS '签到来源:MINI_PROGRAM-小程序扫码,PC_BACKEND-后台管理端';
|
||||||
|
COMMENT ON COLUMN sign_in_record.is_delete IS '软删除标识:false-未删除,true-已删除';
|
||||||
|
COMMENT ON COLUMN sign_in_record.created_at IS '记录创建时间';
|
||||||
|
COMMENT ON COLUMN sign_in_record.updated_at IS '记录更新时间';
|
||||||
+33
@@ -0,0 +1,33 @@
|
|||||||
|
-- ============================================
|
||||||
|
-- 团课类型表
|
||||||
|
-- 版本: V8
|
||||||
|
-- 描述: 创建团课类型表,用于分类管理团课
|
||||||
|
-- ============================================
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS group_course_type (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
type_name VARCHAR(100) NOT NULL,
|
||||||
|
base_difficulty INTEGER DEFAULT 1,
|
||||||
|
description TEXT,
|
||||||
|
category VARCHAR(50),
|
||||||
|
create_by VARCHAR(50),
|
||||||
|
update_by VARCHAR(50),
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
deleted_at TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_group_course_type_type_name ON group_course_type(type_name);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_group_course_type_category ON group_course_type(category);
|
||||||
|
|
||||||
|
COMMENT ON TABLE group_course_type IS '团课类型表';
|
||||||
|
COMMENT ON COLUMN group_course_type.id IS '主键ID';
|
||||||
|
COMMENT ON COLUMN group_course_type.type_name IS '类型名称';
|
||||||
|
COMMENT ON COLUMN group_course_type.base_difficulty IS '基础难度(1-10)';
|
||||||
|
COMMENT ON COLUMN group_course_type.description IS '类型描述';
|
||||||
|
COMMENT ON COLUMN group_course_type.category IS '分类(如:有氧、力量、柔韧等)';
|
||||||
|
COMMENT ON COLUMN group_course_type.create_by IS '创建人';
|
||||||
|
COMMENT ON COLUMN group_course_type.update_by IS '更新人';
|
||||||
|
COMMENT ON COLUMN group_course_type.created_at IS '创建时间';
|
||||||
|
COMMENT ON COLUMN group_course_type.updated_at IS '更新时间';
|
||||||
|
COMMENT ON COLUMN group_course_type.deleted_at IS '删除时间(软删除)';
|
||||||
+56
@@ -0,0 +1,56 @@
|
|||||||
|
-- ============================================
|
||||||
|
-- 团课标签相关表
|
||||||
|
-- 版本: V9
|
||||||
|
-- 描述: 创建团课标签表和类型-标签关联表
|
||||||
|
-- ============================================
|
||||||
|
|
||||||
|
-- 团课标签表
|
||||||
|
CREATE TABLE IF NOT EXISTS course_label (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
label_name VARCHAR(50) NOT NULL,
|
||||||
|
color VARCHAR(20) DEFAULT '#1890ff',
|
||||||
|
description VARCHAR(200),
|
||||||
|
create_by VARCHAR(50),
|
||||||
|
update_by VARCHAR(50),
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
deleted_at TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 团课类型-标签关联表
|
||||||
|
CREATE TABLE IF NOT EXISTS course_type_label (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
type_id BIGINT NOT NULL,
|
||||||
|
label_id BIGINT NOT NULL,
|
||||||
|
create_by VARCHAR(50),
|
||||||
|
update_by VARCHAR(50),
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
deleted_at TIMESTAMP,
|
||||||
|
UNIQUE (type_id, label_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_course_label_label_name ON course_label(label_name);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_course_type_label_type_id ON course_type_label(type_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_course_type_label_label_id ON course_type_label(label_id);
|
||||||
|
|
||||||
|
COMMENT ON TABLE course_label IS '团课标签表';
|
||||||
|
COMMENT ON COLUMN course_label.id IS '主键ID';
|
||||||
|
COMMENT ON COLUMN course_label.label_name IS '标签名称';
|
||||||
|
COMMENT ON COLUMN course_label.color IS '标签颜色(十六进制)';
|
||||||
|
COMMENT ON COLUMN course_label.description IS '标签描述';
|
||||||
|
COMMENT ON COLUMN course_label.create_by IS '创建人';
|
||||||
|
COMMENT ON COLUMN course_label.update_by IS '更新人';
|
||||||
|
COMMENT ON COLUMN course_label.created_at IS '创建时间';
|
||||||
|
COMMENT ON COLUMN course_label.updated_at IS '更新时间';
|
||||||
|
COMMENT ON COLUMN course_label.deleted_at IS '删除时间(软删除)';
|
||||||
|
|
||||||
|
COMMENT ON TABLE course_type_label IS '团课类型-标签关联表';
|
||||||
|
COMMENT ON COLUMN course_type_label.id IS '主键ID';
|
||||||
|
COMMENT ON COLUMN course_type_label.type_id IS '团课类型ID';
|
||||||
|
COMMENT ON COLUMN course_type_label.label_id IS '标签ID';
|
||||||
|
COMMENT ON COLUMN course_type_label.create_by IS '创建人';
|
||||||
|
COMMENT ON COLUMN course_type_label.update_by IS '更新人';
|
||||||
|
COMMENT ON COLUMN course_type_label.created_at IS '创建时间';
|
||||||
|
COMMENT ON COLUMN course_type_label.updated_at IS '更新时间';
|
||||||
|
COMMENT ON COLUMN course_type_label.deleted_at IS '删除时间(软删除)';
|
||||||
+1
@@ -64,6 +64,7 @@ public class JwtAuthenticationFilter extends AbstractGatewayFilterFactory<JwtAut
|
|||||||
path.equals("/api/groupCourse/page") ||
|
path.equals("/api/groupCourse/page") ||
|
||||||
path.startsWith("/api/checkIn") ||
|
path.startsWith("/api/checkIn") ||
|
||||||
path.startsWith("/api/payment") ||
|
path.startsWith("/api/payment") ||
|
||||||
|
path.startsWith("/api/files/") ||
|
||||||
path.startsWith("/actuator/info");
|
path.startsWith("/actuator/info");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+8
-1
@@ -64,7 +64,14 @@ public class RbacAuthorizationFilter extends AbstractGatewayFilterFactory<RbacAu
|
|||||||
return path.startsWith("/api/auth/") ||
|
return path.startsWith("/api/auth/") ||
|
||||||
path.equals("/actuator/health") ||
|
path.equals("/actuator/health") ||
|
||||||
path.startsWith("/actuator/info") ||
|
path.startsWith("/actuator/info") ||
|
||||||
path.startsWith("/api/checkIn/");
|
path.startsWith("/api/checkIn/") ||
|
||||||
|
path.startsWith("/api/member/") ||
|
||||||
|
path.startsWith("/api/groupCourse/") ||
|
||||||
|
path.startsWith("/api/member-cards/") ||
|
||||||
|
path.startsWith("/api/stored-card/") ||
|
||||||
|
path.startsWith("/api/pay-password/") ||
|
||||||
|
path.startsWith("/api/datacount/") ||
|
||||||
|
path.startsWith("/api/files/");
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class Config {
|
public static class Config {
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ signature:
|
|||||||
max-age-minutes: ${SIGNATURE_MAX_AGE_MINUTES:5}
|
max-age-minutes: ${SIGNATURE_MAX_AGE_MINUTES:5}
|
||||||
nonce-cache-size: ${SIGNATURE_NONCE_CACHE_SIZE:10000}
|
nonce-cache-size: ${SIGNATURE_NONCE_CACHE_SIZE:10000}
|
||||||
whitelist:
|
whitelist:
|
||||||
paths: ${SIGNATURE_WHITELIST_PATHS:/actuator/health,/actuator/info,/api/auth/login,/api/auth/register,/api/member/auth/miniapp/login,/api/member/auth/mp/callback,/api/groupCourse/**}
|
paths: ${SIGNATURE_WHITELIST_PATHS:/actuator/health,/actuator/info,/api/auth/login,/api/auth/register,/api/member/auth/miniapp/login,/api/member/auth/mp/callback,/api/groupCourse/**,/api/checkIn/**,/api/member/**,/api/member-cards/**,/api/stored-card/**,/api/pay-password/**,/api/datacount/**,/api/files/**}
|
||||||
|
|
||||||
resilience:
|
resilience:
|
||||||
enabled: ${RESILIENCE_ENABLED:true}
|
enabled: ${RESILIENCE_ENABLED:true}
|
||||||
|
|||||||
+97
@@ -0,0 +1,97 @@
|
|||||||
|
package cn.novalon.gym.manage.notify.core.domain;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
public class Banner {
|
||||||
|
|
||||||
|
private Long id;
|
||||||
|
private String imageUrl;
|
||||||
|
private String title;
|
||||||
|
private String subtitle;
|
||||||
|
private Integer sortOrder;
|
||||||
|
private String isActive;
|
||||||
|
private String linkUrl;
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
private LocalDateTime updatedAt;
|
||||||
|
private LocalDateTime deletedAt;
|
||||||
|
|
||||||
|
public Long getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setId(Long id) {
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getImageUrl() {
|
||||||
|
return imageUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setImageUrl(String imageUrl) {
|
||||||
|
this.imageUrl = imageUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getTitle() {
|
||||||
|
return title;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTitle(String title) {
|
||||||
|
this.title = title;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getSubtitle() {
|
||||||
|
return subtitle;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSubtitle(String subtitle) {
|
||||||
|
this.subtitle = subtitle;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getSortOrder() {
|
||||||
|
return sortOrder;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSortOrder(Integer sortOrder) {
|
||||||
|
this.sortOrder = sortOrder;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getIsActive() {
|
||||||
|
return isActive;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setIsActive(String isActive) {
|
||||||
|
this.isActive = isActive;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getLinkUrl() {
|
||||||
|
return linkUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setLinkUrl(String linkUrl) {
|
||||||
|
this.linkUrl = linkUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LocalDateTime getCreatedAt() {
|
||||||
|
return createdAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCreatedAt(LocalDateTime createdAt) {
|
||||||
|
this.createdAt = createdAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LocalDateTime getUpdatedAt() {
|
||||||
|
return updatedAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUpdatedAt(LocalDateTime updatedAt) {
|
||||||
|
this.updatedAt = updatedAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LocalDateTime getDeletedAt() {
|
||||||
|
return deletedAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDeletedAt(LocalDateTime deletedAt) {
|
||||||
|
this.deletedAt = deletedAt;
|
||||||
|
}
|
||||||
|
}
|
||||||
+18
@@ -0,0 +1,18 @@
|
|||||||
|
package cn.novalon.gym.manage.notify.core.repository;
|
||||||
|
|
||||||
|
import cn.novalon.gym.manage.notify.core.domain.Banner;
|
||||||
|
import reactor.core.publisher.Flux;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
|
public interface IBannerRepository {
|
||||||
|
|
||||||
|
Flux<Banner> findByDeletedAtIsNull();
|
||||||
|
|
||||||
|
Flux<Banner> findActiveBanners();
|
||||||
|
|
||||||
|
Mono<Banner> findById(Long id);
|
||||||
|
|
||||||
|
Mono<Banner> save(Banner banner);
|
||||||
|
|
||||||
|
Mono<Void> deleteByIdAndDeletedAtIsNull(Long id);
|
||||||
|
}
|
||||||
+20
@@ -0,0 +1,20 @@
|
|||||||
|
package cn.novalon.gym.manage.notify.core.service;
|
||||||
|
|
||||||
|
import cn.novalon.gym.manage.notify.core.domain.Banner;
|
||||||
|
import reactor.core.publisher.Flux;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
|
public interface IBannerService {
|
||||||
|
|
||||||
|
Flux<Banner> getAllBanners();
|
||||||
|
|
||||||
|
Flux<Banner> getActiveBanners();
|
||||||
|
|
||||||
|
Mono<Banner> getBannerById(Long id);
|
||||||
|
|
||||||
|
Mono<Banner> createBanner(Banner banner);
|
||||||
|
|
||||||
|
Mono<Banner> updateBanner(Long id, Banner banner);
|
||||||
|
|
||||||
|
Mono<Void> deleteBanner(Long id);
|
||||||
|
}
|
||||||
+80
@@ -0,0 +1,80 @@
|
|||||||
|
package cn.novalon.gym.manage.notify.core.service.impl;
|
||||||
|
|
||||||
|
import cn.novalon.gym.manage.notify.core.domain.Banner;
|
||||||
|
import cn.novalon.gym.manage.notify.core.repository.IBannerRepository;
|
||||||
|
import cn.novalon.gym.manage.notify.core.service.IBannerService;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import reactor.core.publisher.Flux;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class BannerServiceImpl implements IBannerService {
|
||||||
|
|
||||||
|
private final IBannerRepository bannerRepository;
|
||||||
|
|
||||||
|
public BannerServiceImpl(IBannerRepository bannerRepository) {
|
||||||
|
this.bannerRepository = bannerRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Flux<Banner> getAllBanners() {
|
||||||
|
return bannerRepository.findByDeletedAtIsNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Flux<Banner> getActiveBanners() {
|
||||||
|
return bannerRepository.findActiveBanners();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Mono<Banner> getBannerById(Long id) {
|
||||||
|
return bannerRepository.findById(id)
|
||||||
|
.filter(banner -> banner.getDeletedAt() == null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Mono<Banner> createBanner(Banner banner) {
|
||||||
|
banner.setCreatedAt(LocalDateTime.now());
|
||||||
|
return bannerRepository.save(banner);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Mono<Banner> updateBanner(Long id, Banner banner) {
|
||||||
|
return bannerRepository.findById(id)
|
||||||
|
.flatMap(existing -> {
|
||||||
|
if (banner.getImageUrl() != null) {
|
||||||
|
existing.setImageUrl(banner.getImageUrl());
|
||||||
|
}
|
||||||
|
if (banner.getTitle() != null) {
|
||||||
|
existing.setTitle(banner.getTitle());
|
||||||
|
}
|
||||||
|
if (banner.getSubtitle() != null) {
|
||||||
|
existing.setSubtitle(banner.getSubtitle());
|
||||||
|
}
|
||||||
|
if (banner.getSortOrder() != null) {
|
||||||
|
existing.setSortOrder(banner.getSortOrder());
|
||||||
|
}
|
||||||
|
if (banner.getIsActive() != null) {
|
||||||
|
existing.setIsActive(banner.getIsActive());
|
||||||
|
}
|
||||||
|
if (banner.getLinkUrl() != null) {
|
||||||
|
existing.setLinkUrl(banner.getLinkUrl());
|
||||||
|
}
|
||||||
|
existing.setUpdatedAt(LocalDateTime.now());
|
||||||
|
return bannerRepository.save(existing);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Mono<Void> deleteBanner(Long id) {
|
||||||
|
return bannerRepository.findById(id)
|
||||||
|
.filter(banner -> banner.getDeletedAt() == null)
|
||||||
|
.flatMap(banner -> {
|
||||||
|
banner.setDeletedAt(LocalDateTime.now());
|
||||||
|
return bannerRepository.save(banner);
|
||||||
|
})
|
||||||
|
.then();
|
||||||
|
}
|
||||||
|
}
|
||||||
+120
@@ -0,0 +1,120 @@
|
|||||||
|
package cn.novalon.gym.manage.notify.handler;
|
||||||
|
|
||||||
|
import cn.novalon.gym.manage.notify.core.domain.Banner;
|
||||||
|
import cn.novalon.gym.manage.notify.core.service.IBannerService;
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
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.Flux;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
@Tag(name = "轮播图管理", description = "首页轮播图相关操作")
|
||||||
|
public class BannerHandler {
|
||||||
|
|
||||||
|
private final IBannerService bannerService;
|
||||||
|
|
||||||
|
public BannerHandler(IBannerService bannerService) {
|
||||||
|
this.bannerService = bannerService;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "获取所有轮播图", description = "获取系统中所有轮播图列表(按排序升序)")
|
||||||
|
public Mono<ServerResponse> getAllBanners(ServerRequest request) {
|
||||||
|
Flux<Banner> banners = bannerService.getAllBanners();
|
||||||
|
return ServerResponse.ok().body(banners, Banner.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "获取启用的轮播图", description = "获取所有启用状态的轮播图(按排序升序),供外部页面调用")
|
||||||
|
public Mono<ServerResponse> getActiveBanners(ServerRequest request) {
|
||||||
|
Flux<Banner> banners = bannerService.getActiveBanners();
|
||||||
|
return ServerResponse.ok().body(banners, Banner.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "根据ID获取轮播图", description = "根据轮播图ID获取详细信息")
|
||||||
|
public Mono<ServerResponse> getBannerById(ServerRequest request) {
|
||||||
|
Long id = Long.parseLong(request.pathVariable("id"));
|
||||||
|
return bannerService.getBannerById(id)
|
||||||
|
.flatMap(banner -> ServerResponse.ok().bodyValue(banner))
|
||||||
|
.switchIfEmpty(ServerResponse.notFound().build());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "创建轮播图", description = "创建新轮播图")
|
||||||
|
public Mono<ServerResponse> createBanner(ServerRequest request) {
|
||||||
|
return request.bodyToMono(Map.class)
|
||||||
|
.map(this::mapToBanner)
|
||||||
|
.filter(banner -> banner.getImageUrl() != null && !banner.getImageUrl().trim().isEmpty())
|
||||||
|
.switchIfEmpty(Mono.error(new IllegalArgumentException("轮播图图片地址不能为空")))
|
||||||
|
.filter(banner -> banner.getTitle() != null && !banner.getTitle().trim().isEmpty())
|
||||||
|
.switchIfEmpty(Mono.error(new IllegalArgumentException("轮播图标题不能为空")))
|
||||||
|
.flatMap(banner -> {
|
||||||
|
if (banner.getSortOrder() == null) {
|
||||||
|
banner.setSortOrder(0);
|
||||||
|
}
|
||||||
|
if (banner.getIsActive() == null) {
|
||||||
|
banner.setIsActive("1");
|
||||||
|
}
|
||||||
|
return bannerService.createBanner(banner);
|
||||||
|
})
|
||||||
|
.flatMap(banner -> ServerResponse.ok().bodyValue(banner))
|
||||||
|
.onErrorResume(IllegalArgumentException.class, ex ->
|
||||||
|
ServerResponse.badRequest().bodyValue(Map.of(
|
||||||
|
"code", 400,
|
||||||
|
"message", ex.getMessage(),
|
||||||
|
"timestamp", LocalDateTime.now()
|
||||||
|
))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "更新轮播图", description = "更新轮播图信息")
|
||||||
|
public Mono<ServerResponse> updateBanner(ServerRequest request) {
|
||||||
|
Long id = Long.parseLong(request.pathVariable("id"));
|
||||||
|
return request.bodyToMono(Map.class)
|
||||||
|
.map(this::mapToBanner)
|
||||||
|
.flatMap(banner -> bannerService.updateBanner(id, banner))
|
||||||
|
.flatMap(banner -> ServerResponse.ok().bodyValue(banner))
|
||||||
|
.switchIfEmpty(ServerResponse.notFound().build());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "删除轮播图", description = "删除指定轮播图(软删除)")
|
||||||
|
public Mono<ServerResponse> deleteBanner(ServerRequest request) {
|
||||||
|
Long id = Long.parseLong(request.pathVariable("id"));
|
||||||
|
return bannerService.getBannerById(id)
|
||||||
|
.filter(banner -> banner.getDeletedAt() == null)
|
||||||
|
.flatMap(banner -> bannerService.deleteBanner(id)
|
||||||
|
.then(ServerResponse.noContent().build()))
|
||||||
|
.switchIfEmpty(ServerResponse.notFound().build());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将前端传来的 Map 转换为 Banner 领域对象
|
||||||
|
* 兼容 isActive 以 boolean 或 string 形式传入
|
||||||
|
*/
|
||||||
|
private Banner mapToBanner(Map<String, Object> body) {
|
||||||
|
Banner banner = new Banner();
|
||||||
|
banner.setImageUrl((String) body.get("imageUrl"));
|
||||||
|
banner.setTitle((String) body.get("title"));
|
||||||
|
banner.setSubtitle((String) body.get("subtitle"));
|
||||||
|
banner.setLinkUrl((String) body.get("linkUrl"));
|
||||||
|
|
||||||
|
// sortOrder 处理
|
||||||
|
Object sortOrderObj = body.get("sortOrder");
|
||||||
|
if (sortOrderObj instanceof Number) {
|
||||||
|
banner.setSortOrder(((Number) sortOrderObj).intValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
// isActive 兼容 boolean 和 string
|
||||||
|
Object isActiveObj = body.get("isActive");
|
||||||
|
if (isActiveObj instanceof Boolean) {
|
||||||
|
banner.setIsActive((Boolean) isActiveObj ? "1" : "0");
|
||||||
|
} else if (isActiveObj instanceof String) {
|
||||||
|
banner.setIsActive((String) isActiveObj);
|
||||||
|
}
|
||||||
|
|
||||||
|
return banner;
|
||||||
|
}
|
||||||
|
}
|
||||||
+3
-1
@@ -65,7 +65,9 @@ public class SecurityConfig {
|
|||||||
.pathMatchers("/api/payment/create").permitAll()
|
.pathMatchers("/api/payment/create").permitAll()
|
||||||
.pathMatchers("/api/payment/query").permitAll()
|
.pathMatchers("/api/payment/query").permitAll()
|
||||||
.pathMatchers("/api/payment/notify").permitAll()
|
.pathMatchers("/api/payment/notify").permitAll()
|
||||||
.pathMatchers("/api/payment/refund").permitAll();
|
.pathMatchers("/api/payment/refund").permitAll()
|
||||||
|
.pathMatchers("/api/files/**").permitAll()
|
||||||
|
.pathMatchers("/api/coach/**").permitAll();
|
||||||
|
|
||||||
if (isDevOrTest) {
|
if (isDevOrTest) {
|
||||||
spec.pathMatchers("/swagger-ui.html").permitAll()
|
spec.pathMatchers("/swagger-ui.html").permitAll()
|
||||||
|
|||||||
+54
@@ -0,0 +1,54 @@
|
|||||||
|
package cn.novalon.gym.manage.sys.dto.request;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import jakarta.validation.constraints.Email;
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
import jakarta.validation.constraints.Pattern;
|
||||||
|
import jakarta.validation.constraints.Size;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 教练创建请求DTO
|
||||||
|
*
|
||||||
|
* @author 张翔
|
||||||
|
* @date 2026-07-19
|
||||||
|
*/
|
||||||
|
@Schema(description = "教练创建请求")
|
||||||
|
public class CoachCreateRequest {
|
||||||
|
|
||||||
|
@Schema(description = "用户名", example = "coach01")
|
||||||
|
@NotBlank(message = "用户名不能为空")
|
||||||
|
@Size(min = 3, max = 50, message = "用户名长度必须在3-50之间")
|
||||||
|
@Pattern(regexp = "^[a-zA-Z0-9_-]+$", message = "用户名只能包含字母、数字、下划线和横线")
|
||||||
|
private String username;
|
||||||
|
|
||||||
|
@Schema(description = "昵称", example = "张教练")
|
||||||
|
@Size(max = 100, message = "昵称长度不能超过100")
|
||||||
|
private String nickname;
|
||||||
|
|
||||||
|
@Schema(description = "密码", example = "Coach@123")
|
||||||
|
@NotBlank(message = "密码不能为空")
|
||||||
|
@Size(min = 8, max = 20, message = "密码长度必须在8-20之间")
|
||||||
|
@Pattern(regexp = "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d).+$", message = "密码必须包含大小写字母和数字")
|
||||||
|
private String password;
|
||||||
|
|
||||||
|
@Schema(description = "邮箱", example = "coach@example.com")
|
||||||
|
@NotBlank(message = "邮箱不能为空")
|
||||||
|
@Email(message = "邮箱格式不正确")
|
||||||
|
private String email;
|
||||||
|
|
||||||
|
@Schema(description = "手机号", example = "13800138001")
|
||||||
|
@NotBlank(message = "手机号不能为空")
|
||||||
|
@Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确")
|
||||||
|
private String phone;
|
||||||
|
|
||||||
|
public String getUsername() { return username; }
|
||||||
|
public void setUsername(String username) { this.username = username; }
|
||||||
|
public String getNickname() { return nickname; }
|
||||||
|
public void setNickname(String nickname) { this.nickname = nickname; }
|
||||||
|
public String getPassword() { return password; }
|
||||||
|
public void setPassword(String password) { this.password = password; }
|
||||||
|
public String getEmail() { return email; }
|
||||||
|
public void setEmail(String email) { this.email = email; }
|
||||||
|
public String getPhone() { return phone; }
|
||||||
|
public void setPhone(String phone) { this.phone = phone; }
|
||||||
|
}
|
||||||
+35
@@ -0,0 +1,35 @@
|
|||||||
|
package cn.novalon.gym.manage.sys.dto.request;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import jakarta.validation.constraints.Email;
|
||||||
|
import jakarta.validation.constraints.Pattern;
|
||||||
|
import jakarta.validation.constraints.Size;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 教练更新请求DTO
|
||||||
|
*
|
||||||
|
* @author 张翔
|
||||||
|
* @date 2026-07-19
|
||||||
|
*/
|
||||||
|
@Schema(description = "教练更新请求")
|
||||||
|
public class CoachUpdateRequest {
|
||||||
|
|
||||||
|
@Schema(description = "昵称", example = "张教练")
|
||||||
|
@Size(max = 100, message = "昵称长度不能超过100")
|
||||||
|
private String nickname;
|
||||||
|
|
||||||
|
@Schema(description = "邮箱", example = "coach@example.com")
|
||||||
|
@Email(message = "邮箱格式不正确")
|
||||||
|
private String email;
|
||||||
|
|
||||||
|
@Schema(description = "手机号", example = "13800138001")
|
||||||
|
@Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确")
|
||||||
|
private String phone;
|
||||||
|
|
||||||
|
public String getNickname() { return nickname; }
|
||||||
|
public void setNickname(String nickname) { this.nickname = nickname; }
|
||||||
|
public String getEmail() { return email; }
|
||||||
|
public void setEmail(String email) { this.email = email; }
|
||||||
|
public String getPhone() { return phone; }
|
||||||
|
public void setPhone(String phone) { this.phone = phone; }
|
||||||
|
}
|
||||||
@@ -48,6 +48,7 @@
|
|||||||
<module>gym-dataCount</module>
|
<module>gym-dataCount</module>
|
||||||
<module>gym-auth</module>
|
<module>gym-auth</module>
|
||||||
<module>gym-payment</module>
|
<module>gym-payment</module>
|
||||||
|
<module>gym-coach</module>
|
||||||
</modules>
|
</modules>
|
||||||
|
|
||||||
<dependencyManagement>
|
<dependencyManagement>
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
node_modules/
|
||||||
|
unpackage/
|
||||||
|
.hbuilderx/
|
||||||
|
.DS_Store
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
onLaunch: function() {
|
||||||
|
console.log('Coach App Launch')
|
||||||
|
},
|
||||||
|
onShow: function() {
|
||||||
|
console.log('Coach App Show')
|
||||||
|
},
|
||||||
|
onHide: function() {
|
||||||
|
console.log('Coach App Hide')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
page {
|
||||||
|
background: #F5F7FA;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
color: #1E1E1E;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
view, text, button, input {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
const http = require('../utils/request')
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 教练登录(账号密码)
|
||||||
|
*/
|
||||||
|
function login(username, password) {
|
||||||
|
return http.post('/auth/login', { username, password })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取教练负责的团课列表
|
||||||
|
*/
|
||||||
|
function getCoachCourses(coachId) {
|
||||||
|
return http.get('/coach/' + coachId + '/courses')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 教练手动开课
|
||||||
|
*/
|
||||||
|
function startCourse(courseId) {
|
||||||
|
return http.post('/coach/courses/' + courseId + '/start')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 教练手动结课
|
||||||
|
*/
|
||||||
|
function endCourse(courseId) {
|
||||||
|
return http.post('/coach/courses/' + courseId + '/end')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取团课详情
|
||||||
|
*/
|
||||||
|
function getCourseDetail(courseId) {
|
||||||
|
return http.get('/groupCourse/' + courseId + '/detail')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取教练违规记录
|
||||||
|
*/
|
||||||
|
function getCoachViolations(coachId) {
|
||||||
|
return http.get('/coach/' + coachId + '/violations')
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
login,
|
||||||
|
getCoachCourses,
|
||||||
|
startCourse,
|
||||||
|
endCourse,
|
||||||
|
getCourseDetail,
|
||||||
|
getCoachViolations
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<script>
|
||||||
|
var coverSupport = 'CSS' in window && typeof CSS.supports === 'function' && (CSS.supports('top: env(a)') ||
|
||||||
|
CSS.supports('top: constant(a)'))
|
||||||
|
document.write(
|
||||||
|
'<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0' +
|
||||||
|
(coverSupport ? ', viewport-fit=cover' : '') + '" />')
|
||||||
|
</script>
|
||||||
|
<title></title>
|
||||||
|
<!--preload-links-->
|
||||||
|
<!--app-context-->
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"><!--app-html--></div>
|
||||||
|
<script type="module" src="/main.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import App from './App'
|
||||||
|
|
||||||
|
// #ifndef VUE3
|
||||||
|
import Vue from 'vue'
|
||||||
|
import './uni.promisify.adaptor'
|
||||||
|
Vue.config.productionTip = false
|
||||||
|
App.mpType = 'app'
|
||||||
|
const app = new Vue({
|
||||||
|
...App
|
||||||
|
})
|
||||||
|
app.$mount()
|
||||||
|
// #endif
|
||||||
|
|
||||||
|
// #ifdef VUE3
|
||||||
|
import { createSSRApp } from 'vue'
|
||||||
|
export function createApp() {
|
||||||
|
const app = createSSRApp(App)
|
||||||
|
return {
|
||||||
|
app
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// #endif
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
{
|
||||||
|
"name" : "gym-manage-coach-uniapp",
|
||||||
|
"appid" : "",
|
||||||
|
"description" : "",
|
||||||
|
"versionName" : "1.0.0",
|
||||||
|
"versionCode" : "100",
|
||||||
|
"transformPx" : false,
|
||||||
|
"app-plus" : {
|
||||||
|
"usingComponents" : true,
|
||||||
|
"nvueStyleCompiler" : "uni-app",
|
||||||
|
"compilerVersion" : 3,
|
||||||
|
"splashscreen" : {
|
||||||
|
"alwaysShowBeforeRender" : true,
|
||||||
|
"waiting" : true,
|
||||||
|
"autoclose" : true,
|
||||||
|
"delay" : 0
|
||||||
|
},
|
||||||
|
"modules" : {},
|
||||||
|
"distribute" : {
|
||||||
|
"android" : {
|
||||||
|
"permissions" : [
|
||||||
|
"<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.MOUNT_UNMOUNT_FILESYSTEMS\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.VIBRATE\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.READ_LOGS\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\"/>",
|
||||||
|
"<uses-feature android:name=\"android.hardware.camera.autofocus\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.CAMERA\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.GET_ACCOUNTS\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.READ_PHONE_STATE\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.CHANGE_WIFI_STATE\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>",
|
||||||
|
"<uses-feature android:name=\"android.hardware.camera\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"ios" : {},
|
||||||
|
"sdkConfigs" : {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"quickapp" : {},
|
||||||
|
"mp-weixin" : {
|
||||||
|
"appid" : "",
|
||||||
|
"setting" : {
|
||||||
|
"urlCheck" : false
|
||||||
|
},
|
||||||
|
"usingComponents" : true
|
||||||
|
},
|
||||||
|
"mp-alipay" : {
|
||||||
|
"usingComponents" : true
|
||||||
|
},
|
||||||
|
"mp-baidu" : {
|
||||||
|
"usingComponents" : true
|
||||||
|
},
|
||||||
|
"mp-toutiao" : {
|
||||||
|
"usingComponents" : true
|
||||||
|
},
|
||||||
|
"uniStatistics" : {
|
||||||
|
"enable" : false
|
||||||
|
},
|
||||||
|
"vueVersion" : "2"
|
||||||
|
}
|
||||||
+45
@@ -0,0 +1,45 @@
|
|||||||
|
{
|
||||||
|
"name": "gym-manage-coach-uniapp",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"name": "gym-manage-coach-uniapp",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"@dcloudio/uni-ui": "^1.5.12",
|
||||||
|
"crypto-js": "^4.2.0",
|
||||||
|
"luch-request": "^3.1.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@dcloudio/types": {
|
||||||
|
"version": "2.6.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/@dcloudio/types/-/types-2.6.12.tgz",
|
||||||
|
"integrity": "sha512-mrCMwcINy1IFjU9VUqLeWBkj404yWs5paLDttBcA+eqUjanuUQbBcTVPqlrGgkyzLXDcV2oDDZRSNxNpXi4kMQ==",
|
||||||
|
"license": "Apache-2.0"
|
||||||
|
},
|
||||||
|
"node_modules/@dcloudio/uni-ui": {
|
||||||
|
"version": "1.5.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/@dcloudio/uni-ui/-/uni-ui-1.5.12.tgz",
|
||||||
|
"integrity": "sha512-mGDl2OZSz7D8xcUAzJegWDHOqB4MEFBSW9Esb/oJiu2/3Gk9+P/Z4bA4JZ9jv9VWBYbMrYwaTfK1Z728kABdYg==",
|
||||||
|
"license": "Apache-2.0"
|
||||||
|
},
|
||||||
|
"node_modules/crypto-js": {
|
||||||
|
"version": "4.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz",
|
||||||
|
"integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/luch-request": {
|
||||||
|
"version": "3.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/luch-request/-/luch-request-3.1.1.tgz",
|
||||||
|
"integrity": "sha512-p7+mlcEtgRcd0OfXC4XZbyiwSr1XgCeqNT7LlVUjnk7InYl/8d5Rk7BUqAYNA2WRafI1wRIUQWRWZRpeUwWR0w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@dcloudio/types": "^2.0.16"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"name": "gym-manage-coach-uniapp",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "",
|
||||||
|
"main": "main.js",
|
||||||
|
"scripts": {
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
|
},
|
||||||
|
"keywords": [],
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"type": "commonjs",
|
||||||
|
"dependencies": {
|
||||||
|
"@dcloudio/uni-ui": "^1.5.12",
|
||||||
|
"crypto-js": "^4.2.0",
|
||||||
|
"luch-request": "^3.1.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
{
|
||||||
|
"pages": [
|
||||||
|
{
|
||||||
|
"path": "pages/login/login",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "教练登录",
|
||||||
|
"navigationStyle": "custom"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/index/index",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "教练首页",
|
||||||
|
"navigationStyle": "custom",
|
||||||
|
"enablePullDownRefresh": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/course-list/course-list",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "我的团课",
|
||||||
|
"navigationStyle": "custom",
|
||||||
|
"enablePullDownRefresh": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/course-detail/course-detail",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "课程详情",
|
||||||
|
"navigationStyle": "custom"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/profile/profile",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "个人中心",
|
||||||
|
"navigationStyle": "custom"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"globalStyle": {
|
||||||
|
"navigationBarTextStyle": "white",
|
||||||
|
"navigationBarTitleText": "Novalon教练端",
|
||||||
|
"navigationBarBackgroundColor": "#1A1A1A",
|
||||||
|
"backgroundColor": "#F5F7FA"
|
||||||
|
},
|
||||||
|
"uniIdRouter": {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,428 @@
|
|||||||
|
<template>
|
||||||
|
<view class="detail-page">
|
||||||
|
<view class="header-wrap" :style="{ paddingTop: statusBarHeight + 'px' }">
|
||||||
|
<view class="nav-bar" :style="{ height: navBarHeight + 'px' }">
|
||||||
|
<text class="back-btn" @click="goBack">←</text>
|
||||||
|
<text class="nav-title">课程详情</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 封面区 -->
|
||||||
|
<view class="cover-area">
|
||||||
|
<image v-if="course.coverImage && !loading" class="cover-img" :src="course.coverImage" mode="aspectFill"
|
||||||
|
@error="course.coverError = true" />
|
||||||
|
<view v-if="!course.coverImage || course.coverError || loading" class="cover-bg">
|
||||||
|
<text class="cover-text">{{ (course.typeName ? course.typeName.slice(0, 2) : '团课') }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="cover-status" :class="courseStatusClass" v-if="!loading">
|
||||||
|
<text>{{ courseStatusText }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<scroll-view class="content" scroll-y :style="{ height: 'calc(100vh - ' + totalHeaderHeight + 'px - 200px)' }">
|
||||||
|
<view class="content-inner" v-if="!loading">
|
||||||
|
|
||||||
|
<!-- 课程名称 -->
|
||||||
|
<view class="title-row">
|
||||||
|
<text class="detail-title">{{ course.courseName }}</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 信息卡片 -->
|
||||||
|
<view class="info-card">
|
||||||
|
<view class="info-item">
|
||||||
|
<text class="info-label">时间</text>
|
||||||
|
<text class="info-value">{{ formatDate(course.startTime) }} {{ formatTime(course.startTime) }} - {{ formatTime(course.endTime) }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="info-item">
|
||||||
|
<text class="info-label">时长</text>
|
||||||
|
<text class="info-value">{{ duration }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="info-item">
|
||||||
|
<text class="info-label">地点</text>
|
||||||
|
<text class="info-value">{{ course.location || '未设置' }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="info-item">
|
||||||
|
<text class="info-label">人数</text>
|
||||||
|
<text class="info-value">{{ course.currentMembers || 0 }} / {{ course.maxMembers || 0 }}人</text>
|
||||||
|
</view>
|
||||||
|
<view class="info-item" v-if="course.storedValueAmount">
|
||||||
|
<text class="info-label">消耗</text>
|
||||||
|
<text class="info-value price-text">¥{{ course.storedValueAmount }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 课程介绍 -->
|
||||||
|
<view class="desc-card" v-if="course.description">
|
||||||
|
<text class="desc-title">课程介绍</text>
|
||||||
|
<text class="desc-content">{{ course.description }}</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 操作区 -->
|
||||||
|
<view class="action-area">
|
||||||
|
<button
|
||||||
|
v-if="course.status == 0"
|
||||||
|
class="action-btn start-btn"
|
||||||
|
:class="{ disabled: !canStartCourse }"
|
||||||
|
:loading="actionLoading"
|
||||||
|
:disabled="actionLoading || !canStartCourse"
|
||||||
|
@click="handleStartCourse"
|
||||||
|
>
|
||||||
|
{{ actionLoading ? '开课中...' : (canStartCourse ? '开 课' : '尚未到开课时间') }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-if="course.status == 3 || course.status == 7"
|
||||||
|
class="action-btn end-btn"
|
||||||
|
:class="{ disabled: !canEndCourse }"
|
||||||
|
:loading="actionLoading"
|
||||||
|
:disabled="actionLoading || !canEndCourse"
|
||||||
|
@click="handleEndCourse"
|
||||||
|
>
|
||||||
|
{{ actionLoading ? '结课中...' : (canEndCourse ? '结 课' : '尚未到结课时间') }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-if="course.status != 0 && course.status != 3 && course.status != 7"
|
||||||
|
class="action-btn disabled-btn"
|
||||||
|
disabled
|
||||||
|
>
|
||||||
|
不可操作
|
||||||
|
</button>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="bottom-safe"></view>
|
||||||
|
</view>
|
||||||
|
<view v-else class="loading-wrap">
|
||||||
|
<text class="loading-text">加载中...</text>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const coachApi = require('../../api/coach')
|
||||||
|
const store = require('../../store/index')
|
||||||
|
const { resolveCoverUrl } = require('../../utils/request')
|
||||||
|
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
statusBarHeight: 0,
|
||||||
|
navBarHeight: 44,
|
||||||
|
totalHeaderHeight: 44,
|
||||||
|
courseId: '',
|
||||||
|
loading: true,
|
||||||
|
actionLoading: false,
|
||||||
|
course: {},
|
||||||
|
now: Date.now()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
duration() {
|
||||||
|
if (!this.course.startTime || !this.course.endTime) return '--'
|
||||||
|
const start = new Date(this.course.startTime).getTime()
|
||||||
|
const end = new Date(this.course.endTime).getTime()
|
||||||
|
const mins = Math.round((end - start) / 60000)
|
||||||
|
if (mins < 60) return mins + '分钟'
|
||||||
|
return Math.floor(mins / 60) + '小时' + (mins % 60 > 0 ? mins % 60 + '分钟' : '')
|
||||||
|
},
|
||||||
|
canStartCourse() {
|
||||||
|
return (this.course.status == 0) && this.now >= new Date(this.course.startTime || 0).getTime()
|
||||||
|
},
|
||||||
|
canEndCourse() {
|
||||||
|
return (this.course.status == 3 || this.course.status == 7) && this.now >= new Date(this.course.endTime || 0).getTime()
|
||||||
|
},
|
||||||
|
courseStatusClass() {
|
||||||
|
const map = { 0: 'normal', 1: 'cancelled', 2: 'ended', 3: 'in-progress', 4: 'timeout', 5: 'absent', 6: 'auto-ended', 7: 'late' }
|
||||||
|
return map[this.course.status] || ''
|
||||||
|
},
|
||||||
|
courseStatusText() {
|
||||||
|
const labels = { 0: '正常', 1: '已取消', 2: '已结束', 3: '进行中', 4: '超时', 5: '教练缺席', 6: '自动结束', 7: '教练迟到' }
|
||||||
|
return labels[this.course.status] || '未知'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onLoad(options) {
|
||||||
|
const systemInfo = uni.getSystemInfoSync()
|
||||||
|
const statusBarHeight = systemInfo.statusBarHeight || 20
|
||||||
|
let navBarHeight = 44
|
||||||
|
// #ifdef MP-WEIXIN
|
||||||
|
const menuButton = uni.getMenuButtonBoundingClientRect()
|
||||||
|
if (menuButton) {
|
||||||
|
navBarHeight = (menuButton.top - statusBarHeight) * 2 + menuButton.height
|
||||||
|
}
|
||||||
|
// #endif
|
||||||
|
this.statusBarHeight = statusBarHeight
|
||||||
|
this.navBarHeight = navBarHeight
|
||||||
|
this.totalHeaderHeight = statusBarHeight + navBarHeight
|
||||||
|
|
||||||
|
if (options && options.id) {
|
||||||
|
this.courseId = options.id
|
||||||
|
this.loadDetail()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onShow() {
|
||||||
|
if (!store.isLoggedIn) {
|
||||||
|
uni.reLaunch({ url: '/pages/login/login' })
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
goBack() { uni.navigateBack() },
|
||||||
|
|
||||||
|
async loadDetail() {
|
||||||
|
this.loading = true
|
||||||
|
try {
|
||||||
|
const data = await coachApi.getCourseDetail(this.courseId)
|
||||||
|
this.course = {
|
||||||
|
...(data || {}),
|
||||||
|
coverImage: resolveCoverUrl((data || {}).coverImage),
|
||||||
|
coverError: false
|
||||||
|
}
|
||||||
|
this.now = Date.now()
|
||||||
|
} catch (e) {
|
||||||
|
console.error('加载课程详情失败:', e)
|
||||||
|
uni.showToast({ title: '加载失败', icon: 'none' })
|
||||||
|
} finally {
|
||||||
|
this.loading = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async handleStartCourse() {
|
||||||
|
if (this.actionLoading) return
|
||||||
|
this.now = Date.now()
|
||||||
|
if (!this.canStartCourse) {
|
||||||
|
uni.showToast({ title: '尚未到课程预计开始时间,无法开课', icon: 'none', duration: 3000 })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.actionLoading = true
|
||||||
|
try {
|
||||||
|
await coachApi.startCourse(this.courseId)
|
||||||
|
uni.showToast({ title: '开课成功', icon: 'success' })
|
||||||
|
// 刷新课程详情
|
||||||
|
await this.loadDetail()
|
||||||
|
} catch (err) {
|
||||||
|
console.error('开课失败:', err)
|
||||||
|
let msg = '开课失败'
|
||||||
|
if (err && err.data && err.data.message) {
|
||||||
|
msg = err.data.message
|
||||||
|
} else if (err && err.message) {
|
||||||
|
msg = err.message
|
||||||
|
}
|
||||||
|
uni.showToast({ title: msg, icon: 'none', duration: 3000 })
|
||||||
|
} finally {
|
||||||
|
this.actionLoading = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async handleEndCourse() {
|
||||||
|
if (this.actionLoading) return
|
||||||
|
this.now = Date.now()
|
||||||
|
if (!this.canEndCourse) {
|
||||||
|
uni.showToast({ title: '尚未到课程预计结束时间,无法结课', icon: 'none', duration: 3000 })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.actionLoading = true
|
||||||
|
try {
|
||||||
|
await coachApi.endCourse(this.courseId)
|
||||||
|
uni.showToast({ title: '结课成功', icon: 'success' })
|
||||||
|
await this.loadDetail()
|
||||||
|
} catch (err) {
|
||||||
|
console.error('结课失败:', err)
|
||||||
|
let msg = '结课失败'
|
||||||
|
if (err && err.data && err.data.message) {
|
||||||
|
msg = err.data.message
|
||||||
|
} else if (err && err.message) {
|
||||||
|
msg = err.message
|
||||||
|
}
|
||||||
|
uni.showToast({ title: msg, icon: 'none', duration: 3000 })
|
||||||
|
} finally {
|
||||||
|
this.actionLoading = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
formatDate(timeStr) {
|
||||||
|
if (!timeStr) return '--'
|
||||||
|
return timeStr.substring(0, 10)
|
||||||
|
},
|
||||||
|
formatTime(timeStr) {
|
||||||
|
if (!timeStr) return '--'
|
||||||
|
return timeStr.substring(11, 16)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.detail-page {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: #F5F7FA;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
.header-wrap {
|
||||||
|
background: #1A1A1A;
|
||||||
|
}
|
||||||
|
.nav-bar {
|
||||||
|
padding: 0 16px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.back-btn {
|
||||||
|
font-size: 20px;
|
||||||
|
color: #FFFFFF;
|
||||||
|
margin-right: 12px;
|
||||||
|
}
|
||||||
|
.nav-title {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #FFFFFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-inner {
|
||||||
|
padding: 0 14px;
|
||||||
|
}
|
||||||
|
.loading-wrap {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 60px 0;
|
||||||
|
}
|
||||||
|
.loading-text {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #7A7E84;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 封面区 */
|
||||||
|
.cover-area { height: 200px; position: relative; overflow: hidden; }
|
||||||
|
.cover-img { width: 100%; height: 100%; }
|
||||||
|
.cover-bg { width: 100%; height: 100%; background: linear-gradient(135deg, #00C853, #00BFA5); display: flex; align-items: center; justify-content: center; }
|
||||||
|
.cover-text { font-size: 56px; font-weight: 700; color: rgba(255,255,255,0.9); }
|
||||||
|
.cover-status {
|
||||||
|
position: absolute;
|
||||||
|
top: 14px;
|
||||||
|
right: 14px;
|
||||||
|
padding: 4px 12px;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.cover-status.normal, .cover-status.in-progress, .cover-status.late {
|
||||||
|
background: rgba(255,255,255,0.25);
|
||||||
|
color: #FFFFFF;
|
||||||
|
}
|
||||||
|
.cover-status.ended, .cover-status.auto-ended {
|
||||||
|
background: rgba(255,255,255,0.2);
|
||||||
|
color: rgba(255,255,255,0.8);
|
||||||
|
}
|
||||||
|
.cover-status.cancelled, .cover-status.timeout, .cover-status.absent {
|
||||||
|
background: rgba(244,67,54,0.2);
|
||||||
|
color: #FFCDD2;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 课程名称 */
|
||||||
|
.title-row {
|
||||||
|
padding: 0 4px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
.detail-title {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #1E1E1E;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 信息卡片 */
|
||||||
|
.info-card {
|
||||||
|
background: #FFFFFF;
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 16px;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0,0,0,0.06);
|
||||||
|
}
|
||||||
|
.info-item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 10px 0;
|
||||||
|
border-bottom: 1px solid #F0F0F0;
|
||||||
|
}
|
||||||
|
.info-item:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
.info-label {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #7A7E84;
|
||||||
|
}
|
||||||
|
.info-value {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #1E1E1E;
|
||||||
|
font-weight: 500;
|
||||||
|
text-align: right;
|
||||||
|
flex: 1;
|
||||||
|
margin-left: 12px;
|
||||||
|
}
|
||||||
|
.price-text {
|
||||||
|
color: #00C853;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 课程介绍 */
|
||||||
|
.desc-card {
|
||||||
|
background: #FFFFFF;
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 16px;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0,0,0,0.06);
|
||||||
|
}
|
||||||
|
.desc-title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #1E1E1E;
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
.desc-content {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #7A7E84;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 操作区 */
|
||||||
|
.action-area {
|
||||||
|
padding: 10px 0 20px;
|
||||||
|
}
|
||||||
|
.action-btn {
|
||||||
|
width: 100%;
|
||||||
|
height: 50px;
|
||||||
|
border-radius: 40px;
|
||||||
|
font-size: 17px;
|
||||||
|
font-weight: 700;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
.action-btn::after { border: none; }
|
||||||
|
.start-btn {
|
||||||
|
background: #00E676;
|
||||||
|
color: #1A1A1A;
|
||||||
|
box-shadow: 0 4px 16px rgba(0,230,118,0.35);
|
||||||
|
}
|
||||||
|
.start-btn.disabled {
|
||||||
|
background: #E0E0E0;
|
||||||
|
color: #9E9E9E;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
.end-btn {
|
||||||
|
background: #FF9800;
|
||||||
|
color: #FFFFFF;
|
||||||
|
box-shadow: 0 4px 16px rgba(255,152,0,0.35);
|
||||||
|
}
|
||||||
|
.end-btn.disabled {
|
||||||
|
background: #E0E0E0;
|
||||||
|
color: #9E9E9E;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
.disabled-btn {
|
||||||
|
background: #E0E0E0;
|
||||||
|
color: #9E9E9E;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom-safe {
|
||||||
|
height: 30px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,292 @@
|
|||||||
|
<template>
|
||||||
|
<view class="course-list-page">
|
||||||
|
<view class="header-wrap" :style="{ paddingTop: statusBarHeight + 'px' }">
|
||||||
|
<view class="nav-bar" :style="{ height: navBarHeight + 'px' }">
|
||||||
|
<text class="back-btn" @click="goBack">←</text>
|
||||||
|
<text class="nav-title">我的团课</text>
|
||||||
|
</view>
|
||||||
|
<!-- 状态筛选 Tab -->
|
||||||
|
<view class="tab-row">
|
||||||
|
<view
|
||||||
|
v-for="(tab, idx) in tabs"
|
||||||
|
:key="idx"
|
||||||
|
class="tab-item"
|
||||||
|
:class="{ active: currentTab === tab.value }"
|
||||||
|
@click="switchTab(tab.value)"
|
||||||
|
>
|
||||||
|
<text>{{ tab.label }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<scroll-view class="content" scroll-y :style="{ height: 'calc(100vh - ' + totalHeaderHeight + 'px)' }">
|
||||||
|
<view class="content-inner">
|
||||||
|
<view v-if="loading" class="loading-wrap">
|
||||||
|
<text class="loading-text">加载中...</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view v-else-if="filteredCourses.length === 0" class="empty-wrap">
|
||||||
|
<text class="empty-text">暂无课程</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view v-else class="course-list">
|
||||||
|
<view v-for="course in filteredCourses" :key="course.id" class="course-card" :class="{ grayed: !course._isToday }" @click="goToDetail(course.id)">
|
||||||
|
<view class="card-header">
|
||||||
|
<text class="course-name">{{ course.courseName }}</text>
|
||||||
|
<view class="status-tag" :class="course._statusClass">
|
||||||
|
<text>{{ course._statusLabel }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="card-info">
|
||||||
|
<view class="info-row">
|
||||||
|
<text class="info-label">时间</text>
|
||||||
|
<text class="info-value">{{ formatDate(course.startTime) }} {{ formatTime(course.startTime) }} - {{ formatTime(course.endTime) }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="info-row">
|
||||||
|
<text class="info-label">地点</text>
|
||||||
|
<text class="info-value">{{ course.location || '未设置' }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="info-row">
|
||||||
|
<text class="info-label">人数</text>
|
||||||
|
<text class="info-value">{{ course.currentMembers || 0 }} / {{ course.maxMembers || 0 }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="bottom-safe"></view>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const coachApi = require('../../api/coach')
|
||||||
|
const store = require('../../store/index')
|
||||||
|
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
statusBarHeight: 0,
|
||||||
|
navBarHeight: 44,
|
||||||
|
totalHeaderHeight: 0,
|
||||||
|
loading: true,
|
||||||
|
courses: [],
|
||||||
|
currentTab: 'all',
|
||||||
|
tabs: [
|
||||||
|
{ label: '全部', value: 'all' },
|
||||||
|
{ label: '待开课', value: 'pending' },
|
||||||
|
{ label: '进行中', value: 'in_progress' },
|
||||||
|
{ label: '已结束', value: 'ended' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
filteredCourses() {
|
||||||
|
switch (this.currentTab) {
|
||||||
|
case 'pending':
|
||||||
|
return this.courses.filter(c => c.status == 0)
|
||||||
|
case 'in_progress':
|
||||||
|
return this.courses.filter(c => c.status == 3 || c.status == 7)
|
||||||
|
case 'ended':
|
||||||
|
return this.courses.filter(c => c.status == 2 || c.status == 6 || c.status == 1 || c.status == 4 || c.status == 5)
|
||||||
|
default:
|
||||||
|
return this.courses
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onLoad() {
|
||||||
|
const systemInfo = uni.getSystemInfoSync()
|
||||||
|
const statusBarHeight = systemInfo.statusBarHeight || 20
|
||||||
|
let navBarHeight = 44
|
||||||
|
// #ifdef MP-WEIXIN
|
||||||
|
const menuButton = uni.getMenuButtonBoundingClientRect()
|
||||||
|
if (menuButton) {
|
||||||
|
navBarHeight = (menuButton.top - statusBarHeight) * 2 + menuButton.height
|
||||||
|
}
|
||||||
|
// #endif
|
||||||
|
this.statusBarHeight = statusBarHeight
|
||||||
|
this.navBarHeight = navBarHeight
|
||||||
|
// 状态栏 + 导航栏 + tab行高度
|
||||||
|
this.totalHeaderHeight = statusBarHeight + navBarHeight + 44
|
||||||
|
},
|
||||||
|
onShow() {
|
||||||
|
if (!store.isLoggedIn) {
|
||||||
|
uni.reLaunch({ url: '/pages/login/login' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.loadCourses()
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
goBack() { uni.navigateBack() },
|
||||||
|
goToDetail(id) {
|
||||||
|
uni.navigateTo({ url: '/pages/course-detail/course-detail?id=' + id })
|
||||||
|
},
|
||||||
|
switchTab(value) { this.currentTab = value },
|
||||||
|
|
||||||
|
async loadCourses() {
|
||||||
|
this.loading = true
|
||||||
|
try {
|
||||||
|
const coachId = store.getCoachId()
|
||||||
|
if (!coachId) return
|
||||||
|
const result = await coachApi.getCoachCourses(coachId)
|
||||||
|
const list = Array.isArray(result) ? result : (result && result.content ? result.content : [])
|
||||||
|
// 预计算状态 class 和 label,uni-app 模板不支持 :class 中调用方法
|
||||||
|
const classMap = { 0: 'normal', 1: 'cancelled', 2: 'ended', 3: 'in-progress', 4: 'timeout', 5: 'absent', 6: 'auto-ended', 7: 'late' }
|
||||||
|
const labelMap = { 0: '正常', 1: '已取消', 2: '已结束', 3: '进行中', 4: '超时', 5: '教练缺席', 6: '自动结束', 7: '教练迟到' }
|
||||||
|
const todayStr = new Date().toISOString().substring(0, 10)
|
||||||
|
this.courses = list
|
||||||
|
.map(c => ({
|
||||||
|
...c,
|
||||||
|
_statusClass: classMap[c.status] || '',
|
||||||
|
_statusLabel: labelMap[c.status] || '未知',
|
||||||
|
_isToday: (c.startTime || '').substring(0, 10) === todayStr
|
||||||
|
}))
|
||||||
|
.sort((a, b) => {
|
||||||
|
if (a._isToday && !b._isToday) return -1
|
||||||
|
if (!a._isToday && b._isToday) return 1
|
||||||
|
return 0
|
||||||
|
})
|
||||||
|
} catch (e) {
|
||||||
|
console.error('加载课程失败:', e)
|
||||||
|
uni.showToast({ title: '加载课程失败', icon: 'none' })
|
||||||
|
} finally {
|
||||||
|
this.loading = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
formatDate(timeStr) {
|
||||||
|
if (!timeStr) return '--'
|
||||||
|
return timeStr.substring(0, 10)
|
||||||
|
},
|
||||||
|
formatTime(timeStr) {
|
||||||
|
if (!timeStr) return '--'
|
||||||
|
return timeStr.substring(11, 16)
|
||||||
|
},
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.course-list-page {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: #F5F7FA;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
.header-wrap {
|
||||||
|
background: #1A1A1A;
|
||||||
|
}
|
||||||
|
.nav-bar {
|
||||||
|
padding: 0 16px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.back-btn {
|
||||||
|
font-size: 20px;
|
||||||
|
color: #FFFFFF;
|
||||||
|
margin-right: 12px;
|
||||||
|
}
|
||||||
|
.nav-title {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #FFFFFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tab 筛选 */
|
||||||
|
.tab-row {
|
||||||
|
display: flex;
|
||||||
|
padding: 8px 16px 10px;
|
||||||
|
}
|
||||||
|
.tab-item {
|
||||||
|
padding: 6px 16px;
|
||||||
|
border-radius: 20px;
|
||||||
|
margin-right: 8px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: rgba(255,255,255,0.6);
|
||||||
|
background: rgba(255,255,255,0.1);
|
||||||
|
}
|
||||||
|
.tab-item.active {
|
||||||
|
color: #1A1A1A;
|
||||||
|
background: #00E676;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 内容区 */
|
||||||
|
.content-inner {
|
||||||
|
padding: 14px 14px 0;
|
||||||
|
}
|
||||||
|
.loading-wrap, .empty-wrap {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 60px 0;
|
||||||
|
}
|
||||||
|
.loading-text, .empty-text {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #7A7E84;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 课程卡片 */
|
||||||
|
.course-card {
|
||||||
|
background: #FFFFFF;
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 16px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0,0,0,0.06);
|
||||||
|
}
|
||||||
|
.card-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
.course-name {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #1E1E1E;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
.status-tag {
|
||||||
|
padding: 3px 10px;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.status-tag.normal { background: rgba(0,230,118,0.15); color: #00C853; }
|
||||||
|
.status-tag.in-progress, .status-tag.late { background: rgba(33,150,243,0.15); color: #2196F3; }
|
||||||
|
.status-tag.ended, .status-tag.auto-ended { background: rgba(158,158,158,0.15); color: #9E9E9E; }
|
||||||
|
.status-tag.cancelled { background: rgba(255,152,0,0.15); color: #FF9800; }
|
||||||
|
.status-tag.timeout { background: rgba(244,67,54,0.15); color: #F44336; }
|
||||||
|
.status-tag.absent { background: rgba(244,67,54,0.15); color: #F44336; }
|
||||||
|
|
||||||
|
.card-info {
|
||||||
|
background: #F5F7FA;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
}
|
||||||
|
.info-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
.info-row:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
.info-label {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #7A7E84;
|
||||||
|
}
|
||||||
|
.info-value {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #1E1E1E;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 操作按钮 - 已移除,仅保留按钮样式供可能复用 */
|
||||||
|
.course-card.grayed {
|
||||||
|
opacity: 0.5;
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
.bottom-safe {
|
||||||
|
height: 20px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,282 @@
|
|||||||
|
<template>
|
||||||
|
<view class="home-page">
|
||||||
|
<view class="header-wrap" :style="{ paddingTop: statusBarHeight + 'px' }">
|
||||||
|
<view class="nav-bar" :style="{ height: navBarHeight + 'px' }">
|
||||||
|
<text class="nav-title">◆ Novalon 教练端</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<scroll-view class="content" scroll-y :style="{ height: 'calc(100vh - ' + totalHeaderHeight + 'px)' }">
|
||||||
|
<view class="content-inner">
|
||||||
|
<!-- 教练欢迎卡片 -->
|
||||||
|
<view class="greeting-card">
|
||||||
|
<view class="greeting-text">
|
||||||
|
<text class="greeting-label">{{ greetingLabel }}</text>
|
||||||
|
<text class="coach-name">{{ coachName }} 教练</text>
|
||||||
|
</view>
|
||||||
|
<view class="greeting-desc">
|
||||||
|
<text>祝您今日授课顺利</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 今日统计 -->
|
||||||
|
<view class="stats-card">
|
||||||
|
<view class="stat-item">
|
||||||
|
<text class="stat-number">{{ todayStats.pending }}</text>
|
||||||
|
<text class="stat-label">待开课</text>
|
||||||
|
</view>
|
||||||
|
<view class="stat-divider"></view>
|
||||||
|
<view class="stat-item">
|
||||||
|
<text class="stat-number">{{ todayStats.inProgress }}</text>
|
||||||
|
<text class="stat-label">进行中</text>
|
||||||
|
</view>
|
||||||
|
<view class="stat-divider"></view>
|
||||||
|
<view class="stat-item">
|
||||||
|
<text class="stat-number">{{ todayStats.ended }}</text>
|
||||||
|
<text class="stat-label">已结束</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 功能入口 -->
|
||||||
|
<view class="section-header">
|
||||||
|
<text class="section-title">快捷功能</text>
|
||||||
|
</view>
|
||||||
|
<view class="entry-grid">
|
||||||
|
<view class="entry-card" @click="goToCourseList">
|
||||||
|
<view class="entry-icon-wrap green-bg">
|
||||||
|
<text class="entry-icon">☰</text>
|
||||||
|
</view>
|
||||||
|
<text class="entry-name">我的团课</text>
|
||||||
|
<text class="entry-desc">查看和管理课程</text>
|
||||||
|
</view>
|
||||||
|
<view class="entry-card" @click="goToProfile">
|
||||||
|
<view class="entry-icon-wrap dark-bg">
|
||||||
|
<text class="entry-icon">☺</text>
|
||||||
|
</view>
|
||||||
|
<text class="entry-name">个人中心</text>
|
||||||
|
<text class="entry-desc">信息与违规记录</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="bottom-safe"></view>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const store = require('../../store/index')
|
||||||
|
const coachApi = require('../../api/coach')
|
||||||
|
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
statusBarHeight: 0,
|
||||||
|
navBarHeight: 44,
|
||||||
|
totalHeaderHeight: 44,
|
||||||
|
coachName: '加载中...',
|
||||||
|
todayStats: {
|
||||||
|
pending: 0,
|
||||||
|
inProgress: 0,
|
||||||
|
ended: 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
greetingLabel() {
|
||||||
|
const hour = new Date().getHours()
|
||||||
|
if (hour < 6) return '夜深了,'
|
||||||
|
if (hour < 12) return '早上好,'
|
||||||
|
if (hour < 18) return '下午好,'
|
||||||
|
return '晚上好,'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onLoad() {
|
||||||
|
const systemInfo = uni.getSystemInfoSync()
|
||||||
|
const statusBarHeight = systemInfo.statusBarHeight || 20
|
||||||
|
let navBarHeight = 44
|
||||||
|
// #ifdef MP-WEIXIN
|
||||||
|
const menuButton = uni.getMenuButtonBoundingClientRect()
|
||||||
|
if (menuButton) {
|
||||||
|
navBarHeight = (menuButton.top - statusBarHeight) * 2 + menuButton.height
|
||||||
|
}
|
||||||
|
// #endif
|
||||||
|
this.statusBarHeight = statusBarHeight
|
||||||
|
this.navBarHeight = navBarHeight
|
||||||
|
this.totalHeaderHeight = statusBarHeight + navBarHeight
|
||||||
|
},
|
||||||
|
onShow() {
|
||||||
|
if (!store.isLoggedIn) {
|
||||||
|
uni.reLaunch({ url: '/pages/login/login' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.coachName = store.getUsername() || '教练'
|
||||||
|
this.loadStats()
|
||||||
|
},
|
||||||
|
onPullDownRefresh() {
|
||||||
|
this.loadStats().finally(() => { uni.stopPullDownRefresh() })
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
goToCourseList() {
|
||||||
|
uni.navigateTo({ url: '/pages/course-list/course-list' })
|
||||||
|
},
|
||||||
|
goToProfile() {
|
||||||
|
uni.navigateTo({ url: '/pages/profile/profile' })
|
||||||
|
},
|
||||||
|
async loadStats() {
|
||||||
|
try {
|
||||||
|
const coachId = store.getCoachId()
|
||||||
|
if (!coachId) return
|
||||||
|
const courses = await coachApi.getCoachCourses(coachId)
|
||||||
|
const list = Array.isArray(courses) ? courses : (courses && courses.content ? courses.content : [])
|
||||||
|
this.todayStats = {
|
||||||
|
pending: list.filter(c => c.status == 0).length,
|
||||||
|
inProgress: list.filter(c => c.status == 3 || c.status == 7).length,
|
||||||
|
ended: list.filter(c => c.status == 2 || c.status == 6).length
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('加载统计失败:', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.home-page {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: #F5F7FA;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
.header-wrap {
|
||||||
|
background: #1A1A1A;
|
||||||
|
}
|
||||||
|
.nav-bar {
|
||||||
|
background: #1A1A1A;
|
||||||
|
padding: 0 20px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.nav-title {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #FFFFFF;
|
||||||
|
}
|
||||||
|
.content-inner {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 欢迎卡片 */
|
||||||
|
.greeting-card {
|
||||||
|
background: #1A1A1A;
|
||||||
|
border-radius: 20px;
|
||||||
|
padding: 20px;
|
||||||
|
color: #FFFFFF;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0,0,0,0.06);
|
||||||
|
}
|
||||||
|
.greeting-text {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
}
|
||||||
|
.greeting-label {
|
||||||
|
font-size: 16px;
|
||||||
|
color: rgba(255,255,255,0.7);
|
||||||
|
}
|
||||||
|
.coach-name {
|
||||||
|
font-size: 22px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #00E676;
|
||||||
|
}
|
||||||
|
.greeting-desc {
|
||||||
|
margin-top: 6px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: rgba(255,255,255,0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 统计卡片 */
|
||||||
|
.stats-card {
|
||||||
|
background: #FFFFFF;
|
||||||
|
border-radius: 20px;
|
||||||
|
padding: 18px 20px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-around;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0,0,0,0.06);
|
||||||
|
}
|
||||||
|
.stat-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.stat-number {
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #00C853;
|
||||||
|
}
|
||||||
|
.stat-label {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #7A7E84;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
.stat-divider {
|
||||||
|
width: 1px;
|
||||||
|
background: #EEEEEE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 功能入口 */
|
||||||
|
.section-header {
|
||||||
|
margin-bottom: 14px;
|
||||||
|
}
|
||||||
|
.section-title {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #1E1E1E;
|
||||||
|
}
|
||||||
|
.entry-grid {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
.entry-card {
|
||||||
|
width: calc(50% - 6px);
|
||||||
|
background: #FFFFFF;
|
||||||
|
border-radius: 20px;
|
||||||
|
padding: 20px 16px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
box-shadow: 0 4px 12px rgba(0,0,0,0.06);
|
||||||
|
}
|
||||||
|
.entry-icon-wrap {
|
||||||
|
width: 56px;
|
||||||
|
height: 56px;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
.entry-icon-wrap.green-bg {
|
||||||
|
background: rgba(0,230,118,0.15);
|
||||||
|
}
|
||||||
|
.entry-icon-wrap.dark-bg {
|
||||||
|
background: rgba(26,26,26,0.1);
|
||||||
|
}
|
||||||
|
.entry-icon {
|
||||||
|
font-size: 24px;
|
||||||
|
color: #00C853;
|
||||||
|
}
|
||||||
|
.entry-name {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1E1E1E;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
.entry-desc {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #7A7E84;
|
||||||
|
}
|
||||||
|
.bottom-safe {
|
||||||
|
height: 20px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,217 @@
|
|||||||
|
<template>
|
||||||
|
<view class="login-page" :style="{ paddingTop: statusBarHeight + 'px' }">
|
||||||
|
<view class="brand-area">
|
||||||
|
<view class="logo-icon">
|
||||||
|
<text class="logo-symbol">◆</text>
|
||||||
|
</view>
|
||||||
|
<text class="app-name">Novalon 教练端</text>
|
||||||
|
<text class="app-slogan">高效管理 · 轻松授课</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="login-form">
|
||||||
|
<view class="input-group">
|
||||||
|
<text class="input-icon">👤</text>
|
||||||
|
<input
|
||||||
|
class="login-input"
|
||||||
|
type="text"
|
||||||
|
v-model="username"
|
||||||
|
placeholder="请输入教练账号"
|
||||||
|
placeholder-style="color: #BDBDBD"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
<view class="input-group">
|
||||||
|
<text class="input-icon">🔒</text>
|
||||||
|
<input
|
||||||
|
class="login-input"
|
||||||
|
:type="showPassword ? 'text' : 'password'"
|
||||||
|
v-model="password"
|
||||||
|
placeholder="请输入密码"
|
||||||
|
placeholder-style="color: #BDBDBD"
|
||||||
|
/>
|
||||||
|
<text class="toggle-pwd" @click="showPassword = !showPassword">
|
||||||
|
{{ showPassword ? '👁' : '👀' }}
|
||||||
|
</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<button class="login-btn" :loading="loading" :disabled="loading || !username || !password" @click="handleLogin">
|
||||||
|
{{ loading ? '登录中...' : '登 录' }}
|
||||||
|
</button>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="footer-tip">
|
||||||
|
<text>Novalon 健身房管理系统</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const coachApi = require('../../api/coach')
|
||||||
|
const store = require('../../store/index')
|
||||||
|
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
statusBarHeight: 0,
|
||||||
|
username: '',
|
||||||
|
password: '',
|
||||||
|
showPassword: false,
|
||||||
|
loading: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onLoad() {
|
||||||
|
const systemInfo = uni.getSystemInfoSync()
|
||||||
|
this.statusBarHeight = systemInfo.statusBarHeight || 20
|
||||||
|
|
||||||
|
if (store.isLoggedIn) {
|
||||||
|
uni.reLaunch({ url: '/pages/index/index' })
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async handleLogin() {
|
||||||
|
if (!this.username.trim()) {
|
||||||
|
uni.showToast({ title: '请输入教练账号', icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!this.password) {
|
||||||
|
uni.showToast({ title: '请输入密码', icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (this.loading) return
|
||||||
|
|
||||||
|
this.loading = true
|
||||||
|
try {
|
||||||
|
const res = await coachApi.login(this.username.trim(), this.password)
|
||||||
|
if (res && res.token) {
|
||||||
|
store.setLogin(res.token, {
|
||||||
|
coachId: res.userId,
|
||||||
|
username: res.username
|
||||||
|
})
|
||||||
|
uni.showToast({ title: '登录成功', icon: 'success' })
|
||||||
|
setTimeout(() => {
|
||||||
|
uni.reLaunch({ url: '/pages/index/index' })
|
||||||
|
}, 500)
|
||||||
|
} else {
|
||||||
|
uni.showToast({ title: '登录失败:账号或密码错误', icon: 'none' })
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('登录失败:', err)
|
||||||
|
let msg = '登录失败,请重试'
|
||||||
|
if (err && err.data && err.data.message) {
|
||||||
|
msg = err.data.message
|
||||||
|
} else if (err && err.message) {
|
||||||
|
msg = err.message
|
||||||
|
}
|
||||||
|
uni.showToast({ title: msg, icon: 'none' })
|
||||||
|
} finally {
|
||||||
|
this.loading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.login-page {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: #F5F7FA;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 32px;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-area {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 80px;
|
||||||
|
margin-bottom: 60px;
|
||||||
|
}
|
||||||
|
.logo-icon {
|
||||||
|
width: 80px;
|
||||||
|
height: 80px;
|
||||||
|
background: rgba(0,230,118,0.15);
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.logo-symbol {
|
||||||
|
font-size: 36px;
|
||||||
|
color: #00C853;
|
||||||
|
}
|
||||||
|
.app-name {
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #1E1E1E;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
}
|
||||||
|
.app-slogan {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #7A7E84;
|
||||||
|
margin-top: 8px;
|
||||||
|
letter-spacing: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-form {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.input-group {
|
||||||
|
width: 100%;
|
||||||
|
height: 52px;
|
||||||
|
background: #FFFFFF;
|
||||||
|
border-radius: 40px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 18px;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0,0,0,0.06);
|
||||||
|
}
|
||||||
|
.input-icon {
|
||||||
|
font-size: 18px;
|
||||||
|
margin-right: 10px;
|
||||||
|
color: #7A7E84;
|
||||||
|
}
|
||||||
|
.login-input {
|
||||||
|
flex: 1;
|
||||||
|
height: 100%;
|
||||||
|
font-size: 15px;
|
||||||
|
color: #1E1E1E;
|
||||||
|
}
|
||||||
|
.toggle-pwd {
|
||||||
|
font-size: 18px;
|
||||||
|
color: #7A7E84;
|
||||||
|
padding-left: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-btn {
|
||||||
|
width: 100%;
|
||||||
|
height: 52px;
|
||||||
|
background: #00E676;
|
||||||
|
border-radius: 40px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border: none;
|
||||||
|
padding: 0;
|
||||||
|
margin-top: 10px;
|
||||||
|
font-size: 17px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #1A1A1A;
|
||||||
|
box-shadow: 0 4px 16px rgba(0,230,118,0.35);
|
||||||
|
}
|
||||||
|
.login-btn::after { border: none; }
|
||||||
|
.login-btn[disabled] { opacity: 0.5; }
|
||||||
|
|
||||||
|
.footer-tip {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 60px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #BDBDBD;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,347 @@
|
|||||||
|
<template>
|
||||||
|
<view class="profile-page">
|
||||||
|
<view class="header-wrap" :style="{ paddingTop: statusBarHeight + 'px' }">
|
||||||
|
<view class="nav-bar" :style="{ height: navBarHeight + 'px' }">
|
||||||
|
<text class="back-btn" @click="goBack">←</text>
|
||||||
|
<text class="nav-title">个人中心</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<scroll-view class="content" scroll-y :style="{ height: 'calc(100vh - ' + totalHeaderHeight + 'px)' }">
|
||||||
|
<view class="content-inner">
|
||||||
|
<!-- 教练信息卡片 -->
|
||||||
|
<view class="user-card">
|
||||||
|
<view class="user-header">
|
||||||
|
<view class="avatar">
|
||||||
|
<text class="avatar-text">CC</text>
|
||||||
|
</view>
|
||||||
|
<view class="user-info">
|
||||||
|
<text class="user-name">{{ coachInfo.username }}</text>
|
||||||
|
<view class="user-tag">
|
||||||
|
<text>教练</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="user-details">
|
||||||
|
<view class="detail-row">
|
||||||
|
<text class="detail-label">教练 ID</text>
|
||||||
|
<text class="detail-value">{{ coachInfo.coachId }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 违规统计 -->
|
||||||
|
<view class="stats-card">
|
||||||
|
<view class="stat-item">
|
||||||
|
<text class="stat-number">{{ violations.length }}</text>
|
||||||
|
<text class="stat-label">违规次数</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 违规记录列表 -->
|
||||||
|
<view class="section-header">
|
||||||
|
<text class="section-title">违规记录</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view v-if="violations.length === 0 && !loadingViolations" class="empty-wrap">
|
||||||
|
<text class="empty-text">暂无违规记录</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view v-else class="violation-list">
|
||||||
|
<view v-for="(v, idx) in violations" :key="idx" class="violation-card">
|
||||||
|
<view class="violation-header">
|
||||||
|
<text class="violation-course">{{ v.course_name || '未知课程' }}</text>
|
||||||
|
<view class="violation-tag" :class="v._violationClass">
|
||||||
|
<text>{{ v._violationLabel }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<text class="violation-time">{{ formatViolationTime(v.violation_time) }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 退出登录 -->
|
||||||
|
<view class="logout-area">
|
||||||
|
<button class="logout-btn" @click="handleLogout">退出登录</button>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="bottom-safe"></view>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const store = require('../../store/index')
|
||||||
|
const coachApi = require('../../api/coach')
|
||||||
|
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
statusBarHeight: 0,
|
||||||
|
navBarHeight: 44,
|
||||||
|
totalHeaderHeight: 44,
|
||||||
|
coachInfo: {
|
||||||
|
username: '加载中...',
|
||||||
|
coachId: ''
|
||||||
|
},
|
||||||
|
violations: [],
|
||||||
|
loadingViolations: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onLoad() {
|
||||||
|
const systemInfo = uni.getSystemInfoSync()
|
||||||
|
const statusBarHeight = systemInfo.statusBarHeight || 20
|
||||||
|
let navBarHeight = 44
|
||||||
|
// #ifdef MP-WEIXIN
|
||||||
|
const menuButton = uni.getMenuButtonBoundingClientRect()
|
||||||
|
if (menuButton) {
|
||||||
|
navBarHeight = (menuButton.top - statusBarHeight) * 2 + menuButton.height
|
||||||
|
}
|
||||||
|
// #endif
|
||||||
|
this.statusBarHeight = statusBarHeight
|
||||||
|
this.navBarHeight = navBarHeight
|
||||||
|
this.totalHeaderHeight = statusBarHeight + navBarHeight
|
||||||
|
},
|
||||||
|
onShow() {
|
||||||
|
if (!store.isLoggedIn) {
|
||||||
|
uni.reLaunch({ url: '/pages/login/login' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.coachInfo = store.getCoachInfo() || { username: '教练', coachId: '' }
|
||||||
|
this.loadViolations()
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
goBack() { uni.navigateBack() },
|
||||||
|
|
||||||
|
async loadViolations() {
|
||||||
|
this.loadingViolations = true
|
||||||
|
try {
|
||||||
|
const coachId = store.getCoachId()
|
||||||
|
if (!coachId) return
|
||||||
|
const result = await coachApi.getCoachViolations(coachId)
|
||||||
|
const list = Array.isArray(result) ? result : (result && result.content ? result.content : [])
|
||||||
|
// 预计算违规 class 和 label,uni-app 模板不支持 :class 中调用方法
|
||||||
|
const classMap = { COACH_LATE: 'late', COACH_ABSENT: 'absent', NOT_MANUAL_END: 'no-manual-end' }
|
||||||
|
const labelMap = { COACH_LATE: '迟到', COACH_ABSENT: '缺席', NOT_MANUAL_END: '未手动结课' }
|
||||||
|
this.violations = list.map(v => ({
|
||||||
|
...v,
|
||||||
|
_violationClass: classMap[v.violation_reason] || '',
|
||||||
|
_violationLabel: labelMap[v.violation_reason] || v.violation_reason || '违规'
|
||||||
|
}))
|
||||||
|
} catch (e) {
|
||||||
|
console.error('加载违规记录失败:', e)
|
||||||
|
} finally {
|
||||||
|
this.loadingViolations = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
handleLogout() {
|
||||||
|
uni.showModal({
|
||||||
|
title: '提示',
|
||||||
|
content: '确定要退出登录吗?',
|
||||||
|
success: (res) => {
|
||||||
|
if (res.confirm) {
|
||||||
|
store.clearLogin()
|
||||||
|
uni.reLaunch({ url: '/pages/login/login' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
formatViolationTime(timeStr) {
|
||||||
|
if (!timeStr) return ''
|
||||||
|
return timeStr.replace('T', ' ').substring(0, 19)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.profile-page {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: #F5F7FA;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
.header-wrap {
|
||||||
|
background: #1A1A1A;
|
||||||
|
}
|
||||||
|
.nav-bar {
|
||||||
|
padding: 0 16px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.back-btn {
|
||||||
|
font-size: 20px;
|
||||||
|
color: #FFFFFF;
|
||||||
|
margin-right: 12px;
|
||||||
|
}
|
||||||
|
.nav-title {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #FFFFFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-inner {
|
||||||
|
padding: 16px 14px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 用户卡片 */
|
||||||
|
.user-card {
|
||||||
|
background: #FFFFFF;
|
||||||
|
border-radius: 20px;
|
||||||
|
padding: 20px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0,0,0,0.06);
|
||||||
|
}
|
||||||
|
.user-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
.avatar {
|
||||||
|
width: 56px;
|
||||||
|
height: 56px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: rgba(0,230,118,0.15);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-right: 14px;
|
||||||
|
}
|
||||||
|
.avatar-text {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #00C853;
|
||||||
|
}
|
||||||
|
.user-info {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
.user-name {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #1E1E1E;
|
||||||
|
}
|
||||||
|
.user-tag {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #00C853;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
.user-details {
|
||||||
|
background: #F5F7FA;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 14px 16px;
|
||||||
|
}
|
||||||
|
.detail-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
.detail-label {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #7A7E84;
|
||||||
|
}
|
||||||
|
.detail-value {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1E1E1E;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 违规统计 */
|
||||||
|
.stats-card {
|
||||||
|
background: #1A1A1A;
|
||||||
|
border-radius: 20px;
|
||||||
|
padding: 18px 20px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0,0,0,0.06);
|
||||||
|
}
|
||||||
|
.stat-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.stat-number {
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #F44336;
|
||||||
|
}
|
||||||
|
.stat-label {
|
||||||
|
font-size: 12px;
|
||||||
|
color: rgba(255,255,255,0.6);
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 违规记录 */
|
||||||
|
.section-header {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
.section-title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #1E1E1E;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-wrap {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 30px 0;
|
||||||
|
}
|
||||||
|
.empty-text {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #7A7E84;
|
||||||
|
}
|
||||||
|
.violation-list {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
.violation-card {
|
||||||
|
background: #FFFFFF;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0,0,0,0.06);
|
||||||
|
}
|
||||||
|
.violation-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
.violation-course {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1E1E1E;
|
||||||
|
}
|
||||||
|
.violation-tag {
|
||||||
|
padding: 3px 10px;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.violation-tag.late { background: rgba(255,152,0,0.15); color: #FF9800; }
|
||||||
|
.violation-tag.absent { background: rgba(244,67,54,0.15); color: #F44336; }
|
||||||
|
.violation-tag.no-manual-end { background: rgba(33,150,243,0.15); color: #2196F3; }
|
||||||
|
.violation-time {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #7A7E84;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 退出登录 */
|
||||||
|
.logout-area {
|
||||||
|
padding: 20px 0;
|
||||||
|
}
|
||||||
|
.logout-btn {
|
||||||
|
width: 100%;
|
||||||
|
height: 50px;
|
||||||
|
background: #FFFFFF;
|
||||||
|
color: #F44336;
|
||||||
|
border-radius: 40px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border: 2px solid #F44336;
|
||||||
|
}
|
||||||
|
.logout-btn::after { border: none; }
|
||||||
|
.bottom-safe {
|
||||||
|
height: 30px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"setting": {
|
||||||
|
"es6": true,
|
||||||
|
"postcss": true,
|
||||||
|
"minified": true,
|
||||||
|
"uglifyFileName": false,
|
||||||
|
"enhance": true,
|
||||||
|
"packNpmRelationList": [],
|
||||||
|
"babelSetting": {
|
||||||
|
"ignore": [],
|
||||||
|
"disablePlugins": [],
|
||||||
|
"outputPath": ""
|
||||||
|
},
|
||||||
|
"useCompilerPlugins": false,
|
||||||
|
"minifyWXML": true
|
||||||
|
},
|
||||||
|
"compileType": "miniprogram",
|
||||||
|
"simulatorPluginLibVersion": {},
|
||||||
|
"packOptions": {
|
||||||
|
"ignore": [],
|
||||||
|
"include": []
|
||||||
|
},
|
||||||
|
"appid": "wxe734af0ea3f96437",
|
||||||
|
"editorSetting": {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"libVersion": "3.17.0",
|
||||||
|
"projectname": "gym-manage-coach-uniapp",
|
||||||
|
"setting": {
|
||||||
|
"urlCheck": true,
|
||||||
|
"coverView": true,
|
||||||
|
"lazyloadPlaceholderEnable": false,
|
||||||
|
"skylineRenderEnable": false,
|
||||||
|
"preloadBackgroundData": false,
|
||||||
|
"autoAudits": false,
|
||||||
|
"showShadowRootInWxmlPanel": true,
|
||||||
|
"compileHotReLoad": true
|
||||||
|
}
|
||||||
|
}
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 3.9 KiB |
@@ -0,0 +1,68 @@
|
|||||||
|
/**
|
||||||
|
* 教练端全局状态管理 — 以 uni.Storage 为唯一数据源
|
||||||
|
*/
|
||||||
|
const TOKEN_KEY = 'coach_token'
|
||||||
|
const COACH_KEY = 'coach_info'
|
||||||
|
|
||||||
|
const store = {
|
||||||
|
/** 是否已登录 */
|
||||||
|
get isLoggedIn() {
|
||||||
|
try {
|
||||||
|
return !!uni.getStorageSync(TOKEN_KEY)
|
||||||
|
} catch (e) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/** 获取 token */
|
||||||
|
getToken() {
|
||||||
|
try {
|
||||||
|
return uni.getStorageSync(TOKEN_KEY) || null
|
||||||
|
} catch (e) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/** 获取教练信息 */
|
||||||
|
getCoachInfo() {
|
||||||
|
try {
|
||||||
|
return uni.getStorageSync(COACH_KEY) || null
|
||||||
|
} catch (e) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/** 获取教练 ID */
|
||||||
|
getCoachId() {
|
||||||
|
const info = this.getCoachInfo()
|
||||||
|
return info ? info.coachId : null
|
||||||
|
},
|
||||||
|
|
||||||
|
/** 获取教练用户名 */
|
||||||
|
getUsername() {
|
||||||
|
const info = this.getCoachInfo()
|
||||||
|
return info ? info.username : ''
|
||||||
|
},
|
||||||
|
|
||||||
|
/** 设置登录态 */
|
||||||
|
setLogin(token, coachInfo) {
|
||||||
|
try {
|
||||||
|
uni.setStorageSync(TOKEN_KEY, token)
|
||||||
|
if (coachInfo) uni.setStorageSync(COACH_KEY, coachInfo)
|
||||||
|
} catch (e) {
|
||||||
|
console.error('存储登录态失败:', e)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/** 清除登录态 */
|
||||||
|
clearLogin() {
|
||||||
|
try {
|
||||||
|
uni.removeStorageSync(TOKEN_KEY)
|
||||||
|
uni.removeStorageSync(COACH_KEY)
|
||||||
|
} catch (e) {
|
||||||
|
console.error('清除登录态失败:', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = store
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
uni.addInterceptor({
|
||||||
|
returnValue (res) {
|
||||||
|
if (!(!!res && (typeof res === "object" || typeof res === "function") && typeof res.then === "function")) {
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
res.then((res) => {
|
||||||
|
if (!res) return resolve(res)
|
||||||
|
return res[0] ? reject(res[0]) : resolve(res[1])
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
/**
|
||||||
|
* 这里是uni-app内置的常用样式变量
|
||||||
|
*
|
||||||
|
* uni-app 官方扩展插件及插件市场(https://ext.dcloud.net.cn)上很多三方插件均使用了这些样式变量
|
||||||
|
* 如果你是插件开发者,建议你使用scss预处理,并在插件代码中直接使用这些变量(无需 import 这个文件),方便用户通过搭积木的方式开发整体风格一致的App
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 如果你是App开发者(插件使用者),你可以通过修改这些变量来定制自己的插件主题,实现自定义主题功能
|
||||||
|
*
|
||||||
|
* 如果你的项目同样使用了scss预处理,你也可以直接在你的 scss 代码中使用如下变量,同时无需 import 这个文件
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* 颜色变量 */
|
||||||
|
|
||||||
|
/* 行为相关颜色 */
|
||||||
|
$uni-color-primary: #007aff;
|
||||||
|
$uni-color-success: #4cd964;
|
||||||
|
$uni-color-warning: #f0ad4e;
|
||||||
|
$uni-color-error: #dd524d;
|
||||||
|
|
||||||
|
/* 文字基本颜色 */
|
||||||
|
$uni-text-color:#333;//基本色
|
||||||
|
$uni-text-color-inverse:#fff;//反色
|
||||||
|
$uni-text-color-grey:#999;//辅助灰色,如加载更多的提示信息
|
||||||
|
$uni-text-color-placeholder: #808080;
|
||||||
|
$uni-text-color-disable:#c0c0c0;
|
||||||
|
|
||||||
|
/* 背景颜色 */
|
||||||
|
$uni-bg-color:#ffffff;
|
||||||
|
$uni-bg-color-grey:#f8f8f8;
|
||||||
|
$uni-bg-color-hover:#f1f1f1;//点击状态颜色
|
||||||
|
$uni-bg-color-mask:rgba(0, 0, 0, 0.4);//遮罩颜色
|
||||||
|
|
||||||
|
/* 边框颜色 */
|
||||||
|
$uni-border-color:#c8c7cc;
|
||||||
|
|
||||||
|
/* 尺寸变量 */
|
||||||
|
|
||||||
|
/* 文字尺寸 */
|
||||||
|
$uni-font-size-sm:12px;
|
||||||
|
$uni-font-size-base:14px;
|
||||||
|
$uni-font-size-lg:16px;
|
||||||
|
|
||||||
|
/* 图片尺寸 */
|
||||||
|
$uni-img-size-sm:20px;
|
||||||
|
$uni-img-size-base:26px;
|
||||||
|
$uni-img-size-lg:40px;
|
||||||
|
|
||||||
|
/* Border Radius */
|
||||||
|
$uni-border-radius-sm: 2px;
|
||||||
|
$uni-border-radius-base: 3px;
|
||||||
|
$uni-border-radius-lg: 6px;
|
||||||
|
$uni-border-radius-circle: 50%;
|
||||||
|
|
||||||
|
/* 水平间距 */
|
||||||
|
$uni-spacing-row-sm: 5px;
|
||||||
|
$uni-spacing-row-base: 10px;
|
||||||
|
$uni-spacing-row-lg: 15px;
|
||||||
|
|
||||||
|
/* 垂直间距 */
|
||||||
|
$uni-spacing-col-sm: 4px;
|
||||||
|
$uni-spacing-col-base: 8px;
|
||||||
|
$uni-spacing-col-lg: 12px;
|
||||||
|
|
||||||
|
/* 透明度 */
|
||||||
|
$uni-opacity-disabled: 0.3; // 组件禁用态的透明度
|
||||||
|
|
||||||
|
/* 文章场景相关 */
|
||||||
|
$uni-color-title: #2C405A; // 文章标题颜色
|
||||||
|
$uni-font-size-title:20px;
|
||||||
|
$uni-color-subtitle: #555555; // 二级标题颜色
|
||||||
|
$uni-font-size-subtitle:26px;
|
||||||
|
$uni-color-paragraph: #3F536E; // 文章段落颜色
|
||||||
|
$uni-font-size-paragraph:15px;
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
## 2.0.12(2025-08-26)
|
||||||
|
- 优化 uni-app x 下 size 类型问题
|
||||||
|
## 2.0.11(2025-08-18)
|
||||||
|
- 修复 图标点击事件返回
|
||||||
|
## 2.0.9(2024-01-12)
|
||||||
|
fix: 修复图标大小默认值错误的问题
|
||||||
|
## 2.0.8(2023-12-14)
|
||||||
|
- 修复 项目未使用 ts 情况下,打包报错的bug
|
||||||
|
## 2.0.7(2023-12-14)
|
||||||
|
- 修复 size 属性为 string 时,不加单位导致尺寸异常的bug
|
||||||
|
## 2.0.6(2023-12-11)
|
||||||
|
- 优化 兼容老版本icon类型,如 top ,bottom 等
|
||||||
|
## 2.0.5(2023-12-11)
|
||||||
|
- 优化 兼容老版本icon类型,如 top ,bottom 等
|
||||||
|
## 2.0.4(2023-12-06)
|
||||||
|
- 优化 uni-app x 下示例项目图标排序
|
||||||
|
## 2.0.3(2023-12-06)
|
||||||
|
- 修复 nvue下引入组件报错的bug
|
||||||
|
## 2.0.2(2023-12-05)
|
||||||
|
-优化 size 属性支持单位
|
||||||
|
## 2.0.1(2023-12-05)
|
||||||
|
- 新增 uni-app x 支持定义图标
|
||||||
|
## 1.3.5(2022-01-24)
|
||||||
|
- 优化 size 属性可以传入不带单位的字符串数值
|
||||||
|
## 1.3.4(2022-01-24)
|
||||||
|
- 优化 size 支持其他单位
|
||||||
|
## 1.3.3(2022-01-17)
|
||||||
|
- 修复 nvue 有些图标不显示的bug,兼容老版本图标
|
||||||
|
## 1.3.2(2021-12-01)
|
||||||
|
- 优化 示例可复制图标名称
|
||||||
|
## 1.3.1(2021-11-23)
|
||||||
|
- 优化 兼容旧组件 type 值
|
||||||
|
## 1.3.0(2021-11-19)
|
||||||
|
- 新增 更多图标
|
||||||
|
- 优化 自定义图标使用方式
|
||||||
|
- 优化 组件UI,并提供设计资源,详见:[https://uniapp.dcloud.io/component/uniui/resource](https://uniapp.dcloud.io/component/uniui/resource)
|
||||||
|
- 文档迁移,详见:[https://uniapp.dcloud.io/component/uniui/uni-icons](https://uniapp.dcloud.io/component/uniui/uni-icons)
|
||||||
|
## 1.1.7(2021-11-08)
|
||||||
|
## 1.2.0(2021-07-30)
|
||||||
|
- 组件兼容 vue3,如何创建vue3项目,详见 [uni-app 项目支持 vue3 介绍](https://ask.dcloud.net.cn/article/37834)
|
||||||
|
## 1.1.5(2021-05-12)
|
||||||
|
- 新增 组件示例地址
|
||||||
|
## 1.1.4(2021-02-05)
|
||||||
|
- 调整为uni_modules目录规范
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
<template>
|
||||||
|
<text class="uni-icons" :style="styleObj">
|
||||||
|
<slot>{{unicode}}</slot>
|
||||||
|
</text>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { fontData, IconsDataItem } from './uniicons_file'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Icons 图标
|
||||||
|
* @description 用于展示 icon 图标
|
||||||
|
* @tutorial https://ext.dcloud.net.cn/plugin?id=28
|
||||||
|
* @property {Number} size 图标大小
|
||||||
|
* @property {String} type 图标图案,参考示例
|
||||||
|
* @property {String} color 图标颜色
|
||||||
|
* @property {String} customPrefix 自定义图标
|
||||||
|
* @event {Function} click 点击 Icon 触发事件
|
||||||
|
*/
|
||||||
|
export default {
|
||||||
|
name: "uni-icons",
|
||||||
|
props: {
|
||||||
|
type: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
color: {
|
||||||
|
type: String,
|
||||||
|
default: '#333333'
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
type: [Number, String],
|
||||||
|
default: 16
|
||||||
|
},
|
||||||
|
fontFamily: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
unicode() : string {
|
||||||
|
let codes = fontData.find((item : IconsDataItem) : boolean => { return item.font_class == this.type })
|
||||||
|
if (codes !== null) {
|
||||||
|
return codes.unicode
|
||||||
|
}
|
||||||
|
return ''
|
||||||
|
},
|
||||||
|
iconSize() : string {
|
||||||
|
const size = this.size
|
||||||
|
if (typeof size == 'string') {
|
||||||
|
const reg = /^[0-9]*$/g
|
||||||
|
return reg.test(size as string) ? '' + size + 'px' : '' + size;
|
||||||
|
// return '' + this.size
|
||||||
|
}
|
||||||
|
return this.getFontSize(size as number)
|
||||||
|
},
|
||||||
|
styleObj() : UTSJSONObject {
|
||||||
|
if (this.fontFamily !== '') {
|
||||||
|
return { color: this.color, fontSize: this.iconSize, fontFamily: this.fontFamily }
|
||||||
|
}
|
||||||
|
return { color: this.color, fontSize: this.iconSize }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() { },
|
||||||
|
methods: {
|
||||||
|
/**
|
||||||
|
* 字体大小
|
||||||
|
*/
|
||||||
|
getFontSize(size : number) : string {
|
||||||
|
return size + 'px';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
@font-face {
|
||||||
|
font-family: UniIconsFontFamily;
|
||||||
|
src: url('./uniicons.ttf');
|
||||||
|
}
|
||||||
|
|
||||||
|
.uni-icons {
|
||||||
|
font-family: UniIconsFontFamily;
|
||||||
|
font-size: 18px;
|
||||||
|
font-style: normal;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
<template>
|
||||||
|
<!-- #ifdef APP-NVUE -->
|
||||||
|
<text :style="styleObj" class="uni-icons" @click="_onClick">{{unicode}}</text>
|
||||||
|
<!-- #endif -->
|
||||||
|
<!-- #ifndef APP-NVUE -->
|
||||||
|
<text :style="styleObj" class="uni-icons" :class="['uniui-'+type,customPrefix,customPrefix?type:'']" @click="_onClick">
|
||||||
|
<slot></slot>
|
||||||
|
</text>
|
||||||
|
<!-- #endif -->
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { fontData } from './uniicons_file_vue.js';
|
||||||
|
|
||||||
|
const getVal = (val) => {
|
||||||
|
const reg = /^[0-9]*$/g
|
||||||
|
return (typeof val === 'number' || reg.test(val)) ? val + 'px' : val;
|
||||||
|
}
|
||||||
|
|
||||||
|
// #ifdef APP-NVUE
|
||||||
|
var domModule = weex.requireModule('dom');
|
||||||
|
import iconUrl from './uniicons.ttf'
|
||||||
|
domModule.addRule('fontFace', {
|
||||||
|
'fontFamily': "uniicons",
|
||||||
|
'src': "url('" + iconUrl + "')"
|
||||||
|
});
|
||||||
|
// #endif
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Icons 图标
|
||||||
|
* @description 用于展示 icons 图标
|
||||||
|
* @tutorial https://ext.dcloud.net.cn/plugin?id=28
|
||||||
|
* @property {Number} size 图标大小
|
||||||
|
* @property {String} type 图标图案,参考示例
|
||||||
|
* @property {String} color 图标颜色
|
||||||
|
* @property {String} customPrefix 自定义图标
|
||||||
|
* @event {Function} click 点击 Icon 触发事件
|
||||||
|
*/
|
||||||
|
export default {
|
||||||
|
name: 'UniIcons',
|
||||||
|
emits: ['click'],
|
||||||
|
props: {
|
||||||
|
type: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
color: {
|
||||||
|
type: String,
|
||||||
|
default: '#333333'
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
type: [Number, String],
|
||||||
|
default: 16
|
||||||
|
},
|
||||||
|
customPrefix: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
fontFamily: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
icons: fontData
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
unicode() {
|
||||||
|
let code = this.icons.find(v => v.font_class === this.type)
|
||||||
|
if (code) {
|
||||||
|
return code.unicode
|
||||||
|
}
|
||||||
|
return ''
|
||||||
|
},
|
||||||
|
iconSize() {
|
||||||
|
return getVal(this.size)
|
||||||
|
},
|
||||||
|
styleObj() {
|
||||||
|
if (this.fontFamily !== '') {
|
||||||
|
return `color: ${this.color}; font-size: ${this.iconSize}; font-family: ${this.fontFamily};`
|
||||||
|
}
|
||||||
|
return `color: ${this.color}; font-size: ${this.iconSize};`
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
_onClick(e) {
|
||||||
|
this.$emit('click', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
/* #ifndef APP-NVUE */
|
||||||
|
@import './uniicons.css';
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: uniicons;
|
||||||
|
src: url('./uniicons.ttf');
|
||||||
|
}
|
||||||
|
|
||||||
|
/* #endif */
|
||||||
|
.uni-icons {
|
||||||
|
font-family: uniicons;
|
||||||
|
text-decoration: none;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,664 @@
|
|||||||
|
|
||||||
|
.uniui-cart-filled:before {
|
||||||
|
content: "\e6d0";
|
||||||
|
}
|
||||||
|
|
||||||
|
.uniui-gift-filled:before {
|
||||||
|
content: "\e6c4";
|
||||||
|
}
|
||||||
|
|
||||||
|
.uniui-color:before {
|
||||||
|
content: "\e6cf";
|
||||||
|
}
|
||||||
|
|
||||||
|
.uniui-wallet:before {
|
||||||
|
content: "\e6b1";
|
||||||
|
}
|
||||||
|
|
||||||
|
.uniui-settings-filled:before {
|
||||||
|
content: "\e6ce";
|
||||||
|
}
|
||||||
|
|
||||||
|
.uniui-auth-filled:before {
|
||||||
|
content: "\e6cc";
|
||||||
|
}
|
||||||
|
|
||||||
|
.uniui-shop-filled:before {
|
||||||
|
content: "\e6cd";
|
||||||
|
}
|
||||||
|
|
||||||
|
.uniui-staff-filled:before {
|
||||||
|
content: "\e6cb";
|
||||||
|
}
|
||||||
|
|
||||||
|
.uniui-vip-filled:before {
|
||||||
|
content: "\e6c6";
|
||||||
|
}
|
||||||
|
|
||||||
|
.uniui-plus-filled:before {
|
||||||
|
content: "\e6c7";
|
||||||
|
}
|
||||||
|
|
||||||
|
.uniui-folder-add-filled:before {
|
||||||
|
content: "\e6c8";
|
||||||
|
}
|
||||||
|
|
||||||
|
.uniui-color-filled:before {
|
||||||
|
content: "\e6c9";
|
||||||
|
}
|
||||||
|
|
||||||
|
.uniui-tune-filled:before {
|
||||||
|
content: "\e6ca";
|
||||||
|
}
|
||||||
|
|
||||||
|
.uniui-calendar-filled:before {
|
||||||
|
content: "\e6c0";
|
||||||
|
}
|
||||||
|
|
||||||
|
.uniui-notification-filled:before {
|
||||||
|
content: "\e6c1";
|
||||||
|
}
|
||||||
|
|
||||||
|
.uniui-wallet-filled:before {
|
||||||
|
content: "\e6c2";
|
||||||
|
}
|
||||||
|
|
||||||
|
.uniui-medal-filled:before {
|
||||||
|
content: "\e6c3";
|
||||||
|
}
|
||||||
|
|
||||||
|
.uniui-fire-filled:before {
|
||||||
|
content: "\e6c5";
|
||||||
|
}
|
||||||
|
|
||||||
|
.uniui-refreshempty:before {
|
||||||
|
content: "\e6bf";
|
||||||
|
}
|
||||||
|
|
||||||
|
.uniui-location-filled:before {
|
||||||
|
content: "\e6af";
|
||||||
|
}
|
||||||
|
|
||||||
|
.uniui-person-filled:before {
|
||||||
|
content: "\e69d";
|
||||||
|
}
|
||||||
|
|
||||||
|
.uniui-personadd-filled:before {
|
||||||
|
content: "\e698";
|
||||||
|
}
|
||||||
|
|
||||||
|
.uniui-arrowthinleft:before {
|
||||||
|
content: "\e6d2";
|
||||||
|
}
|
||||||
|
|
||||||
|
.uniui-arrowthinup:before {
|
||||||
|
content: "\e6d3";
|
||||||
|
}
|
||||||
|
|
||||||
|
.uniui-arrowthindown:before {
|
||||||
|
content: "\e6d4";
|
||||||
|
}
|
||||||
|
|
||||||
|
.uniui-back:before {
|
||||||
|
content: "\e6b9";
|
||||||
|
}
|
||||||
|
|
||||||
|
.uniui-forward:before {
|
||||||
|
content: "\e6ba";
|
||||||
|
}
|
||||||
|
|
||||||
|
.uniui-arrow-right:before {
|
||||||
|
content: "\e6bb";
|
||||||
|
}
|
||||||
|
|
||||||
|
.uniui-arrow-left:before {
|
||||||
|
content: "\e6bc";
|
||||||
|
}
|
||||||
|
|
||||||
|
.uniui-arrow-up:before {
|
||||||
|
content: "\e6bd";
|
||||||
|
}
|
||||||
|
|
||||||
|
.uniui-arrow-down:before {
|
||||||
|
content: "\e6be";
|
||||||
|
}
|
||||||
|
|
||||||
|
.uniui-arrowthinright:before {
|
||||||
|
content: "\e6d1";
|
||||||
|
}
|
||||||
|
|
||||||
|
.uniui-down:before {
|
||||||
|
content: "\e6b8";
|
||||||
|
}
|
||||||
|
|
||||||
|
.uniui-bottom:before {
|
||||||
|
content: "\e6b8";
|
||||||
|
}
|
||||||
|
|
||||||
|
.uniui-arrowright:before {
|
||||||
|
content: "\e6d5";
|
||||||
|
}
|
||||||
|
|
||||||
|
.uniui-right:before {
|
||||||
|
content: "\e6b5";
|
||||||
|
}
|
||||||
|
|
||||||
|
.uniui-up:before {
|
||||||
|
content: "\e6b6";
|
||||||
|
}
|
||||||
|
|
||||||
|
.uniui-top:before {
|
||||||
|
content: "\e6b6";
|
||||||
|
}
|
||||||
|
|
||||||
|
.uniui-left:before {
|
||||||
|
content: "\e6b7";
|
||||||
|
}
|
||||||
|
|
||||||
|
.uniui-arrowup:before {
|
||||||
|
content: "\e6d6";
|
||||||
|
}
|
||||||
|
|
||||||
|
.uniui-eye:before {
|
||||||
|
content: "\e651";
|
||||||
|
}
|
||||||
|
|
||||||
|
.uniui-eye-filled:before {
|
||||||
|
content: "\e66a";
|
||||||
|
}
|
||||||
|
|
||||||
|
.uniui-eye-slash:before {
|
||||||
|
content: "\e6b3";
|
||||||
|
}
|
||||||
|
|
||||||
|
.uniui-eye-slash-filled:before {
|
||||||
|
content: "\e6b4";
|
||||||
|
}
|
||||||
|
|
||||||
|
.uniui-info-filled:before {
|
||||||
|
content: "\e649";
|
||||||
|
}
|
||||||
|
|
||||||
|
.uniui-reload:before {
|
||||||
|
content: "\e6b2";
|
||||||
|
}
|
||||||
|
|
||||||
|
.uniui-micoff-filled:before {
|
||||||
|
content: "\e6b0";
|
||||||
|
}
|
||||||
|
|
||||||
|
.uniui-map-pin-ellipse:before {
|
||||||
|
content: "\e6ac";
|
||||||
|
}
|
||||||
|
|
||||||
|
.uniui-map-pin:before {
|
||||||
|
content: "\e6ad";
|
||||||
|
}
|
||||||
|
|
||||||
|
.uniui-location:before {
|
||||||
|
content: "\e6ae";
|
||||||
|
}
|
||||||
|
|
||||||
|
.uniui-starhalf:before {
|
||||||
|
content: "\e683";
|
||||||
|
}
|
||||||
|
|
||||||
|
.uniui-star:before {
|
||||||
|
content: "\e688";
|
||||||
|
}
|
||||||
|
|
||||||
|
.uniui-star-filled:before {
|
||||||
|
content: "\e68f";
|
||||||
|
}
|
||||||
|
|
||||||
|
.uniui-calendar:before {
|
||||||
|
content: "\e6a0";
|
||||||
|
}
|
||||||
|
|
||||||
|
.uniui-fire:before {
|
||||||
|
content: "\e6a1";
|
||||||
|
}
|
||||||
|
|
||||||
|
.uniui-medal:before {
|
||||||
|
content: "\e6a2";
|
||||||
|
}
|
||||||
|
|
||||||
|
.uniui-font:before {
|
||||||
|
content: "\e6a3";
|
||||||
|
}
|
||||||
|
|
||||||
|
.uniui-gift:before {
|
||||||
|
content: "\e6a4";
|
||||||
|
}
|
||||||
|
|
||||||
|
.uniui-link:before {
|
||||||
|
content: "\e6a5";
|
||||||
|
}
|
||||||
|
|
||||||
|
.uniui-notification:before {
|
||||||
|
content: "\e6a6";
|
||||||
|
}
|
||||||
|
|
||||||
|
.uniui-staff:before {
|
||||||
|
content: "\e6a7";
|
||||||
|
}
|
||||||
|
|
||||||
|
.uniui-vip:before {
|
||||||
|
content: "\e6a8";
|
||||||
|
}
|
||||||
|
|
||||||
|
.uniui-folder-add:before {
|
||||||
|
content: "\e6a9";
|
||||||
|
}
|
||||||
|
|
||||||
|
.uniui-tune:before {
|
||||||
|
content: "\e6aa";
|
||||||
|
}
|
||||||
|
|
||||||
|
.uniui-auth:before {
|
||||||
|
content: "\e6ab";
|
||||||
|
}
|
||||||
|
|
||||||
|
.uniui-person:before {
|
||||||
|
content: "\e699";
|
||||||
|
}
|
||||||
|
|
||||||
|
.uniui-email-filled:before {
|
||||||
|
content: "\e69a";
|
||||||
|
}
|
||||||
|
|
||||||
|
.uniui-phone-filled:before {
|
||||||
|
content: "\e69b";
|
||||||
|
}
|
||||||
|
|
||||||
|
.uniui-phone:before {
|
||||||
|
content: "\e69c";
|
||||||
|
}
|
||||||
|
|
||||||
|
.uniui-email:before {
|
||||||
|
content: "\e69e";
|
||||||
|
}
|
||||||
|
|
||||||
|
.uniui-personadd:before {
|
||||||
|
content: "\e69f";
|
||||||
|
}
|
||||||
|
|
||||||
|
.uniui-chatboxes-filled:before {
|
||||||
|
content: "\e692";
|
||||||
|
}
|
||||||
|
|
||||||
|
.uniui-contact:before {
|
||||||
|
content: "\e693";
|
||||||
|
}
|
||||||
|
|
||||||
|
.uniui-chatbubble-filled:before {
|
||||||
|
content: "\e694";
|
||||||
|
}
|
||||||
|
|
||||||
|
.uniui-contact-filled:before {
|
||||||
|
content: "\e695";
|
||||||
|
}
|
||||||
|
|
||||||
|
.uniui-chatboxes:before {
|
||||||
|
content: "\e696";
|
||||||
|
}
|
||||||
|
|
||||||
|
.uniui-chatbubble:before {
|
||||||
|
content: "\e697";
|
||||||
|
}
|
||||||
|
|
||||||
|
.uniui-upload-filled:before {
|
||||||
|
content: "\e68e";
|
||||||
|
}
|
||||||
|
|
||||||
|
.uniui-upload:before {
|
||||||
|
content: "\e690";
|
||||||
|
}
|
||||||
|
|
||||||
|
.uniui-weixin:before {
|
||||||
|
content: "\e691";
|
||||||
|
}
|
||||||
|
|
||||||
|
.uniui-compose:before {
|
||||||
|
content: "\e67f";
|
||||||
|
}
|
||||||
|
|
||||||
|
.uniui-qq:before {
|
||||||
|
content: "\e680";
|
||||||
|
}
|
||||||
|
|
||||||
|
.uniui-download-filled:before {
|
||||||
|
content: "\e681";
|
||||||
|
}
|
||||||
|
|
||||||
|
.uniui-pyq:before {
|
||||||
|
content: "\e682";
|
||||||
|
}
|
||||||
|
|
||||||
|
.uniui-sound:before {
|
||||||
|
content: "\e684";
|
||||||
|
}
|
||||||
|
|
||||||
|
.uniui-trash-filled:before {
|
||||||
|
content: "\e685";
|
||||||
|
}
|
||||||
|
|
||||||
|
.uniui-sound-filled:before {
|
||||||
|
content: "\e686";
|
||||||
|
}
|
||||||
|
|
||||||
|
.uniui-trash:before {
|
||||||
|
content: "\e687";
|
||||||
|
}
|
||||||
|
|
||||||
|
.uniui-videocam-filled:before {
|
||||||
|
content: "\e689";
|
||||||
|
}
|
||||||
|
|
||||||
|
.uniui-spinner-cycle:before {
|
||||||
|
content: "\e68a";
|
||||||
|
}
|
||||||
|
|
||||||
|
.uniui-weibo:before {
|
||||||
|
content: "\e68b";
|
||||||
|
}
|
||||||
|
|
||||||
|
.uniui-videocam:before {
|
||||||
|
content: "\e68c";
|
||||||
|
}
|
||||||
|
|
||||||
|
.uniui-download:before {
|
||||||
|
content: "\e68d";
|
||||||
|
}
|
||||||
|
|
||||||
|
.uniui-help:before {
|
||||||
|
content: "\e679";
|
||||||
|
}
|
||||||
|
|
||||||
|
.uniui-navigate-filled:before {
|
||||||
|
content: "\e67a";
|
||||||
|
}
|
||||||
|
|
||||||
|
.uniui-plusempty:before {
|
||||||
|
content: "\e67b";
|
||||||
|
}
|
||||||
|
|
||||||
|
.uniui-smallcircle:before {
|
||||||
|
content: "\e67c";
|
||||||
|
}
|
||||||
|
|
||||||
|
.uniui-minus-filled:before {
|
||||||
|
content: "\e67d";
|
||||||
|
}
|
||||||
|
|
||||||
|
.uniui-micoff:before {
|
||||||
|
content: "\e67e";
|
||||||
|
}
|
||||||
|
|
||||||
|
.uniui-closeempty:before {
|
||||||
|
content: "\e66c";
|
||||||
|
}
|
||||||
|
|
||||||
|
.uniui-clear:before {
|
||||||
|
content: "\e66d";
|
||||||
|
}
|
||||||
|
|
||||||
|
.uniui-navigate:before {
|
||||||
|
content: "\e66e";
|
||||||
|
}
|
||||||
|
|
||||||
|
.uniui-minus:before {
|
||||||
|
content: "\e66f";
|
||||||
|
}
|
||||||
|
|
||||||
|
.uniui-image:before {
|
||||||
|
content: "\e670";
|
||||||
|
}
|
||||||
|
|
||||||
|
.uniui-mic:before {
|
||||||
|
content: "\e671";
|
||||||
|
}
|
||||||
|
|
||||||
|
.uniui-paperplane:before {
|
||||||
|
content: "\e672";
|
||||||
|
}
|
||||||
|
|
||||||
|
.uniui-close:before {
|
||||||
|
content: "\e673";
|
||||||
|
}
|
||||||
|
|
||||||
|
.uniui-help-filled:before {
|
||||||
|
content: "\e674";
|
||||||
|
}
|
||||||
|
|
||||||
|
.uniui-paperplane-filled:before {
|
||||||
|
content: "\e675";
|
||||||
|
}
|
||||||
|
|
||||||
|
.uniui-plus:before {
|
||||||
|
content: "\e676";
|
||||||
|
}
|
||||||
|
|
||||||
|
.uniui-mic-filled:before {
|
||||||
|
content: "\e677";
|
||||||
|
}
|
||||||
|
|
||||||
|
.uniui-image-filled:before {
|
||||||
|
content: "\e678";
|
||||||
|
}
|
||||||
|
|
||||||
|
.uniui-locked-filled:before {
|
||||||
|
content: "\e668";
|
||||||
|
}
|
||||||
|
|
||||||
|
.uniui-info:before {
|
||||||
|
content: "\e669";
|
||||||
|
}
|
||||||
|
|
||||||
|
.uniui-locked:before {
|
||||||
|
content: "\e66b";
|
||||||
|
}
|
||||||
|
|
||||||
|
.uniui-camera-filled:before {
|
||||||
|
content: "\e658";
|
||||||
|
}
|
||||||
|
|
||||||
|
.uniui-chat-filled:before {
|
||||||
|
content: "\e659";
|
||||||
|
}
|
||||||
|
|
||||||
|
.uniui-camera:before {
|
||||||
|
content: "\e65a";
|
||||||
|
}
|
||||||
|
|
||||||
|
.uniui-circle:before {
|
||||||
|
content: "\e65b";
|
||||||
|
}
|
||||||
|
|
||||||
|
.uniui-checkmarkempty:before {
|
||||||
|
content: "\e65c";
|
||||||
|
}
|
||||||
|
|
||||||
|
.uniui-chat:before {
|
||||||
|
content: "\e65d";
|
||||||
|
}
|
||||||
|
|
||||||
|
.uniui-circle-filled:before {
|
||||||
|
content: "\e65e";
|
||||||
|
}
|
||||||
|
|
||||||
|
.uniui-flag:before {
|
||||||
|
content: "\e65f";
|
||||||
|
}
|
||||||
|
|
||||||
|
.uniui-flag-filled:before {
|
||||||
|
content: "\e660";
|
||||||
|
}
|
||||||
|
|
||||||
|
.uniui-gear-filled:before {
|
||||||
|
content: "\e661";
|
||||||
|
}
|
||||||
|
|
||||||
|
.uniui-home:before {
|
||||||
|
content: "\e662";
|
||||||
|
}
|
||||||
|
|
||||||
|
.uniui-home-filled:before {
|
||||||
|
content: "\e663";
|
||||||
|
}
|
||||||
|
|
||||||
|
.uniui-gear:before {
|
||||||
|
content: "\e664";
|
||||||
|
}
|
||||||
|
|
||||||
|
.uniui-smallcircle-filled:before {
|
||||||
|
content: "\e665";
|
||||||
|
}
|
||||||
|
|
||||||
|
.uniui-map-filled:before {
|
||||||
|
content: "\e666";
|
||||||
|
}
|
||||||
|
|
||||||
|
.uniui-map:before {
|
||||||
|
content: "\e667";
|
||||||
|
}
|
||||||
|
|
||||||
|
.uniui-refresh-filled:before {
|
||||||
|
content: "\e656";
|
||||||
|
}
|
||||||
|
|
||||||
|
.uniui-refresh:before {
|
||||||
|
content: "\e657";
|
||||||
|
}
|
||||||
|
|
||||||
|
.uniui-cloud-upload:before {
|
||||||
|
content: "\e645";
|
||||||
|
}
|
||||||
|
|
||||||
|
.uniui-cloud-download-filled:before {
|
||||||
|
content: "\e646";
|
||||||
|
}
|
||||||
|
|
||||||
|
.uniui-cloud-download:before {
|
||||||
|
content: "\e647";
|
||||||
|
}
|
||||||
|
|
||||||
|
.uniui-cloud-upload-filled:before {
|
||||||
|
content: "\e648";
|
||||||
|
}
|
||||||
|
|
||||||
|
.uniui-redo:before {
|
||||||
|
content: "\e64a";
|
||||||
|
}
|
||||||
|
|
||||||
|
.uniui-images-filled:before {
|
||||||
|
content: "\e64b";
|
||||||
|
}
|
||||||
|
|
||||||
|
.uniui-undo-filled:before {
|
||||||
|
content: "\e64c";
|
||||||
|
}
|
||||||
|
|
||||||
|
.uniui-more:before {
|
||||||
|
content: "\e64d";
|
||||||
|
}
|
||||||
|
|
||||||
|
.uniui-more-filled:before {
|
||||||
|
content: "\e64e";
|
||||||
|
}
|
||||||
|
|
||||||
|
.uniui-undo:before {
|
||||||
|
content: "\e64f";
|
||||||
|
}
|
||||||
|
|
||||||
|
.uniui-images:before {
|
||||||
|
content: "\e650";
|
||||||
|
}
|
||||||
|
|
||||||
|
.uniui-paperclip:before {
|
||||||
|
content: "\e652";
|
||||||
|
}
|
||||||
|
|
||||||
|
.uniui-settings:before {
|
||||||
|
content: "\e653";
|
||||||
|
}
|
||||||
|
|
||||||
|
.uniui-search:before {
|
||||||
|
content: "\e654";
|
||||||
|
}
|
||||||
|
|
||||||
|
.uniui-redo-filled:before {
|
||||||
|
content: "\e655";
|
||||||
|
}
|
||||||
|
|
||||||
|
.uniui-list:before {
|
||||||
|
content: "\e644";
|
||||||
|
}
|
||||||
|
|
||||||
|
.uniui-mail-open-filled:before {
|
||||||
|
content: "\e63a";
|
||||||
|
}
|
||||||
|
|
||||||
|
.uniui-hand-down-filled:before {
|
||||||
|
content: "\e63c";
|
||||||
|
}
|
||||||
|
|
||||||
|
.uniui-hand-down:before {
|
||||||
|
content: "\e63d";
|
||||||
|
}
|
||||||
|
|
||||||
|
.uniui-hand-up-filled:before {
|
||||||
|
content: "\e63e";
|
||||||
|
}
|
||||||
|
|
||||||
|
.uniui-hand-up:before {
|
||||||
|
content: "\e63f";
|
||||||
|
}
|
||||||
|
|
||||||
|
.uniui-heart-filled:before {
|
||||||
|
content: "\e641";
|
||||||
|
}
|
||||||
|
|
||||||
|
.uniui-mail-open:before {
|
||||||
|
content: "\e643";
|
||||||
|
}
|
||||||
|
|
||||||
|
.uniui-heart:before {
|
||||||
|
content: "\e639";
|
||||||
|
}
|
||||||
|
|
||||||
|
.uniui-loop:before {
|
||||||
|
content: "\e633";
|
||||||
|
}
|
||||||
|
|
||||||
|
.uniui-pulldown:before {
|
||||||
|
content: "\e632";
|
||||||
|
}
|
||||||
|
|
||||||
|
.uniui-scan:before {
|
||||||
|
content: "\e62a";
|
||||||
|
}
|
||||||
|
|
||||||
|
.uniui-bars:before {
|
||||||
|
content: "\e627";
|
||||||
|
}
|
||||||
|
|
||||||
|
.uniui-checkbox:before {
|
||||||
|
content: "\e62b";
|
||||||
|
}
|
||||||
|
|
||||||
|
.uniui-checkbox-filled:before {
|
||||||
|
content: "\e62c";
|
||||||
|
}
|
||||||
|
|
||||||
|
.uniui-shop:before {
|
||||||
|
content: "\e62f";
|
||||||
|
}
|
||||||
|
|
||||||
|
.uniui-headphones:before {
|
||||||
|
content: "\e630";
|
||||||
|
}
|
||||||
|
|
||||||
|
.uniui-cart:before {
|
||||||
|
content: "\e631";
|
||||||
|
}
|
||||||
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user