新增教练端,实现业务闭环:会员登录注册→查询团课→预约团课→扫码签到→教练端开课→记录实际开课时间→教练端结课→记录实际结课时间→后台查询数据

This commit is contained in:
2026-07-20 17:21:28 +08:00
parent df0e68469b
commit 4a4697c816
84 changed files with 6914 additions and 258 deletions
+6
View File
@@ -169,6 +169,12 @@
<version>1.0.0</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>cn.novalon.gym.manage</groupId>
<artifactId>gym-coach</artifactId>
<version>1.0.0</version>
<scope>compile</scope>
</dependency>
</dependencies>
<build>
@@ -25,7 +25,8 @@ import java.util.List;
"cn.novalon.gym.manage.member.repository",
"cn.novalon.gym.manage.groupcourse.dao",
"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")
public class ManageApplication {
@@ -20,7 +20,8 @@ import cn.novalon.gym.manage.notify.handler.BannerHandler;
import cn.novalon.gym.manage.notify.handler.SysNoticeHandler;
import cn.novalon.gym.manage.notify.handler.SysUserMessageHandler;
import cn.novalon.gym.manage.payment.handler.PaymentHandler;
import cn.novalon.gym.manage.app.handler.CoachHandler;
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.SysAuthHandler;
import cn.novalon.gym.manage.sys.handler.config.SysConfigHandler;
@@ -86,7 +87,8 @@ public class SystemRouter {
DataStatisticsHandler dataStatisticsHandler,
PhoneAuthHandler phoneAuthHandler,
PaymentHandler paymentHandler,
CoachHandler coachHandler) {
CoachHandler coachHandler,
CoachCourseHandler coachCourseHandler) {
return route()
// ========== 诊断路由 ==========
@@ -126,6 +128,10 @@ public class SystemRouter {
.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)
@@ -1,102 +0,0 @@
package cn.novalon.gym.manage.app.handler;
import cn.novalon.gym.manage.app.service.CoachService;
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 中避免模块循环依赖)
*
* @author 张翔
* @date 2026-07-19
*/
@Component
@Tag(name = "教练管理", description = "教练相关操作")
public class CoachHandler {
private static final Logger logger = LoggerFactory.getLogger(CoachHandler.class);
private final CoachService coachService;
private final Validator validator;
public CoachHandler(CoachService coachService, Validator validator) {
this.coachService = coachService;
this.validator = validator;
}
@Operation(summary = "获取所有教练", description = "获取系统中所有教练列表")
public Mono<ServerResponse> getAllCoaches(ServerRequest request) {
return ServerResponse.ok()
.body(coachService.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 coachService.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 -> coachService.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 coachService.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 coachService.getCoachCourses(id)
.collectList()
.flatMap(courses -> ServerResponse.ok().bodyValue(courses))
.switchIfEmpty(ServerResponse.ok().bodyValue(List.of()));
}
}
@@ -1,144 +0,0 @@
package cn.novalon.gym.manage.app.service;
import cn.novalon.gym.manage.common.util.StatusConstants;
import cn.novalon.gym.manage.groupcourse.domain.GroupCourse;
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.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.LocalDateTime;
/**
* 教练服务(跨模块编排,放在 manage-app 中避免模块循环依赖)
*
* @author 张翔
* @date 2026-07-19
*/
@Service
public class CoachService {
private static final Logger logger = LoggerFactory.getLogger(CoachService.class);
private static final String COACH_ROLE_NAME = "教练";
private final ISysUserRepository userRepository;
private final ISysRoleRepository roleRepository;
private final IUserRoleRepository userRoleRepository;
private final IGroupCourseRepository groupCourseRepository;
private final IGroupCourseBookingRepository bookingRepository;
private final PasswordEncoder passwordEncoder;
public CoachService(ISysUserRepository userRepository,
ISysRoleRepository roleRepository,
IUserRoleRepository userRoleRepository,
IGroupCourseRepository groupCourseRepository,
IGroupCourseBookingRepository bookingRepository,
PasswordEncoder passwordEncoder) {
this.userRepository = userRepository;
this.roleRepository = roleRepository;
this.userRoleRepository = userRoleRepository;
this.groupCourseRepository = groupCourseRepository;
this.bookingRepository = bookingRepository;
this.passwordEncoder = passwordEncoder;
}
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();
}))
);
}
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("教练角色未找到,请先执行数据库迁移")));
}
}