From cacc8997ec3ffb5b01b85f40b3c29172e2e2ad6a Mon Sep 17 00:00:00 2001 From: liwentao Date: Tue, 14 Jul 2026 18:49:15 +0800 Subject: [PATCH] =?UTF-8?q?=E7=A7=BB=E9=99=A4=E6=B5=81=E7=A8=8B=E4=B8=AD?= =?UTF-8?q?=E7=9A=84=E6=94=AF=E4=BB=98=E7=9B=B8=E5=85=B3=E5=92=8C=E4=BC=9A?= =?UTF-8?q?=E5=91=98=E5=8D=A1=E7=9B=B8=E5=85=B3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/impl/CheckServiceImpl.java | 108 +++----- gym-manage-api/gym-groupCourse/pom.xml | 9 + .../groupcourse/dao/GroupCourseDao.java | 2 + .../handler/GroupCourseBookingHandler.java | 84 ++----- .../initializer/QrCodeInitializer.java | 41 +-- .../service/IGroupCourseBookingService.java | 3 +- .../impl/GroupCourseBookingService.java | 203 +++++++-------- .../service/impl/GroupCourseService.java | 61 +++-- .../gym/manage/groupcourse/util/OSSUtil.java | 109 -------- .../manage/groupcourse/util/QRCodeUtil.java | 123 ++------- .../groupcourse/util/QRCodeUtilTest.java | 64 ++--- .../member/config/WechatProperties.java | 3 + .../service/impl/WechatApiServiceImpl.java | 25 +- .../src/main/resources/member-config.yml | 16 +- .../migration/V13__Consolidated_Test_Data.sql | 238 +++++++++--------- .../db/migration/V23__Add_Coach_Test_Data.sql | 2 +- .../V24__Add_Second_Coach_Test_Data.sql | 2 +- .../V32__Add_Statistics_Test_Data_Today.sql | 66 ----- .../V32__Make_member_card_id_nullable.sql | 8 + .../file/core/service/ISysFileService.java | 11 + .../core/service/impl/SysFileServiceImpl.java | 37 +++ .../groupcourse/GroupCourseManagement.vue | 133 +++++++++- 22 files changed, 594 insertions(+), 754 deletions(-) delete mode 100644 gym-manage-api/gym-groupCourse/src/main/java/cn/novalon/gym/manage/groupcourse/util/OSSUtil.java delete mode 100644 gym-manage-api/manage-db/src/main/resources/db/migration/V32__Add_Statistics_Test_Data_Today.sql create mode 100644 gym-manage-api/manage-db/src/main/resources/db/migration/V32__Make_member_card_id_nullable.sql diff --git a/gym-manage-api/gym-checkIn/src/main/java/cn/novalon/gym/manage/checkIn/service/impl/CheckServiceImpl.java b/gym-manage-api/gym-checkIn/src/main/java/cn/novalon/gym/manage/checkIn/service/impl/CheckServiceImpl.java index e041842..33f60b4 100644 --- a/gym-manage-api/gym-checkIn/src/main/java/cn/novalon/gym/manage/checkIn/service/impl/CheckServiceImpl.java +++ b/gym-manage-api/gym-checkIn/src/main/java/cn/novalon/gym/manage/checkIn/service/impl/CheckServiceImpl.java @@ -52,32 +52,24 @@ public class CheckServiceImpl implements ICheckInService { @Override public Mono getQRCode(Long memberId) { - log.info("开始查询会员信息, memberId: {}", memberId); + log.info("开始生成会员签到二维码, memberId: {}", memberId); - return findValidMemberCard(memberId) - .flatMap(cardRecord -> { - log.info("会员信息查询完成, memberCardRecordId: {}", cardRecord.getMemberCardRecordId()); - - log.info("开始生成二维码"); - String qrContent = QRRedisKey.generateQrcodeContent(); - Map redisMap = new HashMap<>(); - redisMap.put("qrContent", qrContent); - redisMap.put("isUsed", false); - redisMap.put("memberId", memberId); - redisMap.put("memberCardRecordId", cardRecord.getMemberCardRecordId()); + String qrContent = QRRedisKey.generateQrcodeContent(); + Map redisMap = new HashMap<>(); + redisMap.put("qrContent", qrContent); + redisMap.put("isUsed", false); + redisMap.put("memberId", memberId); - return redisUtil.setWithExpire( - RedisKeyConstants.QRCODE_USER_DAILY + memberId + LocalDate.now(), - redisMap, - getSecondsUntilEndOfDay() - ) - .then(Mono.fromSupplier(() -> { - String qrCodeBase64 = QrCodeUtil.generateAsBase64(qrContent, - BeanUtil.copyProperties(qrCodeConfig, QrConfig.class), "png"); - return new QRCodeVo(qrCodeBase64, false, qrContent, qrCodeConfig.getWidth(), qrCodeConfig.getHeight(), LocalDate.now()); - })); - }) - .switchIfEmpty(Mono.error(new RuntimeException("该会员没有可用的会员卡"))); + return redisUtil.setWithExpire( + RedisKeyConstants.QRCODE_USER_DAILY + memberId + LocalDate.now(), + redisMap, + getSecondsUntilEndOfDay() + ) + .then(Mono.fromSupplier(() -> { + String qrCodeBase64 = QrCodeUtil.generateAsBase64(qrContent, + BeanUtil.copyProperties(qrCodeConfig, QrConfig.class), "png"); + return new QRCodeVo(qrCodeBase64, false, qrContent, qrCodeConfig.getWidth(), qrCodeConfig.getHeight(), LocalDate.now()); + })); } @Override @@ -113,9 +105,10 @@ public class CheckServiceImpl implements ICheckInService { } log.info("二维码匹配成功,memberId: {}", memberId); - Long memberCardRecordId = ((Number) map.get("memberCardRecordId")).longValue(); + Long memberCardRecordId = map.get("memberCardRecordId") != null + ? ((Number) map.get("memberCardRecordId")).longValue() : null; - return processCheckIn(memberId, memberCardRecordId, map, qrContent); + return processCheckIn(memberId, memberCardRecordId, map, qrContent); } else { MyWebSocketHandler.sendFailure(qrContent, "二维码无效"); return Mono.error(new RuntimeException("二维码无效")); @@ -145,54 +138,23 @@ public class CheckServiceImpl implements ICheckInService { LocalDateTime now = LocalDateTime.now(); // 发送实时进度通知 - MyWebSocketHandler.sendProgress(qrContent, "VALIDATE_CARD", "正在验证会员卡..."); - - return memberCardRecordRepository.findById(memberCardRecordId) - .switchIfEmpty(Mono.defer(() -> { - MyWebSocketHandler.sendFailure(qrContent, "会员卡记录不存在"); - return Mono.error(new RuntimeException("会员卡记录不存在")); - })) - .flatMap(cardRecord -> { - if (!"ACTIVE".equals(cardRecord.getStatus().name())) { - MyWebSocketHandler.sendFailure(qrContent, "会员卡状态不正确"); - return Mono.error(new RuntimeException("会员卡状态不正确")); - } + MyWebSocketHandler.sendProgress(qrContent, "VALIDATE_BOOKING", "正在检查预约信息..."); + + // 检查是否有需要签到的团课预约 + return validateBooking(memberId, now) + .then(Mono.defer(() -> { + redisMap.put("isUsed", true); + redisMap.put("checkInTime", now.format(DATE_FORMATTER)); - if (cardRecord.getExpireTime() != null && cardRecord.getExpireTime().isBefore(now)) { - MyWebSocketHandler.sendFailure(qrContent, "会员卡已过期"); - return Mono.error(new RuntimeException("会员卡已过期")); - } - - // 发送实时进度通知 - MyWebSocketHandler.sendProgress(qrContent, "VALIDATE_BOOKING", "会员卡验证通过,正在检查预约信息..."); - - // 检查是否有需要签到的团课预约 - return validateBooking(memberId, now) - .then(memberCardRepository.findByMemberCardIdAndDeletedAtIsNull(cardRecord.getMemberCardId()) - .switchIfEmpty(Mono.defer(() -> { - MyWebSocketHandler.sendFailure(qrContent, "会员卡类型不存在"); - return Mono.error(new RuntimeException("会员卡类型不存在")); - })) - .flatMap(card -> { - // 发送实时进度通知 - MyWebSocketHandler.sendProgress(qrContent, "DEDUCT_USAGE", "正在扣减会员卡次数..."); - - return deductCardUsage(cardRecord, card) - .flatMap(updatedRecord -> { - redisMap.put("isUsed", true); - redisMap.put("checkInTime", now.format(DATE_FORMATTER)); - - return saveSignInRecord(memberId, cardRecord.getMemberCardRecordId(), card.getMemberCardId()) - .then(redisUtil.set(RedisKeyConstants.QRCODE_USER_DAILY + memberId + LocalDate.now(), redisMap)) - .then(Mono.defer(() -> { - String successMsg = buildSuccessResponse(now); - MyWebSocketHandler.sendSuccess(qrContent, memberId, now.format(DATE_FORMATTER)); - log.info("签到成功, memberId: {}, cardRecordId: {}", memberId, memberCardRecordId); - return Mono.just(successMsg); - })); - }); - })); - }); + return saveSignInRecord(memberId, null, null) + .then(redisUtil.set(RedisKeyConstants.QRCODE_USER_DAILY + memberId + LocalDate.now(), redisMap)) + .then(Mono.defer(() -> { + String successMsg = buildSuccessResponse(now); + MyWebSocketHandler.sendSuccess(qrContent, memberId, now.format(DATE_FORMATTER)); + log.info("签到成功, memberId: {}", memberId); + return Mono.just(successMsg); + })); + })); } /** diff --git a/gym-manage-api/gym-groupCourse/pom.xml b/gym-manage-api/gym-groupCourse/pom.xml index 12e0171..c898159 100644 --- a/gym-manage-api/gym-groupCourse/pom.xml +++ b/gym-manage-api/gym-groupCourse/pom.xml @@ -94,11 +94,20 @@ 3.5.3 + + + + + cn.novalon.gym.manage + manage-file + ${project.version} + diff --git a/gym-manage-api/gym-groupCourse/src/main/java/cn/novalon/gym/manage/groupcourse/dao/GroupCourseDao.java b/gym-manage-api/gym-groupCourse/src/main/java/cn/novalon/gym/manage/groupcourse/dao/GroupCourseDao.java index 911f869..aab2e18 100644 --- a/gym-manage-api/gym-groupCourse/src/main/java/cn/novalon/gym/manage/groupcourse/dao/GroupCourseDao.java +++ b/gym-manage-api/gym-groupCourse/src/main/java/cn/novalon/gym/manage/groupcourse/dao/GroupCourseDao.java @@ -161,6 +161,7 @@ public interface GroupCourseDao extends R2dbcRepository 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)); @@ -283,6 +284,7 @@ public interface GroupCourseDao extends R2dbcRepository 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)); diff --git a/gym-manage-api/gym-groupCourse/src/main/java/cn/novalon/gym/manage/groupcourse/handler/GroupCourseBookingHandler.java b/gym-manage-api/gym-groupCourse/src/main/java/cn/novalon/gym/manage/groupcourse/handler/GroupCourseBookingHandler.java index 954d773..19a022b 100644 --- a/gym-manage-api/gym-groupCourse/src/main/java/cn/novalon/gym/manage/groupcourse/handler/GroupCourseBookingHandler.java +++ b/gym-manage-api/gym-groupCourse/src/main/java/cn/novalon/gym/manage/groupcourse/handler/GroupCourseBookingHandler.java @@ -2,7 +2,6 @@ package cn.novalon.gym.manage.groupcourse.handler; import cn.novalon.gym.manage.groupcourse.domain.GroupCourseBooking; import cn.novalon.gym.manage.groupcourse.service.IGroupCourseBookingService; -import cn.novalon.gym.manage.member.service.IMemberStoredCardService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import org.springframework.stereotype.Component; @@ -24,12 +23,9 @@ import java.util.Map; public class GroupCourseBookingHandler { private final IGroupCourseBookingService bookingService; - private final IMemberStoredCardService memberStoredCardService; - public GroupCourseBookingHandler(IGroupCourseBookingService bookingService, - IMemberStoredCardService memberStoredCardService) { + public GroupCourseBookingHandler(IGroupCourseBookingService bookingService) { this.bookingService = bookingService; - this.memberStoredCardService = memberStoredCardService; } /** @@ -46,38 +42,23 @@ public class GroupCourseBookingHandler { if (body.get("memberId") == null) { return buildErrorResponse("请提供会员ID"); } - if (body.get("memberCardRecordId") == null) { - return buildErrorResponse("请提供会员卡记录ID"); - } - if (body.get("payPassword") == null || body.get("payPassword").toString().isEmpty()) { - return buildErrorResponse("请输入支付密码"); - } Long courseId = toLong(body.get("courseId"), "courseId"); Long memberId = toLong(body.get("memberId"), "memberId"); - Long memberCardRecordId = toLong(body.get("memberCardRecordId"), "memberCardRecordId"); - String payPassword = body.get("payPassword").toString(); - // 验证支付密码 - return memberStoredCardService.verifyPayPassword(memberId, payPassword) - .flatMap(passwordValid -> { - if (!passwordValid) { - return buildErrorResponse("支付密码错误"); - } - return bookingService.bookCourse(courseId, memberId, memberCardRecordId) - .flatMap(booking -> { - Map response = new HashMap<>(); - response.put("success", true); - response.put("message", "预约成功"); - response.put("data", booking); - return ServerResponse.ok().bodyValue(response); - }) - .onErrorResume(error -> { - Map response = new HashMap<>(); - response.put("success", false); - response.put("message", error.getMessage()); - return ServerResponse.badRequest().bodyValue(response); - }); + return bookingService.bookCourse(courseId, memberId) + .flatMap(booking -> { + Map response = new HashMap<>(); + response.put("success", true); + response.put("message", "预约成功"); + response.put("data", booking); + return ServerResponse.ok().bodyValue(response); + }) + .onErrorResume(error -> { + Map response = new HashMap<>(); + response.put("success", false); + response.put("message", error.getMessage()); + return ServerResponse.badRequest().bodyValue(response); }); }); } @@ -92,31 +73,20 @@ public class GroupCourseBookingHandler { return request.bodyToMono(Map.class) .flatMap(body -> { Long memberId = toLong(body.get("memberId"), "memberId"); - if (body.get("payPassword") == null || body.get("payPassword").toString().isEmpty()) { - return buildErrorResponse("请输入支付密码"); - } - String payPassword = body.get("payPassword").toString(); - // 验证支付密码 - return memberStoredCardService.verifyPayPassword(memberId, payPassword) - .flatMap(passwordValid -> { - if (!passwordValid) { - return buildErrorResponse("支付密码错误"); - } - return bookingService.cancelBooking(bookingId, memberId) - .flatMap(booking -> { - Map response = new HashMap<>(); - response.put("success", true); - response.put("message", "取消成功"); - response.put("data", booking); - return ServerResponse.ok().bodyValue(response); - }) - .onErrorResume(error -> { - Map response = new HashMap<>(); - response.put("success", false); - response.put("message", error.getMessage()); - return ServerResponse.badRequest().bodyValue(response); - }); + return bookingService.cancelBooking(bookingId, memberId) + .flatMap(booking -> { + Map response = new HashMap<>(); + response.put("success", true); + response.put("message", "取消成功"); + response.put("data", booking); + return ServerResponse.ok().bodyValue(response); + }) + .onErrorResume(error -> { + Map response = new HashMap<>(); + response.put("success", false); + response.put("message", error.getMessage()); + return ServerResponse.badRequest().bodyValue(response); }); }); } diff --git a/gym-manage-api/gym-groupCourse/src/main/java/cn/novalon/gym/manage/groupcourse/initializer/QrCodeInitializer.java b/gym-manage-api/gym-groupCourse/src/main/java/cn/novalon/gym/manage/groupcourse/initializer/QrCodeInitializer.java index 772309f..1c64492 100644 --- a/gym-manage-api/gym-groupCourse/src/main/java/cn/novalon/gym/manage/groupcourse/initializer/QrCodeInitializer.java +++ b/gym-manage-api/gym-groupCourse/src/main/java/cn/novalon/gym/manage/groupcourse/initializer/QrCodeInitializer.java @@ -1,5 +1,6 @@ package cn.novalon.gym.manage.groupcourse.initializer; +import cn.novalon.gym.manage.file.core.service.ISysFileService; import cn.novalon.gym.manage.groupcourse.domain.GroupCourse; import cn.novalon.gym.manage.groupcourse.repository.IGroupCourseRepository; import cn.novalon.gym.manage.groupcourse.util.QRCodeUtil; @@ -10,15 +11,12 @@ import org.springframework.boot.CommandLineRunner; import org.springframework.stereotype.Component; import reactor.core.publisher.Mono; -import java.time.LocalDateTime; -import java.time.format.DateTimeFormatter; import java.util.HashMap; import java.util.Map; -import java.util.UUID; /** * 项目启动时补全缺失的团课二维码 - * 遍历所有未删除的团课,对qrCodePath为空的课程生成二维码并上传至阿里云OSS + * 遍历所有未删除的团课,对qrCodePath为空的课程生成二维码并通过统一文件服务保存 */ @Component public class QrCodeInitializer implements CommandLineRunner { @@ -27,11 +25,14 @@ public class QrCodeInitializer implements CommandLineRunner { private final IGroupCourseRepository groupCourseRepository; private final ObjectMapper objectMapper; + private final ISysFileService fileService; public QrCodeInitializer(IGroupCourseRepository groupCourseRepository, - ObjectMapper objectMapper) { + ObjectMapper objectMapper, + ISysFileService fileService) { this.groupCourseRepository = groupCourseRepository; this.objectMapper = objectMapper; + this.fileService = fileService; } @Override @@ -58,20 +59,22 @@ public class QrCodeInitializer implements CommandLineRunner { String jsonContent = objectMapper.writeValueAsString(qrCodeContent); - // 生成二维码并上传到阿里云OSS - String uuid = UUID.randomUUID().toString().replace("-", ""); - String timestamp = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss")); - String fileName = "qr_" + uuid + "_" + timestamp + ".png"; - String ossUrl = QRCodeUtil.generateQRCodeAndUploadToOSS(jsonContent, fileName); - - course.setQrCodePath(ossUrl); - - // 更新数据库 - return groupCourseRepository.update(course) - .doOnSuccess(updated -> logger.info("团课二维码补全成功 - id={}, name={}, ossUrl={}", - updated.getId(), updated.getCourseName(), ossUrl)) - .doOnError(error -> logger.error("团课二维码补全失败(更新DB) - id={}, name={}, error: {}", - course.getId(), course.getCourseName(), error.getMessage())); + // 生成二维码字节数组,通过统一文件服务保存 + return Mono.fromCallable(() -> QRCodeUtil.generateQrCodeBytes(jsonContent)) + .flatMap(qrCodeBytes -> { + String fileName = "qrcode_" + course.getId() + ".png"; + return fileService.saveBytes(qrCodeBytes, fileName, "image/png", "system"); + }) + .flatMap(sysFile -> { + String qrCodeUrl = "/api/files/" + sysFile.getId() + "/preview"; + course.setQrCodePath(qrCodeUrl); + + return groupCourseRepository.update(course) + .doOnSuccess(updated -> logger.info("团课二维码补全成功 - id={}, name={}, url={}", + updated.getId(), updated.getCourseName(), qrCodeUrl)) + .doOnError(error -> logger.error("团课二维码补全失败(更新DB) - id={}, name={}, error: {}", + course.getId(), course.getCourseName(), error.getMessage())); + }); } catch (Exception e) { logger.error("团课二维码补全失败(生成) - id={}, name={}, error: {}", course.getId(), course.getCourseName(), e.getMessage(), e); diff --git a/gym-manage-api/gym-groupCourse/src/main/java/cn/novalon/gym/manage/groupcourse/service/IGroupCourseBookingService.java b/gym-manage-api/gym-groupCourse/src/main/java/cn/novalon/gym/manage/groupcourse/service/IGroupCourseBookingService.java index ce1d068..8c643bb 100644 --- a/gym-manage-api/gym-groupCourse/src/main/java/cn/novalon/gym/manage/groupcourse/service/IGroupCourseBookingService.java +++ b/gym-manage-api/gym-groupCourse/src/main/java/cn/novalon/gym/manage/groupcourse/service/IGroupCourseBookingService.java @@ -17,10 +17,9 @@ public interface IGroupCourseBookingService { * * @param courseId 团课ID * @param memberId 会员ID - * @param memberCardRecordId 会员卡记录ID * @return 预约记录 */ - Mono bookCourse(Long courseId, Long memberId, Long memberCardRecordId); + Mono bookCourse(Long courseId, Long memberId); /** * 取消预约 diff --git a/gym-manage-api/gym-groupCourse/src/main/java/cn/novalon/gym/manage/groupcourse/service/impl/GroupCourseBookingService.java b/gym-manage-api/gym-groupCourse/src/main/java/cn/novalon/gym/manage/groupcourse/service/impl/GroupCourseBookingService.java index d8cb0f7..6eb5bbe 100644 --- a/gym-manage-api/gym-groupCourse/src/main/java/cn/novalon/gym/manage/groupcourse/service/impl/GroupCourseBookingService.java +++ b/gym-manage-api/gym-groupCourse/src/main/java/cn/novalon/gym/manage/groupcourse/service/impl/GroupCourseBookingService.java @@ -3,7 +3,6 @@ package cn.novalon.gym.manage.groupcourse.service.impl; import cn.novalon.gym.manage.groupcourse.domain.GroupCourse; import cn.novalon.gym.manage.groupcourse.domain.GroupCourseBooking; import cn.novalon.gym.manage.groupcourse.event.BookingReminderEventPublisher; -import cn.novalon.gym.manage.groupcourse.handler.BookingSagaHandler; import cn.novalon.gym.manage.groupcourse.repository.IGroupCourseBookingRepository; import cn.novalon.gym.manage.groupcourse.repository.IGroupCourseRepository; import cn.novalon.gym.manage.groupcourse.service.IGroupCourseBookingService; @@ -13,7 +12,6 @@ import org.springframework.stereotype.Service; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; -import java.math.BigDecimal; import java.time.LocalDateTime; import java.time.temporal.ChronoUnit; import java.util.UUID; @@ -26,7 +24,6 @@ import java.util.UUID; * - 取消预约需在课程开始前至少2小时 * - 每节课最多20人 * - 预约成功后发送提醒 - * - 预约成功后扣减权益 * * 技术要点: * - 使用Redis缓存团课信息 @@ -45,7 +42,6 @@ public class GroupCourseBookingService implements IGroupCourseBookingService { private final IGroupCourseRepository courseRepository; private final GroupCourseRedisService redisService; private final BookingReminderEventPublisher bookingReminderEventPublisher; - private final BookingSagaHandler bookingSagaHandler; // 预约提前时间限制(分钟) private static final long BOOKING_MIN_ADVANCE_MINUTES = 30; @@ -55,18 +51,16 @@ public class GroupCourseBookingService implements IGroupCourseBookingService { public GroupCourseBookingService(IGroupCourseBookingRepository bookingRepository, IGroupCourseRepository courseRepository, GroupCourseRedisService redisService, - BookingReminderEventPublisher bookingReminderEventPublisher, - BookingSagaHandler bookingSagaHandler) { + BookingReminderEventPublisher bookingReminderEventPublisher) { this.bookingRepository = bookingRepository; this.courseRepository = courseRepository; this.redisService = redisService; this.bookingReminderEventPublisher = bookingReminderEventPublisher; - this.bookingSagaHandler = bookingSagaHandler; } @Override - public Mono bookCourse(Long courseId, Long memberId, Long memberCardRecordId) { - logger.info("开始预约团课:courseId={}, memberId={}, memberCardRecordId={}", courseId, memberId, memberCardRecordId); + public Mono bookCourse(Long courseId, Long memberId) { + logger.info("开始预约团课:courseId={}, memberId={}", courseId, memberId); // 生成唯一请求ID用于分布式锁 String requestId = UUID.randomUUID().toString(); @@ -82,14 +76,14 @@ public class GroupCourseBookingService implements IGroupCourseBookingService { return getCourseWithCache(courseId) .flatMap(course -> { // 3. 验证课程状态 - Long status = course.getStatus(); - if (status == null || status != 0L) { + Long courseStatus = course.getStatus(); + if (courseStatus == null || courseStatus != 0L) { String errorMessage; - if (status == null) { + if (courseStatus == null) { errorMessage = "课程状态异常"; - } else if (status == 1L) { + } else if (courseStatus == 1L) { errorMessage = "课程已取消,无法预约"; - } else if (status == 2L) { + } else if (courseStatus == 2L) { errorMessage = "课程已结束,无法预约"; } else { errorMessage = "课程状态不可预约"; @@ -132,57 +126,61 @@ public class GroupCourseBookingService implements IGroupCourseBookingService { // 7. 验证是否已预约 return bookingRepository.findValidBooking(courseId, memberId) - .flatMap(existingBooking -> { - return releaseLockAndError(courseId, requestId, "您已预约该课程"); - }) + .flatMap(existingBooking -> + releaseLockAndError(courseId, requestId, "您已预约该课程") + ) .switchIfEmpty( - // 8. 使用Redis原子操作验证课程人数是否已满 - validateAndIncrementBookingCount(courseId, course.getMaxMembers()) - .flatMap(countValid -> { - if (countValid > course.getMaxMembers()) { - return releaseLockAndError(courseId, requestId, "课程已满"); - } + // 8. 创建预约记录 + Mono.defer(() -> { + GroupCourseBooking booking = new GroupCourseBooking(); + booking.setCourseId(courseId); + booking.setMemberId(memberId); + booking.setBookingTime(LocalDateTime.now()); + booking.setStatus("0"); // 0-已预约 - // 9. 创建预约记录 - GroupCourseBooking booking = new GroupCourseBooking(); - booking.setCourseId(courseId); - booking.setMemberId(memberId); - booking.setMemberCardRecordId(memberCardRecordId); - booking.setBookingTime(LocalDateTime.now()); - booking.setStatus("0"); // 0-已预约 + // 添加课程信息到预约记录 + booking.setCourseName(course.getCourseName()); + booking.setCourseStartTime(course.getStartTime()); + booking.setCourseEndTime(course.getEndTime()); + booking.setLocation(course.getLocation()); - // 添加课程信息到预约记录 - booking.setCourseName(course.getCourseName()); - booking.setCourseStartTime(course.getStartTime()); - booking.setCourseEndTime(course.getEndTime()); - booking.setLocation(course.getLocation()); - - // 10. 使用Saga事务执行预约(包含权益扣减) - BigDecimal courseAmount = course.getStoredValueAmount() != null ? course.getStoredValueAmount() : BigDecimal.ZERO; - return bookingSagaHandler.executeBooking(booking, memberCardRecordId, courseAmount) - .flatMap(savedBooking -> { - // 11. 释放锁 - return redisService.releaseLock(courseId, requestId) - .then(Mono.just(savedBooking)); - }) - .doOnSuccess(savedBooking -> { - logger.info("预约成功:bookingId={}, courseId={}, memberId={}", - savedBooking.getId(), courseId, memberId); - // 发布预约成功事件 - bookingReminderEventPublisher.publishBookingSuccessEvent( - savedBooking.getId(), - savedBooking.getMemberId(), - savedBooking.getCourseName(), - savedBooking.getCourseStartTime().toString() - ); - }) - .doOnError(error -> { - // 回滚Redis计数 - redisService.decrementBookingCount(courseId).subscribe(); - logger.error("预约失败:courseId={}, memberId={}, error={}", - courseId, memberId, error.getMessage()); - }); - }) + // 9. 保存预约记录 + return bookingRepository.save(booking) + .flatMap(saved -> { + if (saved.getId() == null) { + return Mono.error(new RuntimeException("保存预约记录失败")); + } + // 10. 更新课程当前人数 + return courseRepository.updateCurrentMembers(courseId, 1) + .flatMap(updatedCourse -> { + // 11. 更新Redis预约计数 + return redisService.incrementBookingCount(courseId) + .flatMap(count -> { + // 12. 释放锁 + return redisService.releaseLock(courseId, requestId) + .then(Mono.just(saved)); + }); + }) + .doOnSuccess(savedBooking -> { + logger.info("预约成功:bookingId={}, courseId={}, memberId={}", + saved.getId(), courseId, memberId); + // 发布预约成功事件 + bookingReminderEventPublisher.publishBookingSuccessEvent( + saved.getId(), + saved.getMemberId(), + saved.getCourseName(), + saved.getCourseStartTime().toString() + ); + }) + .doOnError(error -> { + // 失败时回滚Redis计数和课程人数 + redisService.decrementBookingCount(courseId).subscribe(); + courseRepository.updateCurrentMembers(courseId, -1).subscribe(); + logger.error("预约失败:courseId={}, memberId={}, error={}", + courseId, memberId, error.getMessage()); + }); + }); + }) ); }); }) @@ -210,29 +208,6 @@ public class GroupCourseBookingService implements IGroupCourseBookingService { ); } - /** - * 验证并增加预约人数(使用Redis原子操作) - */ - private Mono validateAndIncrementBookingCount(Long courseId, Integer maxMembers) { - // 先获取当前Redis中的预约计数 - return redisService.getBookingCount(courseId) - .flatMap(currentCount -> { - // 如果Redis中计数为0,可能是首次访问,需要从数据库同步 - if (currentCount == 0) { - // 从数据库查询实际预约人数 - return bookingRepository.countValidBookings(courseId) - .flatMap(dbCount -> { - // 将数据库中的实际预约人数同步到Redis - return redisService.setBookingCount(courseId, dbCount.intValue()) - .then(Mono.just(dbCount.intValue())); - }); - } - return Mono.just(currentCount); - }) - // 递增预约计数 - .flatMap(count -> redisService.incrementBookingCount(courseId).map(Long::intValue)); - } - /** * 释放锁并返回错误 */ @@ -292,33 +267,37 @@ public class GroupCourseBookingService implements IGroupCourseBookingService { "需在课程开始前" + CANCEL_MIN_ADVANCE_HOURS + "小时取消"); } - // 5. 查询课程金额和已取消次数,使用Saga事务执行取消预约 - return courseRepository.findByIdAndDeletedAtIsNull(booking.getCourseId()) - .flatMap(course -> { - BigDecimal courseAmount = course.getStoredValueAmount() != null ? course.getStoredValueAmount() : BigDecimal.valueOf(50); - return bookingRepository.countCancelledByMemberId(memberId) - .flatMap(cancelCount -> { - return bookingSagaHandler.executeCancelBooking(bookingId, booking.getCourseId(), booking.getMemberCardRecordId(), memberId, courseAmount, cancelCount); - }); - }) - .flatMap(updatedBooking -> { - // 6. 释放锁 - return redisService.releaseLock(bookingId, requestId) - .then(Mono.just(updatedBooking)); - }) - .doOnSuccess(updatedBooking -> { - logger.info("取消预约成功:bookingId={}, memberId={}", bookingId, memberId); - // 发布预约取消事件 - bookingReminderEventPublisher.publishBookingCancelEvent( - updatedBooking.getId(), - updatedBooking.getMemberId(), - updatedBooking.getCourseName() - ); - }) - .doOnError(error -> { - logger.error("取消预约失败:bookingId={}, memberId={}, error={}", - bookingId, memberId, error.getMessage()); - }); + // 5. 更新预约状态为已取消 + return bookingRepository.updateStatus(bookingId, "1") + .flatMap(rows -> { + if (rows == 0) { + return Mono.error(new RuntimeException("更新预约状态失败")); + } + // 6. 减少课程当前人数 + return courseRepository.updateCurrentMembers(booking.getCourseId(), -1) + .flatMap(updatedCourse -> { + // 7. 更新Redis预约计数 + return redisService.decrementBookingCount(booking.getCourseId()) + .flatMap(count -> { + // 8. 释放锁 + return redisService.releaseLock(bookingId, requestId) + .then(bookingRepository.findById(bookingId)); + }); + }) + .doOnSuccess(updatedBooking -> { + logger.info("取消预约成功:bookingId={}, memberId={}", bookingId, memberId); + // 发布预约取消事件 + bookingReminderEventPublisher.publishBookingCancelEvent( + updatedBooking.getId(), + updatedBooking.getMemberId(), + updatedBooking.getCourseName() + ); + }) + .doOnError(error -> { + logger.error("取消预约失败:bookingId={}, memberId={}, error={}", + bookingId, memberId, error.getMessage()); + }); + }); }) .onErrorResume(error -> { redisService.releaseLock(bookingId, requestId).subscribe(); diff --git a/gym-manage-api/gym-groupCourse/src/main/java/cn/novalon/gym/manage/groupcourse/service/impl/GroupCourseService.java b/gym-manage-api/gym-groupCourse/src/main/java/cn/novalon/gym/manage/groupcourse/service/impl/GroupCourseService.java index eb14b37..a2a44ef 100644 --- a/gym-manage-api/gym-groupCourse/src/main/java/cn/novalon/gym/manage/groupcourse/service/impl/GroupCourseService.java +++ b/gym-manage-api/gym-groupCourse/src/main/java/cn/novalon/gym/manage/groupcourse/service/impl/GroupCourseService.java @@ -19,6 +19,7 @@ import cn.novalon.gym.manage.groupcourse.repository.IGroupCourseRepository; import cn.novalon.gym.manage.groupcourse.repository.IGroupCourseTypeRepository; import cn.novalon.gym.manage.groupcourse.service.IGroupCourseService; import cn.novalon.gym.manage.groupcourse.util.QRCodeUtil; +import cn.novalon.gym.manage.file.core.service.ISysFileService; import cn.novalon.gym.manage.member.entity.MemberCard; import cn.novalon.gym.manage.member.entity.MemberCardRecord; import cn.novalon.gym.manage.member.enums.MemberCardType; @@ -51,6 +52,7 @@ public class GroupCourseService implements IGroupCourseService { private final ObjectMapper objectMapper; private final GroupCourseStateMachine stateMachine; private final DatabaseClient databaseClient; + private final ISysFileService fileService; private static final String CACHE_KEY_PREFIX = "group_course:page:"; private static final String CACHE_KEY_ID_PREFIX = "group_course:id:"; @@ -68,7 +70,8 @@ public class GroupCourseService implements IGroupCourseService { RedisUtil redisUtil, ObjectMapper objectMapper, GroupCourseStateMachine stateMachine, - DatabaseClient databaseClient){ + DatabaseClient databaseClient, + ISysFileService fileService){ this.groupCourseRepository = groupCourseRepository; this.bookingRepository = bookingRepository; this.groupCourseTypeRepository = groupCourseTypeRepository; @@ -79,6 +82,7 @@ public class GroupCourseService implements IGroupCourseService { this.objectMapper = objectMapper; this.stateMachine = stateMachine; this.databaseClient = databaseClient; + this.fileService = fileService; } @Override @@ -288,15 +292,21 @@ public class GroupCourseService implements IGroupCourseService { String jsonContent = objectMapper.writeValueAsString(qrCodeContent); - // 生成二维码并上传到阿里云OSS - String qrCodeUrl = QRCodeUtil.generateQRCodeAndUploadToOSS(jsonContent); - course.setQrCodePath(qrCodeUrl); - - logger.info("团课二维码上传到OSS成功 - id={}, qrCodeUrl={}", course.getId(), qrCodeUrl); - - // 更新团课信息,保存二维码路径 - return groupCourseRepository.update(course) - .doOnSuccess(updatedCourse -> logger.info("团课创建成功 - id={}, name={}", updatedCourse.getId(), updatedCourse.getCourseName())); + // 生成二维码字节数组,通过统一文件服务保存 + return Mono.fromCallable(() -> QRCodeUtil.generateQrCodeBytes(jsonContent)) + .flatMap(qrCodeBytes -> { + String fileName = "qrcode_" + course.getId() + ".png"; + return fileService.saveBytes(qrCodeBytes, fileName, "image/png", "system"); + }) + .flatMap(sysFile -> { + String qrCodeUrl = "/api/files/" + sysFile.getId() + "/preview"; + course.setQrCodePath(qrCodeUrl); + + logger.info("团课二维码已保存 - id={}, fileId={}, url={}", course.getId(), sysFile.getId(), qrCodeUrl); + + return groupCourseRepository.update(course) + .doOnSuccess(updatedCourse -> logger.info("团课创建成功 - id={}, name={}", updatedCourse.getId(), updatedCourse.getCourseName())); + }); } catch (Exception e) { logger.error("团课二维码生成失败 - id={}, error: {}", course.getId(), e.getMessage(), e); // 即使二维码生成失败,也返回成功创建的团课 @@ -504,29 +514,14 @@ public class GroupCourseService implements IGroupCourseService { return Mono.error(new RuntimeException("课程已满员,无法签到")); } - // 校验5:用户今日是否已到店签到(直接查询sign_in_record表) - LocalDateTime todayStart = LocalDateTime.now().toLocalDate().atStartOfDay(); - LocalDateTime todayEnd = todayStart.plusDays(1); - return databaseClient.sql("SELECT sign_in_status FROM sign_in_record WHERE member_id = :memberId AND sign_in_time >= :startTime AND sign_in_time < :endTime AND is_delete = false ORDER BY sign_in_time DESC LIMIT 1") - .bind("memberId", memberId) - .bind("startTime", todayStart) - .bind("endTime", todayEnd) - .map(row -> row.get("sign_in_status", String.class)) - .one() - .switchIfEmpty(Mono.error(new RuntimeException("请先完成到店签到"))) - .flatMap(status -> { - if (!"SUCCESS".equals(status)) { - return Mono.error(new RuntimeException("到店签到未成功,请重新签到")); - } - // 校验6:用户已预约此课程(有效预约,状态为0-已预约) - return bookingRepository.findValidBooking(courseId, memberId) - .switchIfEmpty(Mono.error(new RuntimeException("您未预约此课程,无法签到"))) - .flatMap(booking -> { - return groupCourseRepository.updateCurrentMembers(courseId, 1) - .flatMap(updatedCourse -> { - return bookingRepository.updateStatus(booking.getId(), "2") - .thenReturn(updatedCourse); - }); + // 校验5:用户已预约此课程(有效预约,状态为0-已预约) + return bookingRepository.findValidBooking(courseId, memberId) + .switchIfEmpty(Mono.error(new RuntimeException("您未预约此团课"))) + .flatMap(booking -> { + return groupCourseRepository.updateCurrentMembers(courseId, 1) + .flatMap(updatedCourse -> { + return bookingRepository.updateStatus(booking.getId(), "2") + .thenReturn(updatedCourse); }); }); }) diff --git a/gym-manage-api/gym-groupCourse/src/main/java/cn/novalon/gym/manage/groupcourse/util/OSSUtil.java b/gym-manage-api/gym-groupCourse/src/main/java/cn/novalon/gym/manage/groupcourse/util/OSSUtil.java deleted file mode 100644 index 1318db2..0000000 --- a/gym-manage-api/gym-groupCourse/src/main/java/cn/novalon/gym/manage/groupcourse/util/OSSUtil.java +++ /dev/null @@ -1,109 +0,0 @@ -package cn.novalon.gym.manage.groupcourse.util; - -import com.aliyun.oss.OSS; -import com.aliyun.oss.OSSClientBuilder; -import com.aliyun.oss.model.PutObjectRequest; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.File; -import java.time.LocalDateTime; -import java.time.format.DateTimeFormatter; - -/** - * 阿里云OSS工具类 - */ -public class OSSUtil { - - private static final Logger logger = LoggerFactory.getLogger(OSSUtil.class); - - // OSS配置信息 - private static final String ENDPOINT = "oss-cn-beijing.aliyuncs.com"; - private static final String ACCESS_KEY_ID = "LTAI5t9TFh9Vayeahz45kZjg"; - private static final String ACCESS_KEY_SECRET = "zD6NlCeH5UhjBs4vnQVqn8Ksi3CaZz"; - private static final String BUCKET_NAME = "ycc-filesaver"; - - // OSS访问地址前缀 - private static final String OSS_URL_PREFIX = "https://" + BUCKET_NAME + "." + ENDPOINT + "/"; - - // 文件存储目录 - private static final String QRCODE_DIR = "qrcode/"; - - /** - * 上传文件到阿里云OSS - * - * @param localFilePath 本地文件路径 - * @param fileName 文件名(不含路径) - * @return OSS访问地址 - */ - public static String uploadToOSS(String localFilePath, String fileName) { - OSS ossClient = null; - try { - // 创建OSS客户端 - ossClient = new OSSClientBuilder().build(ENDPOINT, ACCESS_KEY_ID, ACCESS_KEY_SECRET); - - // 构建OSS文件路径:qrcode/2026/06/18/xxx.png - String datePath = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy/MM/dd")); - String ossFilePath = QRCODE_DIR + datePath + "/" + fileName; - - // 创建上传请求 - PutObjectRequest putObjectRequest = new PutObjectRequest(BUCKET_NAME, ossFilePath, new File(localFilePath)); - - // 上传文件 - ossClient.putObject(putObjectRequest); - - // 构建访问地址 - String accessUrl = OSS_URL_PREFIX + ossFilePath; - - logger.info("文件上传到OSS成功: localPath={}, ossUrl={}", localFilePath, accessUrl); - - return accessUrl; - } catch (Exception e) { - logger.error("文件上传到OSS失败 - localPath: {}, error: {}", localFilePath, e.getMessage(), e); - throw new RuntimeException("文件上传到OSS失败: " + e.getMessage(), e); - } finally { - if (ossClient != null) { - ossClient.shutdown(); - } - } - } - - /** - * 上传文件到阿里云OSS(自定义存储路径) - * - * @param localFilePath 本地文件路径 - * @param ossDirectory OSS存储目录 - * @param fileName 文件名(不含路径) - * @return OSS访问地址 - */ - public static String uploadToOSS(String localFilePath, String ossDirectory, String fileName) { - OSS ossClient = null; - try { - // 创建OSS客户端 - ossClient = new OSSClientBuilder().build(ENDPOINT, ACCESS_KEY_ID, ACCESS_KEY_SECRET); - - // 构建OSS文件路径 - String ossFilePath = ossDirectory + fileName; - - // 创建上传请求 - PutObjectRequest putObjectRequest = new PutObjectRequest(BUCKET_NAME, ossFilePath, new File(localFilePath)); - - // 上传文件 - ossClient.putObject(putObjectRequest); - - // 构建访问地址 - String accessUrl = OSS_URL_PREFIX + ossFilePath; - - logger.info("文件上传到OSS成功: localPath={}, ossUrl={}", localFilePath, accessUrl); - - return accessUrl; - } catch (Exception e) { - logger.error("文件上传到OSS失败 - localPath: {}, error: {}", localFilePath, e.getMessage(), e); - throw new RuntimeException("文件上传到OSS失败: " + e.getMessage(), e); - } finally { - if (ossClient != null) { - ossClient.shutdown(); - } - } - } -} \ No newline at end of file diff --git a/gym-manage-api/gym-groupCourse/src/main/java/cn/novalon/gym/manage/groupcourse/util/QRCodeUtil.java b/gym-manage-api/gym-groupCourse/src/main/java/cn/novalon/gym/manage/groupcourse/util/QRCodeUtil.java index 44fc2a1..f369543 100644 --- a/gym-manage-api/gym-groupCourse/src/main/java/cn/novalon/gym/manage/groupcourse/util/QRCodeUtil.java +++ b/gym-manage-api/gym-groupCourse/src/main/java/cn/novalon/gym/manage/groupcourse/util/QRCodeUtil.java @@ -10,46 +10,33 @@ import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import javax.imageio.ImageIO; +import java.awt.image.BufferedImage; +import java.io.ByteArrayOutputStream; import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.time.LocalDateTime; -import java.time.format.DateTimeFormatter; import java.util.HashMap; import java.util.Map; -import java.util.UUID; /** * 二维码生成工具类 + * 生成二维码的字节数组,由调用方统一通过文件服务保存 */ public class QRCodeUtil { private static final Logger logger = LoggerFactory.getLogger(QRCodeUtil.class); - // 二维码默认保存路径(本地临时路径) - private static final String DEFAULT_SAVE_PATH = "D:\\Games\\exmp\\image"; - // 二维码尺寸 private static final int QR_CODE_WIDTH = 300; private static final int QR_CODE_HEIGHT = 300; /** - * 生成二维码并保存到指定路径 + * 生成二维码图片的字节数组 * * @param content 二维码内容 - * @param savePath 保存路径 - * @param fileName 文件名(不含扩展名) - * @return 生成的二维码文件完整路径 + * @return PNG格式的二维码图片字节数组 */ - public static String generateQRCode(String content, String savePath, String fileName) { + public static byte[] generateQrCodeBytes(String content) { try { - Path directory = Paths.get(savePath); - if (!Files.exists(directory)) { - Files.createDirectories(directory); - logger.info("创建二维码保存目录: {}", savePath); - } - Map hints = new HashMap<>(); hints.put(EncodeHintType.CHARACTER_SET, "UTF-8"); hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.H); @@ -58,99 +45,21 @@ public class QRCodeUtil { QRCodeWriter qrCodeWriter = new QRCodeWriter(); BitMatrix bitMatrix = qrCodeWriter.encode(content, BarcodeFormat.QR_CODE, QR_CODE_WIDTH, QR_CODE_HEIGHT, hints); - String filePath = Paths.get(savePath, fileName + ".png").toString(); - Path path = Paths.get(filePath); + BufferedImage image = MatrixToImageWriter.toBufferedImage(bitMatrix); - MatrixToImageWriter.writeToPath(bitMatrix, "PNG", path); - logger.info("二维码生成成功: {}", filePath); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + ImageIO.write(image, "PNG", baos); + byte[] bytes = baos.toByteArray(); + baos.close(); - return filePath; + logger.info("二维码字节数组生成成功, size={} bytes", bytes.length); + return bytes; } catch (WriterException e) { logger.error("二维码生成失败 - WriterException: {}", e.getMessage(), e); throw new RuntimeException("二维码生成失败: " + e.getMessage(), e); } catch (IOException e) { - logger.error("二维码保存失败 - IOException: {}", e.getMessage(), e); - throw new RuntimeException("二维码保存失败: " + e.getMessage(), e); - } - } - - /** - * 生成二维码并保存到默认路径 - * 文件名格式: UUID + 创建时间 - * - * @param content 二维码内容 - * @return 生成的二维码文件完整路径 - */ - public static String generateQRCode(String content) { - String uuid = UUID.randomUUID().toString().replace("-", ""); - String timestamp = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss")); - String fileName = uuid + "_" + timestamp; - - return generateQRCode(content, DEFAULT_SAVE_PATH, fileName); - } - - /** - * 生成二维码并保存到默认路径,使用自定义文件名 - * - * @param content 二维码内容 - * @param fileName 文件名(不含扩展名) - * @return 生成的二维码文件完整路径 - */ - public static String generateQRCodeWithFileName(String content, String fileName) { - return generateQRCode(content, DEFAULT_SAVE_PATH, fileName); - } - - /** - * 生成二维码并上传到阿里云OSS - * 文件名格式: UUID + 创建时间 - * - * @param content 二维码内容 - * @return 阿里云OSS访问地址 - */ - public static String generateQRCodeAndUploadToOSS(String content) { - String uuid = UUID.randomUUID().toString().replace("-", ""); - String timestamp = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss")); - String fileName = uuid + "_" + timestamp + ".png"; - - return generateQRCodeAndUploadToOSS(content, fileName); - } - - /** - * 生成二维码并上传到阿里云OSS - * - * @param content 二维码内容 - * @param fileName 文件名(含扩展名) - * @return 阿里云OSS访问地址 - */ - public static String generateQRCodeAndUploadToOSS(String content, String fileName) { - try { - Path tempDir = Files.createTempDirectory("qrcode_temp"); - String tempFilePath = tempDir.resolve(fileName).toString(); - - Map hints = new HashMap<>(); - hints.put(EncodeHintType.CHARACTER_SET, "UTF-8"); - hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.H); - hints.put(EncodeHintType.MARGIN, 1); - - QRCodeWriter qrCodeWriter = new QRCodeWriter(); - BitMatrix bitMatrix = qrCodeWriter.encode(content, BarcodeFormat.QR_CODE, QR_CODE_WIDTH, QR_CODE_HEIGHT, hints); - - MatrixToImageWriter.writeToPath(bitMatrix, "PNG", Paths.get(tempFilePath)); - logger.info("二维码临时文件生成成功: {}", tempFilePath); - - String ossUrl = OSSUtil.uploadToOSS(tempFilePath, fileName); - - Files.deleteIfExists(Paths.get(tempFilePath)); - Files.deleteIfExists(tempDir); - logger.info("临时文件已删除: {}", tempFilePath); - - return ossUrl; - } catch (WriterException e) { - logger.error("二维码生成失败 - WriterException: {}", e.getMessage(), e); - throw new RuntimeException("二维码生成失败: " + e.getMessage(), e); - } catch (IOException e) { - logger.error("二维码处理失败 - IOException: {}", e.getMessage(), e); - throw new RuntimeException("二维码处理失败: " + e.getMessage(), e); + logger.error("二维码输出失败 - IOException: {}", e.getMessage(), e); + throw new RuntimeException("二维码输出失败: " + e.getMessage(), e); } } } \ No newline at end of file diff --git a/gym-manage-api/gym-groupCourse/src/test/java/cn/novalon/gym/manage/groupcourse/util/QRCodeUtilTest.java b/gym-manage-api/gym-groupCourse/src/test/java/cn/novalon/gym/manage/groupcourse/util/QRCodeUtilTest.java index 709cb3f..e86f633 100644 --- a/gym-manage-api/gym-groupCourse/src/test/java/cn/novalon/gym/manage/groupcourse/util/QRCodeUtilTest.java +++ b/gym-manage-api/gym-groupCourse/src/test/java/cn/novalon/gym/manage/groupcourse/util/QRCodeUtilTest.java @@ -1,9 +1,6 @@ package cn.novalon.gym.manage.groupcourse.util; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.io.TempDir; - -import java.nio.file.Path; import static org.junit.jupiter.api.Assertions.*; @@ -12,60 +9,41 @@ import static org.junit.jupiter.api.Assertions.*; */ class QRCodeUtilTest { - @TempDir - Path tempDir; - @Test - void testGenerateQRCode() { + void testGenerateQrCodeBytes() { String content = "测试二维码内容"; - String qrCodePath = QRCodeUtil.generateQRCode(content); + byte[] bytes = QRCodeUtil.generateQrCodeBytes(content); - assertNotNull(qrCodePath, "二维码路径不应为空"); - assertTrue(qrCodePath.endsWith(".png"), "二维码文件应为PNG格式"); - assertTrue(qrCodePath.contains("D:\\Games\\exmp\\image"), "二维码应保存到指定路径"); + assertNotNull(bytes, "二维码字节数组不应为空"); + assertTrue(bytes.length > 0, "二维码字节数组应包含数据"); - System.out.println("生成的二维码路径: " + qrCodePath); + System.out.println("生成的二维码字节大小: " + bytes.length + " bytes"); } @Test - void testGenerateQRCodeWithCustomPath() { - String content = "自定义路径测试"; - String customPath = tempDir.toString(); - String fileName = "test_qrcode"; + void testGenerateQrCodeBytesWithJsonContent() { + String jsonContent = "{\"id\":1,\"courseName\":\"瑜伽课\",\"coachId\":100,\"startTime\":\"2026-07-14T10:00:00\"}"; - String qrCodePath = QRCodeUtil.generateQRCode(content, customPath, fileName); + byte[] bytes = QRCodeUtil.generateQrCodeBytes(jsonContent); - assertNotNull(qrCodePath, "二维码路径不应为空"); - assertTrue(qrCodePath.endsWith(".png"), "二维码文件应为PNG格式"); - assertTrue(qrCodePath.contains(fileName), "二维码文件名应包含指定名称"); + assertNotNull(bytes, "二维码字节数组不应为空"); + assertTrue(bytes.length > 0, "二维码字节数组应包含数据"); - System.out.println("生成的二维码路径: " + qrCodePath); + System.out.println("JSON内容二维码字节大小: " + bytes.length + " bytes"); } @Test - void testGenerateQRCodeWithJsonContent() { - String jsonContent = "{\"id\":1,\"courseName\":\"瑜伽课\",\"coachId\":100,\"startTime\":\"2026-06-18T10:00:00\"}"; + void testGenerateQrCodeBytesWithLongContent() { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < 100; i++) { + sb.append("这是第").append(i).append("行测试数据\n"); + } - String qrCodePath = QRCodeUtil.generateQRCode(jsonContent); + byte[] bytes = QRCodeUtil.generateQrCodeBytes(sb.toString()); - assertNotNull(qrCodePath, "二维码路径不应为空"); - assertTrue(qrCodePath.endsWith(".png"), "二维码文件应为PNG格式"); + assertNotNull(bytes, "二维码字节数组不应为空"); + assertTrue(bytes.length > 0, "长内容二维码应正常生成"); - System.out.println("JSON内容二维码路径: " + qrCodePath); + System.out.println("长内容二维码字节大小: " + bytes.length + " bytes"); } - - @Test - void testGenerateQRCodeAndUploadToOSS() { - String jsonContent = "{\"id\":1,\"courseName\":\"瑜伽课\",\"coachId\":100,\"startTime\":\"2026-06-18T10:00:00\"}"; - - String ossUrl = QRCodeUtil.generateQRCodeAndUploadToOSS(jsonContent); - - assertNotNull(ossUrl, "OSS访问地址不应为空"); - assertTrue(ossUrl.startsWith("https://"), "OSS访问地址应为HTTPS"); - assertTrue(ossUrl.contains("ycc-filesaver.oss-cn-beijing.aliyuncs.com"), "OSS访问地址应包含正确的域名"); - assertTrue(ossUrl.contains("/qrcode/"), "OSS访问地址应包含qrcode目录"); - assertTrue(ossUrl.endsWith(".png"), "OSS访问地址应为PNG格式"); - - System.out.println("上传到OSS的二维码地址: " + ossUrl); - } -} \ No newline at end of file +} diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/config/WechatProperties.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/config/WechatProperties.java index ec32292..9af131b 100644 --- a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/config/WechatProperties.java +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/config/WechatProperties.java @@ -16,6 +16,9 @@ import org.springframework.stereotype.Component; @ConfigurationProperties(prefix = "wechat") public class WechatProperties { + // Mock模式:true=使用模拟数据,false=调用真实微信API + private Boolean mockEnabled; + // 小程序配置 private MiniApp miniapp = new MiniApp(); diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/WechatApiServiceImpl.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/WechatApiServiceImpl.java index 99238ec..2eddee9 100644 --- a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/WechatApiServiceImpl.java +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/WechatApiServiceImpl.java @@ -43,12 +43,25 @@ public class WechatApiServiceImpl implements WechatApiService { @Override public Mono> jsCode2Session(String code) { - log.info("微信jsCode2Session API"); - log.info("信息 - AppID: {}, AppSecret {}", + log.info("微信jsCode2Session API, code: {}", code); + + // Mock模式:返回模拟数据 + if (wechatProperties.getMockEnabled() != null && wechatProperties.getMockEnabled()) { + log.info("Mock模式已启用,返回模拟微信登录数据"); + Map mockResult = new HashMap<>(); + // 使用 code 的哈希生成唯一的 openid,确保每次登录创建不同用户 + String mockOpenId = "mock_openid_" + Math.abs(code.hashCode() % 1000000); + mockResult.put("openid", mockOpenId); + mockResult.put("session_key", "mock_session_key"); + mockResult.put("unionid", "mock_unionid_" + Math.abs(code.hashCode() % 1000000)); + log.info("Mock微信API响应成功, openid: {}, unionid: {}", mockOpenId, mockResult.get("unionid")); + return Mono.just(mockResult); + } + + log.info("真实微信API调用 - AppID: {}, AppSecret {}", wechatProperties.getMiniapp().getAppId(), wechatProperties.getMiniapp().getAppSecret() != null ? wechatProperties.getMiniapp().getAppSecret().substring(0, Math.min(4, wechatProperties.getMiniapp().getAppSecret().length())) + "***" : "null"); - log.info(" - code: {}", code); return webClient.get() .uri(uriBuilder -> uriBuilder @@ -152,6 +165,12 @@ public class WechatApiServiceImpl implements WechatApiService { public Mono getAccessToken(String appType) { log.debug("获取access_token, appType: {}", appType); + // Mock模式:返回模拟access_token + if (wechatProperties.getMockEnabled() != null && wechatProperties.getMockEnabled()) { + log.debug("Mock模式已启用,返回模拟access_token"); + return Mono.just("mock_access_token"); + } + String cacheKey = ACCESS_TOKEN_CACHE_PREFIX + appType; return redisUtil.get(cacheKey, String.class) diff --git a/gym-manage-api/manage-app/src/main/resources/member-config.yml b/gym-manage-api/manage-app/src/main/resources/member-config.yml index 8cb0cf0..76235fc 100644 --- a/gym-manage-api/manage-app/src/main/resources/member-config.yml +++ b/gym-manage-api/manage-app/src/main/resources/member-config.yml @@ -1,18 +1,18 @@ # 微信配置(测试环境使用模拟数据) wechat: # Mock模式:true=使用模拟数据(开发测试),false=调用真实微信API(生产环境) - mock-enabled: false + mock-enabled: true miniapp: - app-id: ${WECHAT_MINIAPP_APP_ID} - app-secret: ${WECHAT_MINIAPP_SECRET} + app-id: ${WECHAT_MINIAPP_APP_ID:} + app-secret: ${WECHAT_MINIAPP_SECRET:} mp: - app-id: ${WECHAT_MP_APP_ID} - app-secret: ${WECHAT_MP_SECRET} - token: ${WECHAT_MP_TOKEN} - aes-key: ${WECHAT_MP_AESKEY} - callback-url: ${WECHAT_MP_CALLBACK_URL} + app-id: ${WECHAT_MP_APP_ID:} + app-secret: ${WECHAT_MP_SECRET:} + token: ${WECHAT_MP_TOKEN:} + aes-key: ${WECHAT_MP_AESKEY:} + callback-url: ${WECHAT_MP_CALLBACK_URL:} # 手机号加密配置(AES-128-CBC,16字节密钥和IV,Base64编码) phone-encryption: diff --git a/gym-manage-api/manage-db/src/main/resources/db/migration/V13__Consolidated_Test_Data.sql b/gym-manage-api/manage-db/src/main/resources/db/migration/V13__Consolidated_Test_Data.sql index 2eb2f99..e104153 100644 --- a/gym-manage-api/manage-db/src/main/resources/db/migration/V13__Consolidated_Test_Data.sql +++ b/gym-manage-api/manage-db/src/main/resources/db/migration/V13__Consolidated_Test_Data.sql @@ -1,7 +1,7 @@ -- ============================================================ --- V13: 整合所有测试数据(基于 2026-06-29 17:13) +-- V13: 整合所有测试数据(基于 2026-07-14) -- 说明: 系统初始数据由 V2 处理,本文件仅包含业务测试数据 --- 所有时间相关字段已更新为基于 2026-06-29 +-- 所有时间相关字段已更新为基于 2026-07-14 -- ============================================================ -- ============================================================ @@ -84,38 +84,38 @@ INSERT INTO member_card (member_card_name, member_card_type, member_card_price, -- E1: 数据统计测试会员(V14,显式 ID 1001-1012) INSERT INTO member_user (id, member_no, nickname, phone, created_at, updated_at, is_deleted) VALUES -(1001, 'M20260629001', '张三', '13800138001', '2026-06-29 08:00:00', '2026-06-29 08:00:00', false), -(1002, 'M20260629002', '李四', '13800138002', '2026-06-29 09:00:00', '2026-06-29 09:00:00', false), -(1003, 'M20260628003', '王五', '13800138003', '2026-06-28 10:00:00', '2026-06-28 10:00:00', false), -(1004, 'M20260627004', '赵六', '13800138004', '2026-06-27 11:00:00', '2026-06-27 11:00:00', false), -(1005, 'M20260629005', '钱七', '13800138005', '2026-06-29 08:00:00', '2026-06-29 08:00:00', false), -(1006, 'M20260629006', '孙八', '13800138006', '2026-06-29 09:00:00', '2026-06-29 09:00:00', false), -(1007, 'M20260629007', '周九', '13800138007', '2026-06-29 10:00:00', '2026-06-29 10:00:00', false), -(1008, 'M20260626008', '吴十', '13800138008', '2026-06-26 14:00:00', '2026-06-26 14:00:00', false), -(1009, 'M20260625009', '郑十一', '13800138009', '2026-06-25 15:00:00', '2026-06-25 15:00:00', false), -(1010, 'M20260624010', '王十二', '13800138010', '2026-06-24 16:00:00', '2026-06-24 16:00:00', false), -(1011, 'M20260623011', '陈十三', '13800138011', '2026-06-23 17:00:00', '2026-06-23 17:00:00', false), -(1012, 'M20260622012', '刘十四', '13800138012', '2026-06-22 18:00:00', '2026-06-22 18:00:00', false); +(1001, 'M20260714001', '张三', '13800138001', '2026-07-14 08:00:00', '2026-07-14 08:00:00', false), +(1002, 'M20260714002', '李四', '13800138002', '2026-07-14 09:00:00', '2026-07-14 09:00:00', false), +(1003, 'M20260713003', '王五', '13800138003', '2026-07-13 10:00:00', '2026-07-13 10:00:00', false), +(1004, 'M20260712004', '赵六', '13800138004', '2026-07-12 11:00:00', '2026-07-12 11:00:00', false), +(1005, 'M20260714005', '钱七', '13800138005', '2026-07-14 08:00:00', '2026-07-14 08:00:00', false), +(1006, 'M20260714006', '孙八', '13800138006', '2026-07-14 09:00:00', '2026-07-14 09:00:00', false), +(1007, 'M20260714007', '周九', '13800138007', '2026-07-14 10:00:00', '2026-07-14 10:00:00', false), +(1008, 'M20260711008', '吴十', '13800138008', '2026-07-11 14:00:00', '2026-07-11 14:00:00', false), +(1009, 'M20260710009', '郑十一', '13800138009', '2026-07-10 15:00:00', '2026-07-10 15:00:00', false), +(1010, 'M20260709010', '王十二', '13800138010', '2026-07-09 16:00:00', '2026-07-09 16:00:00', false), +(1011, 'M20260708011', '陈十三', '13800138011', '2026-07-08 17:00:00', '2026-07-08 17:00:00', false), +(1012, 'M20260707012', '刘十四', '13800138012', '2026-07-07 18:00:00', '2026-07-07 18:00:00', false); -- E2: 预约场景测试会员(V10: A-G) INSERT INTO member_user (member_no, nickname, phone, gender, is_deleted, created_at, updated_at) VALUES -('MEM_TEST_A', '用户A_无卡新用户', '13800001001', 1, false, '2026-06-29 10:00:00', '2026-06-29 10:00:00'), -('MEM_TEST_B', '用户B_过期时长卡', '13800001002', 1, false, '2026-06-28 10:00:00', '2026-06-28 10:00:00'), -('MEM_TEST_C', '会员C_有效时长卡', '13800001003', 2, false, '2026-06-28 10:00:00', '2026-06-28 10:00:00'), -('MEM_TEST_D', '会员D_次数用尽', '13800001004', 1, false, '2026-06-28 10:00:00', '2026-06-28 10:00:00'), -('MEM_TEST_E', '会员E_次数充足', '13800001005', 2, false, '2026-06-28 10:00:00', '2026-06-28 10:00:00'), -('MEM_TEST_F', '会员F_储值卡余额不足', '13800001006', 1, false, '2026-06-28 10:00:00', '2026-06-28 10:00:00'), -('MEM_TEST_G', '会员G_储值卡余额充足', '13800001007', 2, false, '2026-06-28 10:00:00', '2026-06-28 10:00:00'); +('MEM_TEST_A', '用户A_无卡新用户', '13800001001', 1, false, '2026-07-14 10:00:00', '2026-07-14 10:00:00'), +('MEM_TEST_B', '用户B_过期时长卡', '13800001002', 1, false, '2026-07-13 10:00:00', '2026-07-13 10:00:00'), +('MEM_TEST_C', '会员C_有效时长卡', '13800001003', 2, false, '2026-07-13 10:00:00', '2026-07-13 10:00:00'), +('MEM_TEST_D', '会员D_次数用尽', '13800001004', 1, false, '2026-07-13 10:00:00', '2026-07-13 10:00:00'), +('MEM_TEST_E', '会员E_次数充足', '13800001005', 2, false, '2026-07-13 10:00:00', '2026-07-13 10:00:00'), +('MEM_TEST_F', '会员F_储值卡余额不足', '13800001006', 1, false, '2026-07-13 10:00:00', '2026-07-13 10:00:00'), +('MEM_TEST_G', '会员G_储值卡余额充足', '13800001007', 2, false, '2026-07-13 10:00:00', '2026-07-13 10:00:00'); -- E3: 取消预约测试会员(V11: 张三/李四/王五) INSERT INTO member_user (member_no, nickname, phone, gender, is_deleted, created_at, updated_at) VALUES -('MEM_TEST_ZHANG', '张三_时长卡用户', '13800002001', 1, false, '2026-06-28 10:00:00', '2026-06-28 10:00:00'), -('MEM_TEST_LI', '李四_次数卡用户', '13800002002', 1, false, '2026-06-28 10:00:00', '2026-06-28 10:00:00'), -('MEM_TEST_WANG', '王五_储值卡用户', '13800002003', 2, false, '2026-06-28 10:00:00', '2026-06-28 10:00:00'); +('MEM_TEST_ZHANG', '张三_时长卡用户', '13800002001', 1, false, '2026-07-13 10:00:00', '2026-07-13 10:00:00'), +('MEM_TEST_LI', '李四_次数卡用户', '13800002002', 1, false, '2026-07-13 10:00:00', '2026-07-13 10:00:00'), +('MEM_TEST_WANG', '王五_储值卡用户', '13800002003', 2, false, '2026-07-13 10:00:00', '2026-07-13 10:00:00'); -- E4: 时间冲突测试会员(V12) INSERT INTO member_user (member_no, nickname, phone, gender, is_deleted, created_at, updated_at) VALUES -('MEM_TIME_CONFLICT', '时间冲突测试会员', '13800009999', 1, false, '2026-06-28 10:00:00', '2026-06-28 10:00:00'); +('MEM_TIME_CONFLICT', '时间冲突测试会员', '13800009999', 1, false, '2026-07-13 10:00:00', '2026-07-13 10:00:00'); -- ============================================================ -- Section F: 会员卡记录 @@ -123,67 +123,67 @@ INSERT INTO member_user (member_no, nickname, phone, gender, is_deleted, created -- F1: V10 预约场景 - 6条会员卡记录 INSERT INTO member_card_record (member_id, member_card_id, status, remaining_times, remaining_amount, expire_time, purchase_time, version, card_composition, created_at, updated_at) VALUES -((SELECT id FROM member_user WHERE member_no = 'MEM_TEST_B'), (SELECT member_card_id FROM member_card WHERE member_card_name = '30天时长卡'), 'EXPIRED', 0, 0.00, '2026-06-28 23:59:59', '2026-05-29 10:00:00', 0, '{}', '2026-05-29 10:00:00', '2026-06-29 10:00:00'); +((SELECT id FROM member_user WHERE member_no = 'MEM_TEST_B'), (SELECT member_card_id FROM member_card WHERE member_card_name = '30天时长卡'), 'EXPIRED', 0, 0.00, '2026-07-13 23:59:59', '2026-06-13 10:00:00', 0, '{}', '2026-06-13 10:00:00', '2026-07-14 10:00:00'); INSERT INTO member_card_record (member_id, member_card_id, status, remaining_times, remaining_amount, expire_time, purchase_time, version, card_composition, created_at, updated_at) VALUES -((SELECT id FROM member_user WHERE member_no = 'MEM_TEST_C'), (SELECT member_card_id FROM member_card WHERE member_card_name = '30天时长卡'), 'ACTIVE', 0, 0.00, '2026-07-29 23:59:59', '2026-06-29 10:00:00', 0, '{}', '2026-06-29 10:00:00', '2026-06-29 10:00:00'); +((SELECT id FROM member_user WHERE member_no = 'MEM_TEST_C'), (SELECT member_card_id FROM member_card WHERE member_card_name = '30天时长卡'), 'ACTIVE', 0, 0.00, '2026-08-13 23:59:59', '2026-07-14 10:00:00', 0, '{}', '2026-07-14 10:00:00', '2026-07-14 10:00:00'); INSERT INTO member_card_record (member_id, member_card_id, status, remaining_times, remaining_amount, expire_time, purchase_time, version, card_composition, created_at, updated_at) VALUES -((SELECT id FROM member_user WHERE member_no = 'MEM_TEST_D'), (SELECT member_card_id FROM member_card WHERE member_card_name = '10次卡'), 'ACTIVE', 0, 0.00, '2026-12-31 23:59:59', '2026-05-29 10:00:00', 0, '{}', '2026-05-29 10:00:00', '2026-06-29 10:00:00'); +((SELECT id FROM member_user WHERE member_no = 'MEM_TEST_D'), (SELECT member_card_id FROM member_card WHERE member_card_name = '10次卡'), 'ACTIVE', 0, 0.00, '2026-12-31 23:59:59', '2026-06-13 10:00:00', 0, '{}', '2026-06-13 10:00:00', '2026-07-14 10:00:00'); INSERT INTO member_card_record (member_id, member_card_id, status, remaining_times, remaining_amount, expire_time, purchase_time, version, card_composition, created_at, updated_at) VALUES -((SELECT id FROM member_user WHERE member_no = 'MEM_TEST_E'), (SELECT member_card_id FROM member_card WHERE member_card_name = '10次卡'), 'ACTIVE', 5, 0.00, '2026-12-31 23:59:59', '2026-05-29 10:00:00', 0, '{}', '2026-05-29 10:00:00', '2026-06-29 10:00:00'); +((SELECT id FROM member_user WHERE member_no = 'MEM_TEST_E'), (SELECT member_card_id FROM member_card WHERE member_card_name = '10次卡'), 'ACTIVE', 5, 0.00, '2026-12-31 23:59:59', '2026-06-13 10:00:00', 0, '{}', '2026-06-13 10:00:00', '2026-07-14 10:00:00'); INSERT INTO member_card_record (member_id, member_card_id, status, remaining_times, remaining_amount, expire_time, purchase_time, version, card_composition, created_at, updated_at) VALUES -((SELECT id FROM member_user WHERE member_no = 'MEM_TEST_F'), (SELECT member_card_id FROM member_card WHERE member_card_name = '储值卡500'), 'ACTIVE', 0, 30.00, '2027-06-29 23:59:59', '2026-05-29 10:00:00', 0, '{}', '2026-05-29 10:00:00', '2026-06-29 10:00:00'); +((SELECT id FROM member_user WHERE member_no = 'MEM_TEST_F'), (SELECT member_card_id FROM member_card WHERE member_card_name = '储值卡500'), 'ACTIVE', 0, 30.00, '2027-07-14 23:59:59', '2026-06-13 10:00:00', 0, '{}', '2026-06-13 10:00:00', '2026-07-14 10:00:00'); INSERT INTO member_card_record (member_id, member_card_id, status, remaining_times, remaining_amount, expire_time, purchase_time, version, card_composition, created_at, updated_at) VALUES -((SELECT id FROM member_user WHERE member_no = 'MEM_TEST_G'), (SELECT member_card_id FROM member_card WHERE member_card_name = '储值卡500'), 'ACTIVE', 0, 200.00, '2027-06-29 23:59:59', '2026-05-29 10:00:00', 0, '{}', '2026-05-29 10:00:00', '2026-06-29 10:00:00'); +((SELECT id FROM member_user WHERE member_no = 'MEM_TEST_G'), (SELECT member_card_id FROM member_card WHERE member_card_name = '储值卡500'), 'ACTIVE', 0, 200.00, '2027-07-14 23:59:59', '2026-06-13 10:00:00', 0, '{}', '2026-06-13 10:00:00', '2026-07-14 10:00:00'); -- F2: V11 取消预约场景 - 3条 INSERT INTO member_card_record (member_id, member_card_id, status, remaining_times, remaining_amount, expire_time, purchase_time, version, card_composition, created_at, updated_at) VALUES -((SELECT id FROM member_user WHERE member_no = 'MEM_TEST_ZHANG'), (SELECT member_card_id FROM member_card WHERE member_card_name = '30天时长卡'), 'ACTIVE', 0, 0.00, '2026-07-29 23:59:59', '2026-06-29 10:00:00', 0, '{}', '2026-06-29 10:00:00', '2026-06-29 10:00:00'), -((SELECT id FROM member_user WHERE member_no = 'MEM_TEST_LI'), (SELECT member_card_id FROM member_card WHERE member_card_name = '10次卡'), 'ACTIVE', 5, 0.00, '2026-12-31 23:59:59', '2026-05-29 10:00:00', 0, '{}', '2026-05-29 10:00:00', '2026-06-29 10:00:00'), -((SELECT id FROM member_user WHERE member_no = 'MEM_TEST_WANG'), (SELECT member_card_id FROM member_card WHERE member_card_name = '储值卡500'), 'ACTIVE', 0, 200.00, '2027-06-29 23:59:59', '2026-05-29 10:00:00', 0, '{}', '2026-05-29 10:00:00', '2026-06-29 10:00:00'); +((SELECT id FROM member_user WHERE member_no = 'MEM_TEST_ZHANG'), (SELECT member_card_id FROM member_card WHERE member_card_name = '30天时长卡'), 'ACTIVE', 0, 0.00, '2026-08-13 23:59:59', '2026-07-14 10:00:00', 0, '{}', '2026-07-14 10:00:00', '2026-07-14 10:00:00'), +((SELECT id FROM member_user WHERE member_no = 'MEM_TEST_LI'), (SELECT member_card_id FROM member_card WHERE member_card_name = '10次卡'), 'ACTIVE', 5, 0.00, '2026-12-31 23:59:59', '2026-06-13 10:00:00', 0, '{}', '2026-06-13 10:00:00', '2026-07-14 10:00:00'), +((SELECT id FROM member_user WHERE member_no = 'MEM_TEST_WANG'), (SELECT member_card_id FROM member_card WHERE member_card_name = '储值卡500'), 'ACTIVE', 0, 200.00, '2027-07-14 23:59:59', '2026-06-13 10:00:00', 0, '{}', '2026-06-13 10:00:00', '2026-07-14 10:00:00'); -- F3: V12 时间冲突场景 - 1条 INSERT INTO member_card_record (member_id, member_card_id, status, remaining_times, remaining_amount, expire_time, purchase_time, version, card_composition, created_at, updated_at) VALUES -((SELECT id FROM member_user WHERE member_no = 'MEM_TIME_CONFLICT'), (SELECT member_card_id FROM member_card WHERE member_card_name = '10次卡'), 'ACTIVE', 10, 0.00, '2026-12-31 23:59:59', '2026-06-29 10:00:00', 0, '{}', '2026-06-29 10:00:00', '2026-06-29 10:00:00'); +((SELECT id FROM member_user WHERE member_no = 'MEM_TIME_CONFLICT'), (SELECT member_card_id FROM member_card WHERE member_card_name = '10次卡'), 'ACTIVE', 10, 0.00, '2026-12-31 23:59:59', '2026-07-14 10:00:00', 0, '{}', '2026-07-14 10:00:00', '2026-07-14 10:00:00'); -- ============================================================ --- Section G: 团课课程(来源 V7 / V10 / V11 / V12 / V14,基于 2026-06-29) +-- Section G: 团课课程(来源 V7 / V10 / V11 / V12 / V14,基于 2026-07-14) -- ============================================================ -- G1: V7 - 7种场景团课 INSERT INTO group_course (course_name, coach_id, course_type, start_time, end_time, max_members, current_members, status, location, cover_image, description, stored_value_amount, create_by, created_at, updated_at) VALUES -('极速燃脂单车', 104, 2, '2026-06-30 16:45:00', '2026-06-30 20:20:00', 25, 0, 0, '单车房', '/images/spinning.jpg', '跟随音乐节奏变换阻力和速度,体验爬坡与冲刺的快感。', 50.00, 'admin', '2026-06-29 10:00:00', '2026-06-29 10:00:00'), -('清晨流瑜伽', 101, 1, '2026-07-03 09:00:00', '2026-07-03 10:30:00', 15, 5, 0, 'A座3楼瑜伽教室', '/images/yoga_flow.jpg', '适合有一定基础的学员,通过流畅的体式连接呼吸。', 50.00, 'admin', '2026-06-29 10:00:00', '2026-06-29 10:00:00'), -('燃脂搏击', 102, 2, '2026-07-01 18:30:00', '2026-07-01 19:30:00', 20, 20, 0, '综合训练区', '/images/kickboxing.jpg', '高强度间歇训练,名额已满。', 50.00, 'coach_zhang', '2026-06-29 14:30:00', '2026-06-29 14:30:00'), -('哈他瑜伽', 101, 1, '2026-06-29 17:45:00', '2026-06-29 18:50:00', 12, 3, 0, '瑜伽教室B', '/images/hatha_yoga.jpg', '基础哈他瑜伽,距开始不足30分钟,已停止预约。', 50.00, 'coach_li', '2026-06-29 08:00:00', '2026-06-29 08:00:00'), -('周末冥想修复', 101, 1, '2026-07-11 15:00:00', '2026-07-11 16:00:00', 12, 3, 1, '冥想室', '/images/meditation.jpg', '通过呼吸和正念冥想,该课程已被取消。', 50.00, 'coach_wang', '2026-06-29 08:00:00', '2026-06-29 08:00:00'), -('蜜桃臀塑造', 103, 3, '2026-06-23 19:00:00', '2026-06-23 20:00:00', 10, 8, 2, '私教专区', '/images/glute.jpg', '针对性训练臀部肌肉群,课程已结束。', 50.00, 'coach_li', '2026-06-23 09:15:00', '2026-06-23 09:15:00'), -('午间冥想放松', 101, 1, '2026-06-25 12:00:00', '2026-06-25 13:00:00', 15, 6, 2, '冥想室', '/images/meditation_noon.jpg', '午间冥想课程,已结束。', 50.00, 'admin', '2026-06-25 09:00:00', '2026-06-25 09:00:00'); +('极速燃脂单车', 104, 2, '2026-07-15 16:45:00', '2026-07-15 20:20:00', 25, 0, 0, '单车房', '/images/spinning.jpg', '跟随音乐节奏变换阻力和速度,体验爬坡与冲刺的快感。', 50.00, 'admin', '2026-07-14 10:00:00', '2026-07-14 10:00:00'), +('清晨流瑜伽', 101, 1, '2026-07-18 09:00:00', '2026-07-18 10:30:00', 15, 5, 0, 'A座3楼瑜伽教室', '/images/yoga_flow.jpg', '适合有一定基础的学员,通过流畅的体式连接呼吸。', 50.00, 'admin', '2026-07-14 10:00:00', '2026-07-14 10:00:00'), +('燃脂搏击', 102, 2, '2026-07-16 18:30:00', '2026-07-16 19:30:00', 20, 20, 0, '综合训练区', '/images/kickboxing.jpg', '高强度间歇训练,名额已满。', 50.00, 'coach_zhang', '2026-07-14 14:30:00', '2026-07-14 14:30:00'), +('哈他瑜伽', 101, 1, '2026-07-14 17:45:00', '2026-07-14 18:50:00', 12, 3, 0, '瑜伽教室B', '/images/hatha_yoga.jpg', '基础哈他瑜伽,距开始不足30分钟,已停止预约。', 50.00, 'coach_li', '2026-07-14 08:00:00', '2026-07-14 08:00:00'), +('周末冥想修复', 101, 1, '2026-07-26 15:00:00', '2026-07-26 16:00:00', 12, 3, 1, '冥想室', '/images/meditation.jpg', '通过呼吸和正念冥想,该课程已被取消。', 50.00, 'coach_wang', '2026-07-14 08:00:00', '2026-07-14 08:00:00'), +('蜜桃臀塑造', 103, 3, '2026-07-08 19:00:00', '2026-07-08 20:00:00', 10, 8, 2, '私教专区', '/images/glute.jpg', '针对性训练臀部肌肉群,课程已结束。', 50.00, 'coach_li', '2026-07-08 09:15:00', '2026-07-08 09:15:00'), +('午间冥想放松', 101, 1, '2026-07-10 12:00:00', '2026-07-10 13:00:00', 15, 6, 2, '冥想室', '/images/meditation_noon.jpg', '午间冥想课程,已结束。', 50.00, 'admin', '2026-07-10 09:00:00', '2026-07-10 09:00:00'); -- G2: V10 预约场景(已取消次数卡支付,仅保留储值卡) INSERT INTO group_course (course_name, coach_id, course_type, start_time, end_time, max_members, current_members, status, location, description, stored_value_amount, create_by, created_at, updated_at) VALUES -('燃脂搏击_储值测试', 102, 2, '2026-07-01 19:30:00', '2026-07-01 20:30:00', 20, 0, 0, '综合训练区', '消耗储值50元', 50.00, 'admin', '2026-06-29 10:00:00', '2026-06-29 10:00:00'), -('高端普拉提_储值卡课程', 103, 1, '2026-07-02 19:00:00', '2026-07-02 20:00:00', 15, 0, 0, '普拉提教室', '消耗储值20元', 20.00, 'admin', '2026-06-29 10:00:00', '2026-06-29 10:00:00'); +('燃脂搏击_储值测试', 102, 2, '2026-07-16 19:30:00', '2026-07-16 20:30:00', 20, 0, 0, '综合训练区', '消耗储值50元', 50.00, 'admin', '2026-07-14 10:00:00', '2026-07-14 10:00:00'), +('高端普拉提_储值卡课程', 103, 1, '2026-07-17 19:00:00', '2026-07-17 20:00:00', 15, 0, 0, '普拉提教室', '消耗储值20元', 20.00, 'admin', '2026-07-14 10:00:00', '2026-07-14 10:00:00'); -- G3: V11 取消预约场景 INSERT INTO group_course (course_name, coach_id, course_type, start_time, end_time, max_members, current_members, status, location, description, stored_value_amount, create_by, created_at, updated_at) VALUES -('晚间瑜伽_取消测试', 101, 1, '2026-07-02 19:00:00', '2026-07-02 20:00:00', 20, 0, 0, '瑜伽教室', '用于测试取消预约功能', 30.00, 'admin', '2026-06-29 10:00:00', '2026-06-29 10:00:00'); +('晚间瑜伽_取消测试', 101, 1, '2026-07-17 19:00:00', '2026-07-17 20:00:00', 20, 0, 0, '瑜伽教室', '用于测试取消预约功能', 30.00, 'admin', '2026-07-14 10:00:00', '2026-07-14 10:00:00'); -- G4: V12 时间冲突场景 INSERT INTO group_course (course_name, coach_id, course_type, start_time, end_time, max_members, current_members, status, location, description, stored_value_amount, create_by, created_at, updated_at) VALUES -('时间冲突测试_A_13点-15点', 102, 2, '2026-07-01 13:00:00', '2026-07-01 15:00:00', 20, 0, 0, '综合训练区', '测试用团课A', 50.00, 'admin', '2026-06-29 10:00:00', '2026-06-29 10:00:00'), -('时间冲突测试_B_14点-16点', 103, 1, '2026-07-01 14:00:00', '2026-07-01 16:00:00', 15, 0, 0, '普拉提教室', '与团课A时间重叠', 50.00, 'admin', '2026-06-29 10:00:00', '2026-06-29 10:00:00'), -('时间冲突测试_C_10点-12点', 101, 1, '2026-07-01 10:00:00', '2026-07-01 12:00:00', 15, 0, 0, '瑜伽教室', '与A/B不冲突', 50.00, 'admin', '2026-06-29 10:00:00', '2026-06-29 10:00:00'); +('时间冲突测试_A_13点-15点', 102, 2, '2026-07-16 13:00:00', '2026-07-16 15:00:00', 20, 0, 0, '综合训练区', '测试用团课A', 50.00, 'admin', '2026-07-14 10:00:00', '2026-07-14 10:00:00'), +('时间冲突测试_B_14点-16点', 103, 1, '2026-07-16 14:00:00', '2026-07-16 16:00:00', 15, 0, 0, '普拉提教室', '与团课A时间重叠', 50.00, 'admin', '2026-07-14 10:00:00', '2026-07-14 10:00:00'), +('时间冲突测试_C_10点-12点', 101, 1, '2026-07-16 10:00:00', '2026-07-16 12:00:00', 15, 0, 0, '瑜伽教室', '与A/B不冲突', 50.00, 'admin', '2026-07-14 10:00:00', '2026-07-14 10:00:00'); --- G5: V14 数据统计场景(今天 2026-06-29) +-- G5: V14 数据统计场景(今天 2026-07-14) INSERT INTO group_course (id, course_name, coach_id, course_type, start_time, end_time, max_members, current_members, status, location, cover_image, description, stored_value_amount, created_at, updated_at) VALUES -(3001, '瑜伽入门', 1, 1, '2026-06-29 08:00:00', '2026-06-29 09:00:00', 20, 15, 0, '健身房A区', 'https://example.com/yoga.jpg', '适合初学者的瑜伽课程', 50.00, '2026-06-29 10:00:00', '2026-06-29 10:00:00'), -(3002, '动感单车', 2, 2, '2026-06-29 09:30:00', '2026-06-29 10:30:00', 25, 20, 0, '健身房B区', 'https://example.com/spinning.jpg', '高强度有氧运动', 50.00, '2026-06-29 11:00:00', '2026-06-29 11:00:00'), -(3003, '普拉提', 3, 1, '2026-06-29 14:00:00', '2026-06-29 15:00:00', 15, 10, 0, '健身房C区', 'https://example.com/pilates.jpg', '核心力量训练', 50.00, '2026-06-29 12:00:00', '2026-06-29 12:00:00'); +(3001, '瑜伽入门', 1, 1, '2026-07-14 08:00:00', '2026-07-14 09:00:00', 20, 15, 0, '健身房A区', 'https://example.com/yoga.jpg', '适合初学者的瑜伽课程', 50.00, '2026-07-14 10:00:00', '2026-07-14 10:00:00'), +(3002, '动感单车', 2, 2, '2026-07-14 09:30:00', '2026-07-14 10:30:00', 25, 20, 0, '健身房B区', 'https://example.com/spinning.jpg', '高强度有氧运动', 50.00, '2026-07-14 11:00:00', '2026-07-14 11:00:00'), +(3003, '普拉提', 3, 1, '2026-07-14 14:00:00', '2026-07-14 15:00:00', 15, 10, 0, '健身房C区', 'https://example.com/pilates.jpg', '核心力量训练', 50.00, '2026-07-14 12:00:00', '2026-07-14 12:00:00'); -- ============================================================ -- Section H: 团课预约记录 @@ -191,116 +191,116 @@ INSERT INTO group_course (id, course_name, coach_id, course_type, start_time, en -- H1: V11 取消预约测试 INSERT INTO group_course_booking (course_id, member_id, member_card_id, booking_time, status, course_name, course_start_time, course_end_time, location, created_at, updated_at) VALUES -((SELECT id FROM group_course WHERE course_name = '晚间瑜伽_取消测试'), (SELECT id FROM member_user WHERE member_no = 'MEM_TEST_ZHANG'), (SELECT id FROM member_card_record WHERE member_id = (SELECT id FROM member_user WHERE member_no = 'MEM_TEST_ZHANG')), '2026-06-29 11:00:00', '0', '晚间瑜伽_取消测试', '2026-07-02 19:00:00', '2026-07-02 20:00:00', '瑜伽教室', '2026-06-29 11:00:00', '2026-06-29 11:00:00'), -((SELECT id FROM group_course WHERE course_name = '晚间瑜伽_取消测试'), (SELECT id FROM member_user WHERE member_no = 'MEM_TEST_LI'), (SELECT id FROM member_card_record WHERE member_id = (SELECT id FROM member_user WHERE member_no = 'MEM_TEST_LI')), '2026-06-29 11:30:00', '0', '晚间瑜伽_取消测试', '2026-07-02 19:00:00', '2026-07-02 20:00:00', '瑜伽教室', '2026-06-29 11:30:00', '2026-06-29 11:30:00'), -((SELECT id FROM group_course WHERE course_name = '晚间瑜伽_取消测试'), (SELECT id FROM member_user WHERE member_no = 'MEM_TEST_WANG'), (SELECT id FROM member_card_record WHERE member_id = (SELECT id FROM member_user WHERE member_no = 'MEM_TEST_WANG')), '2026-06-29 12:00:00', '0', '晚间瑜伽_取消测试', '2026-07-02 19:00:00', '2026-07-02 20:00:00', '瑜伽教室', '2026-06-29 12:00:00', '2026-06-29 12:00:00'); +((SELECT id FROM group_course WHERE course_name = '晚间瑜伽_取消测试'), (SELECT id FROM member_user WHERE member_no = 'MEM_TEST_ZHANG'), (SELECT id FROM member_card_record WHERE member_id = (SELECT id FROM member_user WHERE member_no = 'MEM_TEST_ZHANG')), '2026-07-14 11:00:00', '0', '晚间瑜伽_取消测试', '2026-07-17 19:00:00', '2026-07-17 20:00:00', '瑜伽教室', '2026-07-14 11:00:00', '2026-07-14 11:00:00'), +((SELECT id FROM group_course WHERE course_name = '晚间瑜伽_取消测试'), (SELECT id FROM member_user WHERE member_no = 'MEM_TEST_LI'), (SELECT id FROM member_card_record WHERE member_id = (SELECT id FROM member_user WHERE member_no = 'MEM_TEST_LI')), '2026-07-14 11:30:00', '0', '晚间瑜伽_取消测试', '2026-07-17 19:00:00', '2026-07-17 20:00:00', '瑜伽教室', '2026-07-14 11:30:00', '2026-07-14 11:30:00'), +((SELECT id FROM group_course WHERE course_name = '晚间瑜伽_取消测试'), (SELECT id FROM member_user WHERE member_no = 'MEM_TEST_WANG'), (SELECT id FROM member_card_record WHERE member_id = (SELECT id FROM member_user WHERE member_no = 'MEM_TEST_WANG')), '2026-07-14 12:00:00', '0', '晚间瑜伽_取消测试', '2026-07-17 19:00:00', '2026-07-17 20:00:00', '瑜伽教室', '2026-07-14 12:00:00', '2026-07-14 12:00:00'); UPDATE group_course SET current_members = current_members + 3 WHERE course_name = '晚间瑜伽_取消测试'; --- H2: V14 数据统计预约 - 今天(6月24日) +-- H2: V14 数据统计预约 - 今天(7月14日) INSERT INTO group_course_booking (id, member_id, member_card_id, course_id, booking_time, status, created_at, updated_at) VALUES -(4001, 1001, 1, 3001, '2026-06-29 08:00:00', '2', '2026-06-23 20:00:00', '2026-06-29 08:30:00'), -(4002, 1002, 2, 3001, '2026-06-29 08:00:00', '2', '2026-06-23 21:00:00', '2026-06-29 08:30:00'), -(4003, 1003, 3, 3001, '2026-06-29 08:00:00', '3', '2026-06-23 22:00:00', '2026-06-29 09:00:00'), -(4004, 1004, 4, 3002, '2026-06-29 09:30:00', '2', '2026-06-23 19:00:00', '2026-06-29 09:30:00'), -(4005, 1005, 5, 3002, '2026-06-29 09:30:00', '1', '2026-06-23 20:30:00', '2026-06-29 09:00:00'), -(4006, 1006, 6, 3002, '2026-06-29 09:30:00', '2', '2026-06-23 21:30:00', '2026-06-29 09:30:00'), -(4007, 1007, 7, 3003, '2026-06-29 14:00:00', '2', '2026-06-23 22:30:00', '2026-06-29 14:00:00'), -(4008, 1008, 8, 3003, '2026-06-29 14:00:00', '3', '2026-06-29 08:00:00', '2026-06-29 14:00:00'), -(4009, 1009, 9, 3003, '2026-06-29 14:00:00', '2', '2026-06-29 09:00:00', '2026-06-29 14:00:00'), -(4010, 1010, 10, 3001, '2026-06-29 08:00:00', '2', '2026-06-29 07:00:00', '2026-06-29 08:00:00'), -(4011, 1011, 11, 3002, '2026-06-29 09:30:00', '1', '2026-06-29 08:30:00', '2026-06-29 09:00:00'), -(4012, 1012, 12, 3003, '2026-06-29 14:00:00', '2', '2026-06-29 10:00:00', '2026-06-29 14:00:00'); +(4001, 1001, 1, 3001, '2026-07-14 08:00:00', '2', '2026-07-08 20:00:00', '2026-07-14 08:30:00'), +(4002, 1002, 2, 3001, '2026-07-14 08:00:00', '2', '2026-07-08 21:00:00', '2026-07-14 08:30:00'), +(4003, 1003, 3, 3001, '2026-07-14 08:00:00', '3', '2026-07-08 22:00:00', '2026-07-14 09:00:00'), +(4004, 1004, 4, 3002, '2026-07-14 09:30:00', '2', '2026-07-08 19:00:00', '2026-07-14 09:30:00'), +(4005, 1005, 5, 3002, '2026-07-14 09:30:00', '1', '2026-07-08 20:30:00', '2026-07-14 09:00:00'), +(4006, 1006, 6, 3002, '2026-07-14 09:30:00', '2', '2026-07-08 21:30:00', '2026-07-14 09:30:00'), +(4007, 1007, 7, 3003, '2026-07-14 14:00:00', '2', '2026-07-08 22:30:00', '2026-07-14 14:00:00'), +(4008, 1008, 8, 3003, '2026-07-14 14:00:00', '3', '2026-07-14 08:00:00', '2026-07-14 14:00:00'), +(4009, 1009, 9, 3003, '2026-07-14 14:00:00', '2', '2026-07-14 09:00:00', '2026-07-14 14:00:00'), +(4010, 1010, 10, 3001, '2026-07-14 08:00:00', '2', '2026-07-14 07:00:00', '2026-07-14 08:00:00'), +(4011, 1011, 11, 3002, '2026-07-14 09:30:00', '1', '2026-07-14 08:30:00', '2026-07-14 09:00:00'), +(4012, 1012, 12, 3003, '2026-07-14 14:00:00', '2', '2026-07-14 10:00:00', '2026-07-14 14:00:00'); --- H3: V14 数据统计预约 - 昨天(6月23日) +-- H3: V14 数据统计预约 - 昨天(7月13日) INSERT INTO group_course_booking (id, member_id, member_card_id, course_id, booking_time, status, created_at, updated_at) VALUES -(4013, 1001, 1, 3001, '2026-06-23 08:00:00', '2', '2026-06-22 20:00:00', '2026-06-23 08:30:00'), -(4014, 1002, 2, 3001, '2026-06-23 08:00:00', '2', '2026-06-22 21:00:00', '2026-06-23 08:30:00'), -(4015, 1003, 3, 3002, '2026-06-23 09:30:00', '3', '2026-06-22 22:00:00', '2026-06-23 09:30:00'), -(4016, 1004, 4, 3002, '2026-06-23 09:30:00', '2', '2026-06-22 19:00:00', '2026-06-23 09:30:00'), -(4017, 1005, 5, 3003, '2026-06-23 14:00:00', '1', '2026-06-22 20:30:00', '2026-06-23 13:00:00'), -(4018, 1006, 6, 3003, '2026-06-23 14:00:00', '2', '2026-06-22 21:30:00', '2026-06-23 14:00:00'); +(4013, 1001, 1, 3001, '2026-07-08 08:00:00', '2', '2026-07-07 20:00:00', '2026-07-08 08:30:00'), +(4014, 1002, 2, 3001, '2026-07-08 08:00:00', '2', '2026-07-07 21:00:00', '2026-07-08 08:30:00'), +(4015, 1003, 3, 3002, '2026-07-08 09:30:00', '3', '2026-07-07 22:00:00', '2026-07-08 09:30:00'), +(4016, 1004, 4, 3002, '2026-07-08 09:30:00', '2', '2026-07-07 19:00:00', '2026-07-08 09:30:00'), +(4017, 1005, 5, 3003, '2026-07-08 14:00:00', '1', '2026-07-07 20:30:00', '2026-07-08 13:00:00'), +(4018, 1006, 6, 3003, '2026-07-08 14:00:00', '2', '2026-07-07 21:30:00', '2026-07-08 14:00:00'); --- H4: V14 数据统计预约 - 前天(6月22日) +-- H4: V14 数据统计预约 - 前天(7月12日) INSERT INTO group_course_booking (id, member_id, member_card_id, course_id, booking_time, status, created_at, updated_at) VALUES -(4019, 1001, 1, 3002, '2026-06-22 09:30:00', '2', '2026-06-21 20:00:00', '2026-06-22 09:30:00'), -(4020, 1002, 2, 3002, '2026-06-22 09:30:00', '2', '2026-06-21 21:00:00', '2026-06-22 09:30:00'), -(4021, 1003, 3, 3003, '2026-06-22 14:00:00', '2', '2026-06-21 22:00:00', '2026-06-22 14:00:00'), -(4022, 1004, 4, 3003, '2026-06-22 14:00:00', '3', '2026-06-21 19:00:00', '2026-06-22 14:00:00'); +(4019, 1001, 1, 3002, '2026-07-07 09:30:00', '2', '2026-07-06 20:00:00', '2026-07-07 09:30:00'), +(4020, 1002, 2, 3002, '2026-07-07 09:30:00', '2', '2026-07-06 21:00:00', '2026-07-07 09:30:00'), +(4021, 1003, 3, 3003, '2026-07-07 14:00:00', '2', '2026-07-06 22:00:00', '2026-07-07 14:00:00'), +(4022, 1004, 4, 3003, '2026-07-07 14:00:00', '3', '2026-07-06 19:00:00', '2026-07-07 14:00:00'); -- ============================================================ --- Section I: 签到记录(来源 V14,基于 2026-06-29) +-- Section I: 签到记录(来源 V14,基于 2026-07-14) -- ============================================================ INSERT INTO sign_in_record (id, member_id, sign_in_time, sign_in_type, sign_in_status, source, is_delete) VALUES -(2001, 1001, '2026-06-29 08:00:00', 'QR_CODE', 'SUCCESS', 'MINI_PROGRAM', false), -(2002, 1002, '2026-06-29 08:15:00', 'MANUAL', 'SUCCESS', 'PC_BACKEND', false), -(2003, 1003, '2026-06-29 08:30:00', 'FACE', 'SUCCESS', 'MINI_PROGRAM', false), -(2004, 1004, '2026-06-29 09:00:00', 'QR_CODE', 'SUCCESS', 'MINI_PROGRAM', false), -(2005, 1005, '2026-06-29 09:15:00', 'MANUAL', 'SUCCESS', 'PC_BACKEND', false), -(2006, 1006, '2026-06-29 09:30:00', 'QR_CODE', 'SUCCESS', 'MINI_PROGRAM', false), -(2007, 1007, '2026-06-29 10:00:00', 'FACE', 'SUCCESS', 'MINI_PROGRAM', false), -(2008, 1008, '2026-06-29 10:15:00', 'QR_CODE', 'SUCCESS', 'MINI_PROGRAM', false), -(2009, 1009, '2026-06-29 10:30:00', 'MANUAL', 'SUCCESS', 'PC_BACKEND', false), -(2010, 1010, '2026-06-29 11:00:00', 'QR_CODE', 'SUCCESS', 'MINI_PROGRAM', false), -(2011, 1011, '2026-06-29 11:15:00', 'FACE', 'FAIL', 'MINI_PROGRAM', false), -(2012, 1012, '2026-06-29 11:30:00', 'MANUAL', 'SUCCESS', 'PC_BACKEND', false); +(2001, 1001, '2026-07-14 08:00:00', 'QR_CODE', 'SUCCESS', 'MINI_PROGRAM', false), +(2002, 1002, '2026-07-14 08:15:00', 'MANUAL', 'SUCCESS', 'PC_BACKEND', false), +(2003, 1003, '2026-07-14 08:30:00', 'FACE', 'SUCCESS', 'MINI_PROGRAM', false), +(2004, 1004, '2026-07-14 09:00:00', 'QR_CODE', 'SUCCESS', 'MINI_PROGRAM', false), +(2005, 1005, '2026-07-14 09:15:00', 'MANUAL', 'SUCCESS', 'PC_BACKEND', false), +(2006, 1006, '2026-07-14 09:30:00', 'QR_CODE', 'SUCCESS', 'MINI_PROGRAM', false), +(2007, 1007, '2026-07-14 10:00:00', 'FACE', 'SUCCESS', 'MINI_PROGRAM', false), +(2008, 1008, '2026-07-14 10:15:00', 'QR_CODE', 'SUCCESS', 'MINI_PROGRAM', false), +(2009, 1009, '2026-07-14 10:30:00', 'MANUAL', 'SUCCESS', 'PC_BACKEND', false), +(2010, 1010, '2026-07-14 11:00:00', 'QR_CODE', 'SUCCESS', 'MINI_PROGRAM', false), +(2011, 1011, '2026-07-14 11:15:00', 'FACE', 'FAIL', 'MINI_PROGRAM', false), +(2012, 1012, '2026-07-14 11:30:00', 'MANUAL', 'SUCCESS', 'PC_BACKEND', false); INSERT INTO sign_in_record (id, member_id, sign_in_time, sign_in_type, sign_in_status, source, is_delete) VALUES -(2013, 1001, '2026-06-23 07:30:00', 'QR_CODE', 'SUCCESS', 'MINI_PROGRAM', false), -(2014, 1002, '2026-06-23 08:00:00', 'MANUAL', 'SUCCESS', 'PC_BACKEND', false), -(2015, 1003, '2026-06-23 08:30:00', 'FACE', 'SUCCESS', 'MINI_PROGRAM', false), -(2016, 1004, '2026-06-23 09:00:00', 'QR_CODE', 'SUCCESS', 'MINI_PROGRAM', false), -(2017, 1005, '2026-06-23 09:30:00', 'MANUAL', 'SUCCESS', 'PC_BACKEND', false), -(2018, 1006, '2026-06-23 10:00:00', 'QR_CODE', 'SUCCESS', 'MINI_PROGRAM', false), -(2019, 1007, '2026-06-23 10:30:00', 'FACE', 'SUCCESS', 'MINI_PROGRAM', false), -(2020, 1008, '2026-06-23 11:00:00', 'QR_CODE', 'SUCCESS', 'MINI_PROGRAM', false); +(2013, 1001, '2026-07-08 07:30:00', 'QR_CODE', 'SUCCESS', 'MINI_PROGRAM', false), +(2014, 1002, '2026-07-08 08:00:00', 'MANUAL', 'SUCCESS', 'PC_BACKEND', false), +(2015, 1003, '2026-07-08 08:30:00', 'FACE', 'SUCCESS', 'MINI_PROGRAM', false), +(2016, 1004, '2026-07-08 09:00:00', 'QR_CODE', 'SUCCESS', 'MINI_PROGRAM', false), +(2017, 1005, '2026-07-08 09:30:00', 'MANUAL', 'SUCCESS', 'PC_BACKEND', false), +(2018, 1006, '2026-07-08 10:00:00', 'QR_CODE', 'SUCCESS', 'MINI_PROGRAM', false), +(2019, 1007, '2026-07-08 10:30:00', 'FACE', 'SUCCESS', 'MINI_PROGRAM', false), +(2020, 1008, '2026-07-08 11:00:00', 'QR_CODE', 'SUCCESS', 'MINI_PROGRAM', false); INSERT INTO sign_in_record (id, member_id, sign_in_time, sign_in_type, sign_in_status, source, is_delete) VALUES -(2021, 1001, '2026-06-22 07:00:00', 'QR_CODE', 'SUCCESS', 'MINI_PROGRAM', false), -(2022, 1002, '2026-06-22 07:30:00', 'MANUAL', 'SUCCESS', 'PC_BACKEND', false), -(2023, 1003, '2026-06-22 08:00:00', 'FACE', 'FAIL', 'MINI_PROGRAM', false), -(2024, 1004, '2026-06-22 08:30:00', 'QR_CODE', 'SUCCESS', 'MINI_PROGRAM', false), -(2025, 1005, '2026-06-22 09:00:00', 'MANUAL', 'SUCCESS', 'PC_BACKEND', false); +(2021, 1001, '2026-07-07 07:00:00', 'QR_CODE', 'SUCCESS', 'MINI_PROGRAM', false), +(2022, 1002, '2026-07-07 07:30:00', 'MANUAL', 'SUCCESS', 'PC_BACKEND', false), +(2023, 1003, '2026-07-07 08:00:00', 'FACE', 'FAIL', 'MINI_PROGRAM', false), +(2024, 1004, '2026-07-07 08:30:00', 'QR_CODE', 'SUCCESS', 'MINI_PROGRAM', false), +(2025, 1005, '2026-07-07 09:00:00', 'MANUAL', 'SUCCESS', 'PC_BACKEND', false); -- ============================================================ --- Section J: 团课推荐测试数据(来源 V18,基于 2026-06-29) +-- Section J: 团课推荐测试数据(来源 V18,基于 2026-07-14) -- ============================================================ INSERT INTO group_course_recommend (course_id, recommend_title, recommend_content, recommend_reason, priority, is_active, create_by, created_at, updated_at) VALUES -((SELECT id FROM group_course WHERE course_name = '极速燃脂单车'), '本周热门推荐', '极速燃脂单车课程,跟随音乐节奏变换阻力和速度,体验爬坡与冲刺的快感,一节课消耗800大卡!', '教练专业,课程内容丰富,深受学员喜爱,燃脂效果显著', 20, true, 'admin', '2026-06-29 10:00:00', '2026-06-29 10:00:00'); +((SELECT id FROM group_course WHERE course_name = '极速燃脂单车'), '本周热门推荐', '极速燃脂单车课程,跟随音乐节奏变换阻力和速度,体验爬坡与冲刺的快感,一节课消耗800大卡!', '教练专业,课程内容丰富,深受学员喜爱,燃脂效果显著', 20, true, 'admin', '2026-07-14 10:00:00', '2026-07-14 10:00:00'); INSERT INTO group_course_recommend (course_id, recommend_title, recommend_content, recommend_reason, priority, is_active, create_by, created_at, updated_at) VALUES -((SELECT id FROM group_course WHERE course_name = '清晨流瑜伽'), '新手友好推荐', '清晨流瑜伽课程,适合有一定基础的学员,通过流畅的体式连接呼吸,唤醒身体能量。', '适合新手入门,教练耐心指导,课程节奏适中', 15, true, 'admin', '2026-06-29 11:00:00', '2026-06-29 11:00:00'); +((SELECT id FROM group_course WHERE course_name = '清晨流瑜伽'), '新手友好推荐', '清晨流瑜伽课程,适合有一定基础的学员,通过流畅的体式连接呼吸,唤醒身体能量。', '适合新手入门,教练耐心指导,课程节奏适中', 15, true, 'admin', '2026-07-14 11:00:00', '2026-07-14 11:00:00'); INSERT INTO group_course_recommend (course_id, recommend_title, recommend_content, recommend_reason, priority, is_active, create_by, created_at, updated_at) VALUES -((SELECT id FROM group_course WHERE course_name = '燃脂搏击'), '高强度燃脂', '燃脂搏击课程,高强度间歇训练,配合音乐快速燃脂,释放压力。', '高强度训练,适合进阶学员,快速燃脂塑形', 10, false, 'admin', '2026-06-29 12:00:00', '2026-06-29 12:00:00'); +((SELECT id FROM group_course WHERE course_name = '燃脂搏击'), '高强度燃脂', '燃脂搏击课程,高强度间歇训练,配合音乐快速燃脂,释放压力。', '高强度训练,适合进阶学员,快速燃脂塑形', 10, false, 'admin', '2026-07-14 12:00:00', '2026-07-14 12:00:00'); INSERT INTO group_course_recommend (course_id, recommend_title, recommend_content, recommend_reason, priority, is_active, create_by, created_at, updated_at) VALUES -((SELECT id FROM group_course WHERE course_name = '哈他瑜伽'), '基础瑜伽推荐', '基础哈他瑜伽课程,适合所有级别学员,通过基础体式练习提升身体柔韧性和平衡能力。', '零基础友好,适合所有健身水平,放松身心', 12, true, 'coach_li', '2026-06-29 13:00:00', '2026-06-29 13:00:00'); +((SELECT id FROM group_course WHERE course_name = '哈他瑜伽'), '基础瑜伽推荐', '基础哈他瑜伽课程,适合所有级别学员,通过基础体式练习提升身体柔韧性和平衡能力。', '零基础友好,适合所有健身水平,放松身心', 12, true, 'coach_li', '2026-07-14 13:00:00', '2026-07-14 13:00:00'); INSERT INTO group_course_recommend (course_id, recommend_title, recommend_content, recommend_reason, priority, is_active, create_by, created_at, updated_at) VALUES -((SELECT id FROM group_course WHERE course_name = '蜜桃臀塑造'), '塑形热门课程', '蜜桃臀塑造课程,针对性训练臀部肌肉群,打造完美曲线。', '专业私教指导,动作标准,效果显著,深受女性学员喜爱', 18, true, 'coach_li', '2026-06-29 09:15:00', '2026-06-29 09:15:00'); +((SELECT id FROM group_course WHERE course_name = '蜜桃臀塑造'), '塑形热门课程', '蜜桃臀塑造课程,针对性训练臀部肌肉群,打造完美曲线。', '专业私教指导,动作标准,效果显著,深受女性学员喜爱', 18, true, 'coach_li', '2026-07-14 09:15:00', '2026-07-14 09:15:00'); INSERT INTO group_course_recommend (course_id, recommend_title, recommend_content, recommend_reason, priority, is_active, create_by, created_at, updated_at) VALUES -((SELECT id FROM group_course WHERE course_name = '午间冥想放松'), '午间放松推荐', '午间冥想放松课程,通过呼吸和正念冥想,深度放松身心,缓解工作压力。', '适合上班族,午间放松充电,提升下午工作效率', 8, true, 'admin', '2026-06-29 09:00:00', '2026-06-29 09:00:00'); +((SELECT id FROM group_course WHERE course_name = '午间冥想放松'), '午间放松推荐', '午间冥想放松课程,通过呼吸和正念冥想,深度放松身心,缓解工作压力。', '适合上班族,午间放松充电,提升下午工作效率', 8, true, 'admin', '2026-07-14 09:00:00', '2026-07-14 09:00:00'); INSERT INTO group_course_recommend (course_id, recommend_title, recommend_content, recommend_reason, priority, is_active, create_by, created_at, updated_at) VALUES -((SELECT id FROM group_course WHERE course_name = '极速燃脂单车'), '减脂首选课程', '想要快速减脂?极速燃脂单车是你的最佳选择!专业教练带领,科学训练计划。', '减脂效果最佳,课程强度适中,适合想要快速瘦身的学员', 16, true, 'coach_zhang', '2026-06-29 14:00:00', '2026-06-29 14:00:00'); +((SELECT id FROM group_course WHERE course_name = '极速燃脂单车'), '减脂首选课程', '想要快速减脂?极速燃脂单车是你的最佳选择!专业教练带领,科学训练计划。', '减脂效果最佳,课程强度适中,适合想要快速瘦身的学员', 16, true, 'coach_zhang', '2026-07-14 14:00:00', '2026-07-14 14:00:00'); INSERT INTO group_course_recommend (course_id, recommend_title, recommend_content, recommend_reason, priority, is_active, create_by, created_at, updated_at) VALUES -((SELECT id FROM group_course WHERE course_name = '清晨流瑜伽'), '晨练优选', '清晨流瑜伽,唤醒身体能量,开启活力一天!适合晨练爱好者。', '晨练最佳选择,提升身体活力,改善精神状态', 14, true, 'coach_wang', '2026-06-29 15:00:00', '2026-06-29 15:00:00'); +((SELECT id FROM group_course WHERE course_name = '清晨流瑜伽'), '晨练优选', '清晨流瑜伽,唤醒身体能量,开启活力一天!适合晨练爱好者。', '晨练最佳选择,提升身体活力,改善精神状态', 14, true, 'coach_wang', '2026-07-14 15:00:00', '2026-07-14 15:00:00'); INSERT INTO group_course_recommend (course_id, recommend_title, recommend_content, recommend_reason, priority, is_active, create_by, created_at, updated_at) VALUES -((SELECT id FROM group_course WHERE course_name = '哈他瑜伽'), '身心平衡推荐', '哈他瑜伽课程,通过体式练习和呼吸控制,达到身心平衡,提升整体健康水平。', '改善身体柔韧性,增强核心力量,提升身体协调性', 11, true, 'coach_li', '2026-06-29 16:00:00', '2026-06-29 16:00:00'); +((SELECT id FROM group_course WHERE course_name = '哈他瑜伽'), '身心平衡推荐', '哈他瑜伽课程,通过体式练习和呼吸控制,达到身心平衡,提升整体健康水平。', '改善身体柔韧性,增强核心力量,提升身体协调性', 11, true, 'coach_li', '2026-07-14 16:00:00', '2026-07-14 16:00:00'); INSERT INTO group_course_recommend (course_id, recommend_title, recommend_content, recommend_reason, priority, is_active, create_by, created_at, updated_at) VALUES -((SELECT id FROM group_course WHERE course_name = '午间冥想放松'), '职场减压课程', '午间冥想放松,专为职场人士设计,快速缓解工作压力,提升工作状态。', '职场减压首选,课程时间短,效果显著', 9, false, 'admin', '2026-06-29 10:00:00', '2026-06-29 10:00:00'); +((SELECT id FROM group_course WHERE course_name = '午间冥想放松'), '职场减压课程', '午间冥想放松,专为职场人士设计,快速缓解工作压力,提升工作状态。', '职场减压首选,课程时间短,效果显著', 9, false, 'admin', '2026-07-14 10:00:00', '2026-07-14 10:00:00'); -- ============================================================ -- Section K: 统计说明 -- ============================================================ --- 今日(2026-06-29)统计预期: +-- 今日(2026-07-14)统计预期: -- 新增会员: 3 (1005, 1006, 1007) -- 签到会员: 12 -- 预约总数: 12 (4001-4012),取消: 2,出席: 8,缺席: 2 diff --git a/gym-manage-api/manage-db/src/main/resources/db/migration/V23__Add_Coach_Test_Data.sql b/gym-manage-api/manage-db/src/main/resources/db/migration/V23__Add_Coach_Test_Data.sql index 48bd01a..2de0c04 100644 --- a/gym-manage-api/manage-db/src/main/resources/db/migration/V23__Add_Coach_Test_Data.sql +++ b/gym-manage-api/manage-db/src/main/resources/db/migration/V23__Add_Coach_Test_Data.sql @@ -1,7 +1,7 @@ -- ============================================================ -- V23: 教练账号测试数据 -- 描述: 创建教练角色、教练用户及相关团课测试数据 --- 日期: 2026-06-22 +-- 日期: 2026-07-14 -- ============================================================ -- ============================================================ diff --git a/gym-manage-api/manage-db/src/main/resources/db/migration/V24__Add_Second_Coach_Test_Data.sql b/gym-manage-api/manage-db/src/main/resources/db/migration/V24__Add_Second_Coach_Test_Data.sql index 3aee92c..fbb52d8 100644 --- a/gym-manage-api/manage-db/src/main/resources/db/migration/V24__Add_Second_Coach_Test_Data.sql +++ b/gym-manage-api/manage-db/src/main/resources/db/migration/V24__Add_Second_Coach_Test_Data.sql @@ -4,7 +4,7 @@ -- 1. 新增测试教练用户 coachli(李教练) -- 2. 为李教练分配教练角色(继承教练的有限权限) -- 3. 为李教练分配未来一周的测试团课 --- 日期: 2026-06-22 +-- 日期: 2026-07-14 -- ============================================================ -- ============================================================ diff --git a/gym-manage-api/manage-db/src/main/resources/db/migration/V32__Add_Statistics_Test_Data_Today.sql b/gym-manage-api/manage-db/src/main/resources/db/migration/V32__Add_Statistics_Test_Data_Today.sql deleted file mode 100644 index c1a3b8f..0000000 --- a/gym-manage-api/manage-db/src/main/resources/db/migration/V32__Add_Statistics_Test_Data_Today.sql +++ /dev/null @@ -1,66 +0,0 @@ --- ============================================================ --- V32: 新增测试数据(用于数据统计看板展示) --- 描述: 基于当天日期,创建跨多个时间区间的会员、签到、预约数据 --- 覆盖时间区间: 今日、本周、本月、近30天、近90天、今年 --- ============================================================ - --- ============================================================ --- 1. 新增会员(100人,ID 2001-2100) --- 按时间分布:今日10人 / 本周20人 / 本月30人 / 近30天60人 / 近90天85人 / 今年100人 --- 创建日期从 180 天前均匀分布到今天 --- ============================================================ -INSERT INTO member_user (id, member_no, nickname, phone, gender, subscribed, is_deleted, created_at, updated_at) -SELECT - 2000 + g, - 'M' || TO_CHAR(CURRENT_DATE - (g % 180), 'YYYYMMDD') || LPAD(g::text, 3, '0'), - '测试会员' || g, - '138' || LPAD(g::text, 8, '0'), - (g % 3)::int, - false, - false, - (CURRENT_DATE - (g % 180) + TIME '10:00:00')::timestamp, - (CURRENT_DATE - (g % 180) + TIME '10:00:00')::timestamp -FROM generate_series(1, 100) AS g -ON CONFLICT (id) DO NOTHING; - --- ============================================================ --- 2. 签到记录(80条,使用已有会员 ID 1001-1010) --- 按时间分布:最近90天,每天约1条 --- 签到类型:QR_CODE/MANUAL/FACE 均匀分布 --- 签到状态:SUCCESS(90%) / FAILED(10%) --- ============================================================ -INSERT INTO sign_in_record (member_id, sign_in_time, sign_in_type, sign_in_status, source, is_delete, created_at, updated_at) -SELECT - 1001 + (g % 10), - (CURRENT_DATE - (g % 90) + TIME '08:00:00' + (INTERVAL '1 hour' * (g % 12)))::timestamp, - CASE (g % 3) WHEN 0 THEN 'QR_CODE' WHEN 1 THEN 'MANUAL' ELSE 'FACE' END, - CASE WHEN (g % 10) = 0 THEN 'FAILED' ELSE 'SUCCESS' END, - CASE (g % 2) WHEN 0 THEN 'MINI_PROGRAM' ELSE 'PC_BACKEND' END, - false, - (CURRENT_DATE - (g % 90) + TIME '08:00:00')::timestamp, - (CURRENT_DATE - (g % 90) + TIME '08:00:00')::timestamp -FROM generate_series(1, 80) AS g; - --- ============================================================ --- 3. 团课预约(40条,使用已有会员 ID 1001-1010) --- 按时间分布:最近90天 --- 状态分布:出勤(2) 60% / 取消(1) 20% / 缺席(3) 20% --- ============================================================ -INSERT INTO group_course_booking (id, course_id, member_id, member_card_id, booking_time, status, created_at, updated_at) -SELECT - 5100 + g, - CASE (g % 3) WHEN 0 THEN 3001 WHEN 1 THEN 3002 ELSE 3003 END, - 1001 + (g % 10), - (SELECT id FROM member_card_record ORDER BY id LIMIT 1), - (CURRENT_DATE - (g % 90) + TIME '10:00:00')::timestamp, - CASE WHEN (g % 5) = 0 THEN '1' WHEN (g % 5) = 1 THEN '3' ELSE '2' END, - (CURRENT_DATE - (g % 90) + TIME '10:00:00')::timestamp, - (CURRENT_DATE - (g % 90) + TIME '10:00:00')::timestamp -FROM generate_series(1, 40) AS g; - --- ============================================================ --- 4. 重置序列(防止后续自动插入 ID 冲突) --- ============================================================ -SELECT setval('member_user_id_seq', (SELECT COALESCE(MAX(id), 1) FROM member_user)); -SELECT setval('group_course_booking_id_seq', (SELECT COALESCE(MAX(id), 1) FROM group_course_booking)); -SELECT setval('sign_in_record_id_seq', (SELECT COALESCE(MAX(id), 1) FROM sign_in_record)); diff --git a/gym-manage-api/manage-db/src/main/resources/db/migration/V32__Make_member_card_id_nullable.sql b/gym-manage-api/manage-db/src/main/resources/db/migration/V32__Make_member_card_id_nullable.sql new file mode 100644 index 0000000..9ef0cb8 --- /dev/null +++ b/gym-manage-api/manage-db/src/main/resources/db/migration/V32__Make_member_card_id_nullable.sql @@ -0,0 +1,8 @@ +-- ============================================================ +-- V32: 将 group_course_booking 表的 member_card_id 改为可空 +-- 说明: 预约团课不再需要会员卡,member_card_id 允许为空 +-- 日期: 2026-07-14 +-- ============================================================ + +ALTER TABLE group_course_booking + ALTER COLUMN member_card_id DROP NOT NULL; diff --git a/gym-manage-api/manage-file/src/main/java/cn/novalon/gym/manage/file/core/service/ISysFileService.java b/gym-manage-api/manage-file/src/main/java/cn/novalon/gym/manage/file/core/service/ISysFileService.java index 45f387d..22f8b33 100644 --- a/gym-manage-api/manage-file/src/main/java/cn/novalon/gym/manage/file/core/service/ISysFileService.java +++ b/gym-manage-api/manage-file/src/main/java/cn/novalon/gym/manage/file/core/service/ISysFileService.java @@ -15,6 +15,17 @@ public interface ISysFileService { Mono uploadFile(FilePart filePart, String username); + /** + * 保存字节数组为文件(用于服务端生成的文件,如二维码) + * + * @param content 文件字节内容 + * @param filename 原始文件名 + * @param contentType MIME类型 + * @param username 操作用户名 + * @return 保存后的文件信息 + */ + Mono saveBytes(byte[] content, String filename, String contentType, String username); + Mono downloadFile(Long id); Mono deleteFile(Long id); diff --git a/gym-manage-api/manage-file/src/main/java/cn/novalon/gym/manage/file/core/service/impl/SysFileServiceImpl.java b/gym-manage-api/manage-file/src/main/java/cn/novalon/gym/manage/file/core/service/impl/SysFileServiceImpl.java index c13ec08..b02a88d 100644 --- a/gym-manage-api/manage-file/src/main/java/cn/novalon/gym/manage/file/core/service/impl/SysFileServiceImpl.java +++ b/gym-manage-api/manage-file/src/main/java/cn/novalon/gym/manage/file/core/service/impl/SysFileServiceImpl.java @@ -29,6 +29,43 @@ public class SysFileServiceImpl implements ISysFileService { this.uploadDir = uploadDir; } + @Override + public Mono saveBytes(byte[] content, String filename, String contentType, String username) { + String fileExtension = filename.contains(".") + ? filename.substring(filename.lastIndexOf(".")) + : ".png"; + String newFileName = UUID.randomUUID().toString() + fileExtension; + + Path uploadPath = Paths.get(uploadDir); + return Mono.fromCallable(() -> { + if (!Files.exists(uploadPath)) { + Files.createDirectories(uploadPath); + } + return uploadPath; + }) + .flatMap(path -> { + Path filePath = path.resolve(newFileName); + return Mono.fromCallable(() -> { + Files.write(filePath, content); + return filePath; + }); + }) + .flatMap(filePath -> Mono.fromCallable(() -> { + long fileSize = Files.size(filePath); + + SysFile sysFile = new SysFile(); + sysFile.setFileName(filename); + sysFile.setFilePath(filePath.toString()); + sysFile.setFileSize(fileSize); + sysFile.setFileType(contentType != null ? contentType : "image/png"); + sysFile.setStorageType("LOCAL"); + sysFile.setCreateBy(username); + sysFile.setCreatedAt(LocalDateTime.now()); + + return sysFile; + }).flatMap(fileRepository::save)); + } + @Override public Flux getAllFiles() { return fileRepository.findByDeletedAtIsNullOrderByCreatedAtDesc(); diff --git a/gym-manage-web/src/views/groupcourse/GroupCourseManagement.vue b/gym-manage-web/src/views/groupcourse/GroupCourseManagement.vue index ba45409..570695d 100644 --- a/gym-manage-web/src/views/groupcourse/GroupCourseManagement.vue +++ b/gym-manage-web/src/views/groupcourse/GroupCourseManagement.vue @@ -91,11 +91,14 @@ {{ row.currentMembers || 0 }}/{{ row.maxMembers || 0 }} - + + + + +
+
+ 签到二维码 + +
+

扫码签到参加团课

+
+ +
@@ -395,6 +425,75 @@ const uploadCover = async (options: any) => { const coverPreviewSrc = ref('') +// ===== 二维码弹窗相关 ===== +const qrDialogVisible = ref(false) +const qrLoading = ref(false) +const qrImageUrl = ref('') +const currentCourseName = ref('') + +/** + * 从 qrCodePath 中提取文件 ID + * 支持格式:/api/files/preview/{id} 或旧版 OSS URL + */ +const extractFileId = (path: string | undefined): string | null => { + if (!path) return null + // 新格式: /api/files/{id}/preview + const match = path.match(/\/files\/(\d+)\/preview/) + if (match) return match[1] + // 旧格式 OSS URL: 直接作为图片 URL 使用 + if (path.startsWith('http')) return path + // 纯数字 ID + if (/^\d+$/.test(path)) return path + return null +} + +/** + * 显示团课签到二维码 + */ +const showQrCode = async (row: GroupCourse) => { + currentCourseName.value = row.courseName + qrDialogVisible.value = true + qrLoading.value = true + qrImageUrl.value = '' + + const fileId = extractFileId(row.qrCodePath) + if (!fileId) { + qrLoading.value = false + return + } + + try { + if (fileId.startsWith('http')) { + // 旧版 OSS URL,直接使用 + qrImageUrl.value = fileId + } else { + // 新版文件 ID,通过预览接口获取 + const blob: any = await request.get(`/files/${fileId}/preview`, { responseType: 'blob' }) + // 释放旧 URL + if (qrImageUrl.value) URL.revokeObjectURL(qrImageUrl.value) + qrImageUrl.value = URL.createObjectURL(blob) + } + } catch { + ElMessage.error('获取二维码失败') + } finally { + qrLoading.value = false + } +} + +/** + * 下载二维码图片 + */ +const downloadQrCode = () => { + if (!qrImageUrl.value) return + const a = document.createElement('a') + a.href = qrImageUrl.value + a.download = `签到二维码_${currentCourseName.value}.png` + document.body.appendChild(a) + a.click() + document.body.removeChild(a) + ElMessage.success('二维码下载成功') +} + const loadCoverPreview = async (val: string) => { if (!val) { coverPreviewSrc.value = '' @@ -576,4 +675,36 @@ onMounted(() => { color: #c0c4cc; font-size: 12px; } + +/* 二维码弹窗样式 */ +.qr-dialog-body { + display: flex; + flex-direction: column; + align-items: center; + padding: 16px 0; +} + +.qr-image-wrapper { + width: 260px; + height: 260px; + display: flex; + align-items: center; + justify-content: center; + border: 1px solid #ebeef5; + border-radius: 8px; + background: #fafafa; + overflow: hidden; +} + +.qr-image { + width: 100%; + height: 100%; + object-fit: contain; +} + +.qr-tip { + margin-top: 16px; + color: #909399; + font-size: 14px; +}