diff --git a/gym-manage-api/gym-groupCourse/src/main/java/cn/novalon/gym/manage/groupcourse/dao/GroupCourseBookingDao.java b/gym-manage-api/gym-groupCourse/src/main/java/cn/novalon/gym/manage/groupcourse/dao/GroupCourseBookingDao.java index 766c617..ec0bc5c 100644 --- a/gym-manage-api/gym-groupCourse/src/main/java/cn/novalon/gym/manage/groupcourse/dao/GroupCourseBookingDao.java +++ b/gym-manage-api/gym-groupCourse/src/main/java/cn/novalon/gym/manage/groupcourse/dao/GroupCourseBookingDao.java @@ -61,6 +61,12 @@ public interface GroupCourseBookingDao extends R2dbcRepository countByCourseIdAndStatusAndDeletedAtIsNull(Long courseId, String status); + /** + * 统计会员取消的预约次数 + */ + @org.springframework.data.r2dbc.repository.Query("SELECT COUNT(*) FROM group_course_booking WHERE member_id = :memberId AND status = '1' AND deleted_at IS NULL") + Mono countCancelledByMemberId(Long memberId); + /** * 查询会员是否有时间冲突的预约(状态为已预约且未取消) * 时间冲突条件:新课程的开始时间 < 已预约课程的结束时间 且 新课程的结束时间 > 已预约课程的开始时间 diff --git a/gym-manage-api/gym-groupCourse/src/main/java/cn/novalon/gym/manage/groupcourse/handler/BookingSagaHandler.java b/gym-manage-api/gym-groupCourse/src/main/java/cn/novalon/gym/manage/groupcourse/handler/BookingSagaHandler.java index 17249b3..582b497 100644 --- a/gym-manage-api/gym-groupCourse/src/main/java/cn/novalon/gym/manage/groupcourse/handler/BookingSagaHandler.java +++ b/gym-manage-api/gym-groupCourse/src/main/java/cn/novalon/gym/manage/groupcourse/handler/BookingSagaHandler.java @@ -9,11 +9,13 @@ import cn.novalon.gym.manage.member.entity.MemberCardRecord; import cn.novalon.gym.manage.member.enums.MemberCardType; import cn.novalon.gym.manage.member.repository.MemberCardRepository; import cn.novalon.gym.manage.member.service.IMemberCardRecordService; +import cn.novalon.gym.manage.member.service.IMemberStoredCardService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; import reactor.core.publisher.Mono; +import java.math.BigDecimal; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -34,6 +36,7 @@ public class BookingSagaHandler { private final IGroupCourseBookingRepository bookingRepository; private final IGroupCourseRepository courseRepository; private final IMemberCardRecordService memberCardRecordService; + private final IMemberStoredCardService memberStoredCardService; private final MemberCardRepository memberCardRepository; private final GroupCourseRedisService redisService; @@ -44,10 +47,10 @@ public class BookingSagaHandler { * * 步骤: * 1. 保存预约记录 - * 2. 扣减会员卡权益 + * 2. 扣减储值卡余额(储值卡类型) * 3. 更新课程当前人数 */ - public Mono executeBooking(GroupCourseBooking booking, Long recordId) { + public Mono executeBooking(GroupCourseBooking booking, Long recordId, BigDecimal storedValueAmount) { List steps = new ArrayList<>(); List rollbackSteps = new ArrayList<>(); @@ -60,11 +63,11 @@ public class BookingSagaHandler { steps.add(step1); rollbackSteps.add(0, step1); - // 步骤2:扣减会员卡权益(根据卡类型决定扣除次数还是金额) + // 步骤2:扣减权益(根据卡类型) SagaStep step2 = new SagaStep( - "扣减会员卡权益", - deductCardUsageByCardType(booking.getMemberId(), recordId), - Mono.defer(() -> restoreCardUsageByCardType(booking.getMemberId(), recordId)) + "扣减权益", + deductCardUsageByCardType(booking.getMemberId(), recordId, storedValueAmount), + Mono.defer(() -> restoreCardUsageByCardType(booking.getMemberId(), recordId, storedValueAmount)) ); steps.add(step2); rollbackSteps.add(0, step2); @@ -94,7 +97,7 @@ public class BookingSagaHandler { /** * 根据会员卡类型扣减权益 */ - private Mono deductCardUsageByCardType(Long memberId, Long recordId) { + private Mono deductCardUsageByCardType(Long memberId, Long recordId, BigDecimal storedValueAmount) { return memberCardRecordService.findById(recordId) .switchIfEmpty(Mono.error(new RuntimeException("会员卡记录不存在"))) .flatMap(record -> { @@ -109,14 +112,13 @@ public class BookingSagaHandler { switch (cardType) { case COUNT_CARD: - // 次数卡不再支持团课预约 - return Mono.error(new RuntimeException("团课预约仅支持储值卡支付")); + return Mono.error(new RuntimeException("团课预约仅支持储值卡和时长卡支付")); case STORED_VALUE_CARD: - // 储值卡扣除金额 - return deductCardUsage(recordId, 0, DEFAULT_GROUP_COURSE_PRICE); + return deductStoredCardBalance(memberId, storedValueAmount); case TIME_CARD: - // 时长卡不扣除,但需验证有效期 - return validateTimeCard(record); + // 时长卡验证有效期后,仍须从储值卡扣款 + return validateTimeCard(record) + .then(deductStoredCardBalance(memberId, storedValueAmount)); default: return Mono.error(new RuntimeException("不支持的会员卡类型: " + cardType)); } @@ -153,11 +155,10 @@ public class BookingSagaHandler { /** * 根据会员卡类型恢复权益 */ - private Mono restoreCardUsageByCardType(Long memberId, Long recordId) { + private Mono restoreCardUsageByCardType(Long memberId, Long recordId, BigDecimal storedValueAmount) { return memberCardRecordService.findById(recordId) .switchIfEmpty(Mono.error(new RuntimeException("会员卡记录不存在,无法恢复权益"))) .flatMap(record -> { - // 验证会员卡归属(memberId为null时跳过验证) if (memberId != null && !record.getMemberId().equals(memberId)) { return Mono.error(new RuntimeException("会员卡不归属当前用户")); } @@ -168,12 +169,29 @@ public class BookingSagaHandler { switch (cardType) { case COUNT_CARD: - // 次数卡不再支持团课预约,无需恢复 return Mono.empty(); case STORED_VALUE_CARD: - return restoreCardUsage(recordId, 0, DEFAULT_GROUP_COURSE_PRICE); + // 返还储值卡余额(回滚时全额返还) + BigDecimal amount = storedValueAmount != null ? storedValueAmount : BigDecimal.valueOf(DEFAULT_GROUP_COURSE_PRICE); + return memberStoredCardService.recharge(memberId, amount) + .flatMap(rows -> { + if (rows > 0) { + log.info("储值卡回滚返还成功: memberId={}, amount={}", memberId, amount); + return Mono.empty(); + } + return Mono.empty(); + }); case TIME_CARD: - return Mono.empty(); + // 时长卡回滚时也返还储值卡 + BigDecimal timeAmount = storedValueAmount != null ? storedValueAmount : BigDecimal.valueOf(DEFAULT_GROUP_COURSE_PRICE); + return memberStoredCardService.recharge(memberId, timeAmount) + .flatMap(rows -> { + if (rows > 0) { + log.info("储值卡回滚返还成功(时长卡): memberId={}, amount={}", memberId, timeAmount); + return Mono.empty(); + } + return Mono.empty(); + }); default: return Mono.error(new RuntimeException("不支持的会员卡类型: " + cardType)); } @@ -184,7 +202,7 @@ public class BookingSagaHandler { /** * 执行取消预约事务 */ - public Mono executeCancelBooking(Long bookingId, Long courseId, Long recordId, Long memberId) { + public Mono executeCancelBooking(Long bookingId, Long courseId, Long recordId, Long memberId, BigDecimal storedValueAmount, long cancelCount) { List steps = new ArrayList<>(); List rollbackSteps = new ArrayList<>(); @@ -197,11 +215,12 @@ public class BookingSagaHandler { steps.add(step1); rollbackSteps.add(0, step1); - // 步骤2:恢复会员卡权益 + // 步骤2:返还储值卡余额(含手续费逻辑) SagaStep step2 = new SagaStep( - "恢复会员卡权益", - restoreCardUsageByCardType(memberId, recordId), - Mono.defer(() -> deductCardUsageByCardType(memberId, recordId)) + "返还储值卡余额", + refundStoredCardWithFee(memberId, storedValueAmount, cancelCount), + // 回滚时重新扣减 + Mono.defer(() -> deductCardUsageByCardType(memberId, recordId, storedValueAmount)) ); steps.add(step2); rollbackSteps.add(0, step2); @@ -253,54 +272,32 @@ public class BookingSagaHandler { return bookingRepository.deleteById(bookingId); } - private Mono deductCardUsage(Long recordId, Integer deductTimes, Double deductAmount) { - if (deductTimes == 0 && deductAmount == 0.0) { - return Mono.empty(); - } - - return memberCardRecordService.findById(recordId) - .switchIfEmpty(Mono.error(new RuntimeException("会员卡记录不存在"))) - .flatMap(record -> { - cn.novalon.gym.manage.member.enums.MemberCardRecordStatus status = record.getStatus(); - if (status != cn.novalon.gym.manage.member.enums.MemberCardRecordStatus.ACTIVE) { - return Mono.error(new RuntimeException("会员卡状态无效,当前状态: " + (status != null ? status.getDesc() : "未知"))); + /** + * 从储值卡扣减余额 + */ + private Mono deductStoredCardBalance(Long memberId, BigDecimal storedValueAmount) { + BigDecimal amount = storedValueAmount != null && storedValueAmount.compareTo(BigDecimal.ZERO) > 0 + ? storedValueAmount : BigDecimal.valueOf(DEFAULT_GROUP_COURSE_PRICE); + return memberStoredCardService.consume(memberId, amount) + .flatMap(rows -> { + if (rows == 0) { + return Mono.error(new RuntimeException("储值卡扣减失败,余额不足")); } - java.time.LocalDateTime expireTime = record.getExpireTime(); - if (expireTime != null && expireTime.isBefore(java.time.LocalDateTime.now())) { - return Mono.error(new RuntimeException("会员卡已过期")); - } - if (record.getRemainingTimes() != null && deductTimes > 0 && record.getRemainingTimes() < deductTimes) { - return Mono.error(new RuntimeException("会员卡剩余次数不足,当前剩余: " + record.getRemainingTimes() + "次")); - } - if (record.getRemainingAmount() != null && deductAmount > 0 && record.getRemainingAmount() < deductAmount) { - return Mono.error(new RuntimeException("会员卡余额不足,当前剩余: " + record.getRemainingAmount())); - } - return memberCardRecordService.deductUsage(recordId, deductTimes, deductAmount) - .flatMap(rows -> { - if (rows == 0) { - return Mono.error(new RuntimeException("扣减会员卡权益失败,请重试")); - } - return Mono.empty(); - }); + log.info("储值卡扣减成功: memberId={}, amount={}", memberId, amount); + return Mono.empty(); }); } - private Mono restoreCardUsage(Long recordId, Integer addTimes, Double addAmount) { - if (addTimes == 0 && addAmount == 0.0) { - return Mono.empty(); - } - - return memberCardRecordService.findById(recordId) - .switchIfEmpty(Mono.error(new RuntimeException("会员卡记录不存在,无法恢复权益"))) - .flatMap(record -> { - // 使用当前记录的过期时间,避免清空过期时间 - return memberCardRecordService.renewCard(recordId, addTimes, addAmount, record.getExpireTime()) - .flatMap(rows -> { - if (rows == 0) { - return Mono.error(new RuntimeException("恢复会员卡权益失败,请重试")); - } - return Mono.empty(); - }); + /** + * 返还储值卡余额(含手续费逻辑,仅用于取消预约) + */ + private Mono refundStoredCardWithFee(Long memberId, BigDecimal storedValueAmount, long cancelCount) { + BigDecimal amount = storedValueAmount != null && storedValueAmount.compareTo(BigDecimal.ZERO) > 0 + ? storedValueAmount : BigDecimal.valueOf(DEFAULT_GROUP_COURSE_PRICE); + return memberStoredCardService.refundBalanceWithFee(memberId, amount, cancelCount) + .flatMap(refundedAmount -> { + log.info("储值卡退款成功(含手续费): memberId={}, cancelCount={}, 退款金额={}", memberId, cancelCount, refundedAmount); + return Mono.empty(); }); } 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 e879c5c..954d773 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,6 +2,7 @@ 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; @@ -23,9 +24,12 @@ import java.util.Map; public class GroupCourseBookingHandler { private final IGroupCourseBookingService bookingService; + private final IMemberStoredCardService memberStoredCardService; - public GroupCourseBookingHandler(IGroupCourseBookingService bookingService) { + public GroupCourseBookingHandler(IGroupCourseBookingService bookingService, + IMemberStoredCardService memberStoredCardService) { this.bookingService = bookingService; + this.memberStoredCardService = memberStoredCardService; } /** @@ -45,24 +49,35 @@ public class GroupCourseBookingHandler { if (body.get("memberCardRecordId") == null) { return buildErrorResponse("请提供会员卡记录ID"); } + if (body.get("payPassword") == null || body.get("payPassword").toString().isEmpty()) { + return buildErrorResponse("请输入支付密码"); + } - Long courseId = ((Number) body.get("courseId")).longValue(); - Long memberId = ((Number) body.get("memberId")).longValue(); - Long memberCardRecordId = ((Number) body.get("memberCardRecordId")).longValue(); + 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 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 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); + }); }); }); } @@ -76,21 +91,32 @@ public class GroupCourseBookingHandler { return request.bodyToMono(Map.class) .flatMap(body -> { - Long memberId = ((Number) body.get("memberId")).longValue(); + 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 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 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); + }); }); }); } @@ -129,6 +155,19 @@ public class GroupCourseBookingHandler { .body(bookingService.getBookingsByCourseId(courseId), GroupCourseBooking.class); } + /** + * 安全转换为 Long,支持 Number 和 String 类型 + */ + private Long toLong(Object value, String fieldName) { + if (value instanceof Number) { + return ((Number) value).longValue(); + } + if (value instanceof String) { + return Long.valueOf((String) value); + } + throw new IllegalArgumentException("参数 " + fieldName + " 类型不正确"); + } + /** * 构建错误响应 */ diff --git a/gym-manage-api/gym-groupCourse/src/main/java/cn/novalon/gym/manage/groupcourse/repository/IGroupCourseBookingRepository.java b/gym-manage-api/gym-groupCourse/src/main/java/cn/novalon/gym/manage/groupcourse/repository/IGroupCourseBookingRepository.java index 99d832e..cabc940 100644 --- a/gym-manage-api/gym-groupCourse/src/main/java/cn/novalon/gym/manage/groupcourse/repository/IGroupCourseBookingRepository.java +++ b/gym-manage-api/gym-groupCourse/src/main/java/cn/novalon/gym/manage/groupcourse/repository/IGroupCourseBookingRepository.java @@ -89,4 +89,11 @@ public interface IGroupCourseBookingRepository { * @return 更新记录数 */ Mono updateToAbsent(Long bookingId); + + /** + * 统计会员取消的预约次数 + * @param memberId 会员ID + * @return 取消次数 + */ + Mono countCancelledByMemberId(Long memberId); } \ No newline at end of file diff --git a/gym-manage-api/gym-groupCourse/src/main/java/cn/novalon/gym/manage/groupcourse/repository/impl/GroupCourseBookingRepository.java b/gym-manage-api/gym-groupCourse/src/main/java/cn/novalon/gym/manage/groupcourse/repository/impl/GroupCourseBookingRepository.java index c95b0bb..73d45df 100644 --- a/gym-manage-api/gym-groupCourse/src/main/java/cn/novalon/gym/manage/groupcourse/repository/impl/GroupCourseBookingRepository.java +++ b/gym-manage-api/gym-groupCourse/src/main/java/cn/novalon/gym/manage/groupcourse/repository/impl/GroupCourseBookingRepository.java @@ -112,4 +112,9 @@ public class GroupCourseBookingRepository implements IGroupCourseBookingReposito java.time.LocalDateTime now = java.time.LocalDateTime.now(); return groupCourseBookingDao.updateToAbsent(bookingId, now); } + + @Override + public Mono countCancelledByMemberId(Long memberId) { + return groupCourseBookingDao.countCancelledByMemberId(memberId); + } } \ No newline at end of file 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 1159f71..d8cb0f7 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 @@ -13,6 +13,7 @@ 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; @@ -157,7 +158,8 @@ public class GroupCourseBookingService implements IGroupCourseBookingService { booking.setLocation(course.getLocation()); // 10. 使用Saga事务执行预约(包含权益扣减) - return bookingSagaHandler.executeBooking(booking, memberCardRecordId) + BigDecimal courseAmount = course.getStoredValueAmount() != null ? course.getStoredValueAmount() : BigDecimal.ZERO; + return bookingSagaHandler.executeBooking(booking, memberCardRecordId, courseAmount) .flatMap(savedBooking -> { // 11. 释放锁 return redisService.releaseLock(courseId, requestId) @@ -290,8 +292,15 @@ public class GroupCourseBookingService implements IGroupCourseBookingService { "需在课程开始前" + CANCEL_MIN_ADVANCE_HOURS + "小时取消"); } - // 5. 使用Saga事务执行取消预约(包含权益恢复) - return bookingSagaHandler.executeCancelBooking(bookingId, booking.getCourseId(), booking.getMemberCardRecordId(), memberId) + // 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) diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/repository/MemberStoredCardRepository.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/repository/MemberStoredCardRepository.java index 8c638c0..d1af27d 100644 --- a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/repository/MemberStoredCardRepository.java +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/repository/MemberStoredCardRepository.java @@ -34,6 +34,14 @@ public interface MemberStoredCardRepository extends R2dbcRepository rechargeBalance(Long memberId, BigDecimal addAmount); + /** + * 储值卡返还 - 增加余额(用于取消预约退款) + */ + @Modifying + @Query("UPDATE member_stored_card SET balance = balance + :addAmount, updated_at = NOW() " + + "WHERE member_id = :memberId AND deleted_at IS NULL") + Mono refundBalance(Long memberId, BigDecimal addAmount); + /** * 储值卡消费 - 扣减余额 */ diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/IMemberStoredCardService.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/IMemberStoredCardService.java index 24229a7..72779cb 100644 --- a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/IMemberStoredCardService.java +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/IMemberStoredCardService.java @@ -52,6 +52,17 @@ public interface IMemberStoredCardService { */ Mono payWithStoredCard(Long memberId, String password, BigDecimal amount); + /** + * 储值卡返还(取消预约退款,仅增加余额不修改充值总额) + */ + Mono refundToStoredCard(Long memberId, String password, BigDecimal amount, long cancelCount); + + /** + * 储值卡返还(内部调用,无需密码,含手续费逻辑) + * @return 实际返还金额 + */ + Mono refundBalanceWithFee(Long memberId, BigDecimal amount, long cancelCount); + /** * 查询会员的充值记录列表 */ diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/MemberCardRecordServiceImpl.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/MemberCardRecordServiceImpl.java index d979aa3..1a810d6 100644 --- a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/MemberCardRecordServiceImpl.java +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/MemberCardRecordServiceImpl.java @@ -213,9 +213,9 @@ public class MemberCardRecordServiceImpl implements IMemberCardRecordService { .filter(record -> record.getMemberCardType() == null || !record.getMemberCardType().equals("STORED_VALUE_CARD")) .collectList() - .map(records -> { + .flatMap(records -> { if (records.isEmpty()) { - return null; + return Mono.empty(); } // 优先找临期卡(剩余1-3天) @@ -230,7 +230,7 @@ public class MemberCardRecordServiceImpl implements IMemberCardRecordService { .orElse(null); if (expiringCard != null) { - return expiringCard; + return Mono.just(expiringCard); } // 再找普通有效卡(剩余>3天) @@ -245,11 +245,11 @@ public class MemberCardRecordServiceImpl implements IMemberCardRecordService { .orElse(null); if (activeCard != null) { - return activeCard; + return Mono.just(activeCard); } // 最后返回第一张卡 - return records.get(0); + return Mono.just(records.get(0)); }); } diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/MemberStoredCardServiceImpl.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/MemberStoredCardServiceImpl.java index d425526..d70e710 100644 --- a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/MemberStoredCardServiceImpl.java +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/MemberStoredCardServiceImpl.java @@ -5,6 +5,7 @@ import cn.novalon.gym.manage.member.entity.MemberStoredCardRecharge; import cn.novalon.gym.manage.member.repository.MemberStoredCardRechargeRepository; import cn.novalon.gym.manage.member.repository.MemberStoredCardRepository; import cn.novalon.gym.manage.member.service.IMemberStoredCardService; +import cn.novalon.gym.manage.sys.core.service.ISysConfigService; import lombok.extern.slf4j.Slf4j; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.stereotype.Service; @@ -22,12 +23,15 @@ public class MemberStoredCardServiceImpl implements IMemberStoredCardService { private final MemberStoredCardRepository memberStoredCardRepository; private final MemberStoredCardRechargeRepository memberStoredCardRechargeRepository; + private final ISysConfigService sysConfigService; private final BCryptPasswordEncoder passwordEncoder; public MemberStoredCardServiceImpl(MemberStoredCardRepository memberStoredCardRepository, - MemberStoredCardRechargeRepository memberStoredCardRechargeRepository) { + MemberStoredCardRechargeRepository memberStoredCardRechargeRepository, + ISysConfigService sysConfigService) { this.memberStoredCardRepository = memberStoredCardRepository; this.memberStoredCardRechargeRepository = memberStoredCardRechargeRepository; + this.sysConfigService = sysConfigService; this.passwordEncoder = new BCryptPasswordEncoder(); } @@ -137,12 +141,94 @@ public class MemberStoredCardServiceImpl implements IMemberStoredCardService { })); } + @Override + public Mono refundToStoredCard(Long memberId, String password, BigDecimal amount, long cancelCount) { + // 从系统配置读取免手续费次数阈值,默认3次 + Mono freeLimitMono = sysConfigService.getConfigValue("group_course.cancel_free_limit") + .map(Long::parseLong) + .defaultIfEmpty(3L); + + return freeLimitMono.flatMap(freeLimit -> + memberStoredCardRepository.findByMemberId(memberId) + .flatMap(card -> { + // 验证密码 + if (card.getPayPassword() == null || card.getPayPassword().isEmpty()) { + log.warn("[储值卡退款] 未设置支付密码: memberId={}", memberId); + return Mono.just(PayResult.fail("请先设置支付密码")); + } + + if (!passwordEncoder.matches(password, card.getPayPassword())) { + log.warn("[储值卡退款] 密码错误: memberId={}", memberId); + return Mono.just(PayResult.fail("支付密码错误")); + } + + // 前N次取消不扣手续费,超出后扣除10% + BigDecimal refundAmount = amount; + BigDecimal fee = BigDecimal.ZERO; + if (cancelCount >= freeLimit) { + fee = amount.multiply(new BigDecimal("0.1")).setScale(2, java.math.RoundingMode.HALF_UP); + refundAmount = amount.subtract(fee); + log.info("[储值卡退款] 第{}次取消,已超出免手续费阈值({}次),扣除10%手续费: 原额={}, 手续费={}, 退款={}", + cancelCount + 1, freeLimit, amount, fee, refundAmount); + } else { + log.info("[储值卡退款] 第{}次取消,在免手续费阈值({}次)内,全额退款: 原额={}", + cancelCount + 1, freeLimit, amount); + } + + // 执行退款 + BigDecimal finalRefundAmount = refundAmount; + return memberStoredCardRepository.refundBalance(memberId, refundAmount) + .flatMap(rows -> { + if (rows > 0) { + return memberStoredCardRepository.findByMemberId(memberId) + .map(updatedCard -> PayResult.success( + updatedCard.getBalance() != null ? updatedCard.getBalance() : BigDecimal.ZERO)); + } else { + log.warn("[储值卡退款] 退款失败: memberId={}, amount={}", memberId, finalRefundAmount); + return Mono.just(PayResult.fail("退款失败,请稍后重试")); + } + }); + }) + .switchIfEmpty(Mono.defer(() -> { + log.warn("[储值卡退款] 储值卡不存在: memberId={}", memberId); + return Mono.just(PayResult.fail("储值卡不存在")); + })) + ); + } + @Override public Flux getRechargeRecords(Long memberId, int page, int size) { int offset = (page - 1) * size; return memberStoredCardRechargeRepository.findByMemberId(memberId, size, offset); } + @Override + public Mono refundBalanceWithFee(Long memberId, BigDecimal amount, long cancelCount) { + return sysConfigService.getConfigValue("group_course.cancel_free_limit") + .map(Long::parseLong) + .defaultIfEmpty(3L) + .flatMap(freeLimit -> { + BigDecimal refundAmount; + if (cancelCount >= freeLimit) { + BigDecimal fee = amount.multiply(new BigDecimal("0.1")).setScale(2, java.math.RoundingMode.HALF_UP); + refundAmount = amount.subtract(fee); + log.info("[储值卡退款-内部] 第{}次取消,超出免手续费阈值({}次),扣除10%: 原额={}, 手续费={}, 退款={}", + cancelCount + 1, freeLimit, amount, fee, refundAmount); + } else { + refundAmount = amount; + log.info("[储值卡退款-内部] 第{}次取消,在免手续费阈值({}次)内,全额退款: {}", + cancelCount + 1, freeLimit, amount); + } + return memberStoredCardRepository.refundBalance(memberId, refundAmount) + .flatMap(rows -> { + if (rows > 0) { + return Mono.just(refundAmount); + } + return Mono.error(new RuntimeException("储值卡退款失败")); + }); + }); + } + @Override public Mono countRechargeRecords(Long memberId) { return memberStoredCardRechargeRepository.countByMemberIdAndDeletedAtIsNull(memberId); diff --git a/gym-manage-api/gym-payment/src/main/java/cn/novalon/gym/manage/payment/service/impl/PaymentServiceImpl.java b/gym-manage-api/gym-payment/src/main/java/cn/novalon/gym/manage/payment/service/impl/PaymentServiceImpl.java index 12eeed6..b9445b9 100644 --- a/gym-manage-api/gym-payment/src/main/java/cn/novalon/gym/manage/payment/service/impl/PaymentServiceImpl.java +++ b/gym-manage-api/gym-payment/src/main/java/cn/novalon/gym/manage/payment/service/impl/PaymentServiceImpl.java @@ -222,14 +222,15 @@ public class PaymentServiceImpl implements PaymentService { if ("00000000".equals(respCode)) { if ("S".equals(transStat)) { if (!"SUCCESS".equals(order.getPayStatus())) { - log.info("[Huifu] 查询到支付成功,仅更新订单状态(业务处理由汇付异步回调触发), orderNo={}", order.getOrderNo()); + log.info("[Huifu] 查询到支付成功,更新订单状态并触发业务通知, orderNo={}", order.getOrderNo()); order.setPayStatus("SUCCESS"); order.setHfSeqId(String.valueOf(response.getOrDefault("hf_seq_id", order.getHfSeqId()))); order.setPayTime(LocalDateTime.now()); return paymentOrderRepository.save(order) - .map(savedOrder -> { + .flatMap(savedOrder -> { log.info("[Huifu] 订单状态已更新为SUCCESS, orderNo={}", savedOrder.getOrderNo()); - return convertToResponse(savedOrder); + return paymentNotifyService.notifyPaymentStatus(savedOrder.getOrderNo(), "SUCCESS", savedOrder.getMemberId()) + .thenReturn(convertToResponse(savedOrder)); }); } else { log.info("[Huifu] 订单已处理过,跳过, orderNo={}", order.getOrderNo()); diff --git a/gym-manage-api/manage-app/src/main/java/cn/novalon/gym/manage/app/service/PaymentNotifyServiceImpl.java b/gym-manage-api/manage-app/src/main/java/cn/novalon/gym/manage/app/service/PaymentNotifyServiceImpl.java index e044c4d..b979c96 100644 --- a/gym-manage-api/manage-app/src/main/java/cn/novalon/gym/manage/app/service/PaymentNotifyServiceImpl.java +++ b/gym-manage-api/manage-app/src/main/java/cn/novalon/gym/manage/app/service/PaymentNotifyServiceImpl.java @@ -127,6 +127,18 @@ public class PaymentNotifyServiceImpl implements PaymentNotifyService { } private Mono handleStoredCardRecharge(PaymentOrder order) { + String orderNo = order.getOrderNo(); + + return memberStoredCardRechargeRepository.findByOrderNo(orderNo) + .flatMap(existing -> { + log.info("[PaymentNotify] 充值记录已存在,跳过重复处理, orderNo={}, status={}", orderNo, existing.getStatus()); + return Mono.empty(); + }) + .switchIfEmpty(Mono.defer(() -> doHandleStoredCardRecharge(order))) + .then(); + } + + private Mono doHandleStoredCardRecharge(PaymentOrder order) { String remark = order.getRemark(); log.info("[PaymentNotify] 开始处理储值卡充值, orderNo={}, memberId={}, orderType={}, remark={}", order.getOrderNo(), order.getMemberId(), order.getOrderType(), remark); 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 e785261..2eb2f99 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-16 17:13) +-- V13: 整合所有测试数据(基于 2026-06-29 17:13) -- 说明: 系统初始数据由 V2 处理,本文件仅包含业务测试数据 --- 所有时间相关字段已更新为基于 2026-06-16 +-- 所有时间相关字段已更新为基于 2026-06-29 -- ============================================================ -- ============================================================ @@ -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, 'M20260616001', '张三', '13800138001', '2026-06-16 08:00:00', '2026-06-16 08:00:00', false), -(1002, 'M20260616002', '李四', '13800138002', '2026-06-16 09:00:00', '2026-06-16 09:00:00', false), -(1003, 'M20260615003', '王五', '13800138003', '2026-06-15 10:00:00', '2026-06-15 10:00:00', false), -(1004, 'M20260614004', '赵六', '13800138004', '2026-06-14 11:00:00', '2026-06-14 11:00:00', false), -(1005, 'M20260616005', '钱七', '13800138005', '2026-06-16 08:00:00', '2026-06-16 08:00:00', false), -(1006, 'M20260616006', '孙八', '13800138006', '2026-06-16 09:00:00', '2026-06-16 09:00:00', false), -(1007, 'M20260616007', '周九', '13800138007', '2026-06-16 10:00:00', '2026-06-16 10:00:00', false), -(1008, 'M20260613008', '吴十', '13800138008', '2026-06-13 14:00:00', '2026-06-13 14:00:00', false), -(1009, 'M20260612009', '郑十一', '13800138009', '2026-06-12 15:00:00', '2026-06-12 15:00:00', false), -(1010, 'M20260611010', '王十二', '13800138010', '2026-06-11 16:00:00', '2026-06-11 16:00:00', false), -(1011, 'M20260610011', '陈十三', '13800138011', '2026-06-10 17:00:00', '2026-06-10 17:00:00', false), -(1012, 'M20260609012', '刘十四', '13800138012', '2026-06-09 18:00:00', '2026-06-09 18:00:00', false); +(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); -- 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-16 10:00:00', '2026-06-16 10:00:00'), -('MEM_TEST_B', '用户B_过期时长卡', '13800001002', 1, false, '2026-06-15 10:00:00', '2026-06-15 10:00:00'), -('MEM_TEST_C', '会员C_有效时长卡', '13800001003', 2, false, '2026-06-15 10:00:00', '2026-06-15 10:00:00'), -('MEM_TEST_D', '会员D_次数用尽', '13800001004', 1, false, '2026-06-15 10:00:00', '2026-06-15 10:00:00'), -('MEM_TEST_E', '会员E_次数充足', '13800001005', 2, false, '2026-06-15 10:00:00', '2026-06-15 10:00:00'), -('MEM_TEST_F', '会员F_储值卡余额不足', '13800001006', 1, false, '2026-06-15 10:00:00', '2026-06-15 10:00:00'), -('MEM_TEST_G', '会员G_储值卡余额充足', '13800001007', 2, false, '2026-06-15 10:00:00', '2026-06-15 10:00:00'); +('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'); -- 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-15 10:00:00', '2026-06-15 10:00:00'), -('MEM_TEST_LI', '李四_次数卡用户', '13800002002', 1, false, '2026-06-15 10:00:00', '2026-06-15 10:00:00'), -('MEM_TEST_WANG', '王五_储值卡用户', '13800002003', 2, false, '2026-06-15 10:00:00', '2026-06-15 10:00:00'); +('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'); -- 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-15 10:00:00', '2026-06-15 10:00:00'); +('MEM_TIME_CONFLICT', '时间冲突测试会员', '13800009999', 1, false, '2026-06-28 10:00:00', '2026-06-28 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-15 23:59:59', '2026-05-16 10:00:00', 0, '{}', '2026-05-16 10:00:00', '2026-06-16 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-06-28 23:59:59', '2026-05-29 10:00:00', 0, '{}', '2026-05-29 10:00:00', '2026-06-29 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-16 23:59:59', '2026-06-16 10:00:00', 0, '{}', '2026-06-16 10:00:00', '2026-06-16 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-07-29 23:59:59', '2026-06-29 10:00:00', 0, '{}', '2026-06-29 10:00:00', '2026-06-29 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-16 10:00:00', 0, '{}', '2026-05-16 10:00:00', '2026-06-16 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-05-29 10:00:00', 0, '{}', '2026-05-29 10:00:00', '2026-06-29 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-16 10:00:00', 0, '{}', '2026-05-16 10:00:00', '2026-06-16 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-05-29 10:00:00', 0, '{}', '2026-05-29 10:00:00', '2026-06-29 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-16 23:59:59', '2026-05-16 10:00:00', 0, '{}', '2026-05-16 10:00:00', '2026-06-16 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-06-29 23:59:59', '2026-05-29 10:00:00', 0, '{}', '2026-05-29 10:00:00', '2026-06-29 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-16 23:59:59', '2026-05-16 10:00:00', 0, '{}', '2026-05-16 10:00:00', '2026-06-16 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-06-29 23:59:59', '2026-05-29 10:00:00', 0, '{}', '2026-05-29 10:00:00', '2026-06-29 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-16 23:59:59', '2026-06-16 10:00:00', 0, '{}', '2026-06-16 10:00:00', '2026-06-16 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-16 10:00:00', 0, '{}', '2026-05-16 10:00:00', '2026-06-16 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-16 23:59:59', '2026-05-16 10:00:00', 0, '{}', '2026-05-16 10:00:00', '2026-06-16 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-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'); -- 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-16 10:00:00', 0, '{}', '2026-06-16 10:00:00', '2026-06-16 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-06-29 10:00:00', 0, '{}', '2026-06-29 10:00:00', '2026-06-29 10:00:00'); -- ============================================================ --- Section G: 团课课程(来源 V7 / V10 / V11 / V12 / V14,基于 2026-06-24) +-- Section G: 团课课程(来源 V7 / V10 / V11 / V12 / V14,基于 2026-06-29) -- ============================================================ -- 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-25 16:45:00', '2026-06-25 20:20:00', 25, 0, 0, '单车房', '/images/spinning.jpg', '跟随音乐节奏变换阻力和速度,体验爬坡与冲刺的快感。', 50.00, 'admin', '2026-06-24 10:00:00', '2026-06-24 10:00:00'), -('清晨流瑜伽', 101, 1, '2026-06-28 09:00:00', '2026-06-28 10:30:00', 15, 5, 0, 'A座3楼瑜伽教室', '/images/yoga_flow.jpg', '适合有一定基础的学员,通过流畅的体式连接呼吸。', 50.00, 'admin', '2026-06-24 10:00:00', '2026-06-24 10:00:00'), -('燃脂搏击', 102, 2, '2026-06-26 18:30:00', '2026-06-26 19:30:00', 20, 20, 0, '综合训练区', '/images/kickboxing.jpg', '高强度间歇训练,名额已满。', 50.00, 'coach_zhang', '2026-06-24 14:30:00', '2026-06-24 14:30:00'), -('哈他瑜伽', 101, 1, '2026-06-24 17:45:00', '2026-06-24 18:50:00', 12, 3, 0, '瑜伽教室B', '/images/hatha_yoga.jpg', '基础哈他瑜伽,距开始不足30分钟,已停止预约。', 50.00, 'coach_li', '2026-06-24 08:00:00', '2026-06-24 08:00:00'), -('周末冥想修复', 101, 1, '2026-07-06 15:00:00', '2026-07-06 16:00:00', 12, 3, 1, '冥想室', '/images/meditation.jpg', '通过呼吸和正念冥想,该课程已被取消。', 50.00, 'coach_wang', '2026-06-24 08:00:00', '2026-06-24 08:00:00'), -('蜜桃臀塑造', 103, 3, '2026-06-18 19:00:00', '2026-06-18 20:00:00', 10, 8, 2, '私教专区', '/images/glute.jpg', '针对性训练臀部肌肉群,课程已结束。', 50.00, 'coach_li', '2026-06-18 09:15:00', '2026-06-18 09:15:00'), -('午间冥想放松', 101, 1, '2026-06-20 12:00:00', '2026-06-20 13:00:00', 15, 6, 2, '冥想室', '/images/meditation_noon.jpg', '午间冥想课程,已结束。', 50.00, 'admin', '2026-06-20 09:00:00', '2026-06-20 09:00:00'); +('极速燃脂单车', 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'); -- 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-06-26 19:30:00', '2026-06-26 20:30:00', 20, 0, 0, '综合训练区', '消耗储值50元', 50.00, 'admin', '2026-06-24 10:00:00', '2026-06-24 10:00:00'), -('高端普拉提_储值卡课程', 103, 1, '2026-06-27 19:00:00', '2026-06-27 20:00:00', 15, 0, 0, '普拉提教室', '消耗储值20元', 20.00, 'admin', '2026-06-24 10:00:00', '2026-06-24 10:00:00'); +('燃脂搏击_储值测试', 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'); -- 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-06-27 19:00:00', '2026-06-27 20:00:00', 20, 0, 0, '瑜伽教室', '用于测试取消预约功能', 30.00, 'admin', '2026-06-24 10:00:00', '2026-06-24 10:00:00'); +('晚间瑜伽_取消测试', 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'); -- 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-06-26 13:00:00', '2026-06-26 15:00:00', 20, 0, 0, '综合训练区', '测试用团课A', 50.00, 'admin', '2026-06-24 10:00:00', '2026-06-24 10:00:00'), -('时间冲突测试_B_14点-16点', 103, 1, '2026-06-26 14:00:00', '2026-06-26 16:00:00', 15, 0, 0, '普拉提教室', '与团课A时间重叠', 50.00, 'admin', '2026-06-24 10:00:00', '2026-06-24 10:00:00'), -('时间冲突测试_C_10点-12点', 101, 1, '2026-06-26 10:00:00', '2026-06-26 12:00:00', 15, 0, 0, '瑜伽教室', '与A/B不冲突', 50.00, 'admin', '2026-06-24 10:00:00', '2026-06-24 10:00:00'); +('时间冲突测试_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'); --- G5: V14 数据统计场景(今天 2026-06-24) +-- G5: V14 数据统计场景(今天 2026-06-29) 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-24 08:00:00', '2026-06-24 09:00:00', 20, 15, 0, '健身房A区', 'https://example.com/yoga.jpg', '适合初学者的瑜伽课程', 50.00, '2026-06-24 10:00:00', '2026-06-24 10:00:00'), -(3002, '动感单车', 2, 2, '2026-06-24 09:30:00', '2026-06-24 10:30:00', 25, 20, 0, '健身房B区', 'https://example.com/spinning.jpg', '高强度有氧运动', 50.00, '2026-06-24 11:00:00', '2026-06-24 11:00:00'), -(3003, '普拉提', 3, 1, '2026-06-24 14:00:00', '2026-06-24 15:00:00', 15, 10, 0, '健身房C区', 'https://example.com/pilates.jpg', '核心力量训练', 50.00, '2026-06-24 12:00:00', '2026-06-24 12:00:00'); +(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'); -- ============================================================ -- Section H: 团课预约记录 @@ -191,26 +191,26 @@ 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-24 11:00:00', '0', '晚间瑜伽_取消测试', '2026-06-27 19:00:00', '2026-06-27 20:00:00', '瑜伽教室', '2026-06-24 11:00:00', '2026-06-24 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-24 11:30:00', '0', '晚间瑜伽_取消测试', '2026-06-27 19:00:00', '2026-06-27 20:00:00', '瑜伽教室', '2026-06-24 11:30:00', '2026-06-24 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-24 12:00:00', '0', '晚间瑜伽_取消测试', '2026-06-27 19:00:00', '2026-06-27 20:00:00', '瑜伽教室', '2026-06-24 12:00:00', '2026-06-24 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-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'); UPDATE group_course SET current_members = current_members + 3 WHERE course_name = '晚间瑜伽_取消测试'; -- H2: V14 数据统计预约 - 今天(6月24日) 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-24 08:00:00', '2', '2026-06-23 20:00:00', '2026-06-24 08:30:00'), -(4002, 1002, 2, 3001, '2026-06-24 08:00:00', '2', '2026-06-23 21:00:00', '2026-06-24 08:30:00'), -(4003, 1003, 3, 3001, '2026-06-24 08:00:00', '3', '2026-06-23 22:00:00', '2026-06-24 09:00:00'), -(4004, 1004, 4, 3002, '2026-06-24 09:30:00', '2', '2026-06-23 19:00:00', '2026-06-24 09:30:00'), -(4005, 1005, 5, 3002, '2026-06-24 09:30:00', '1', '2026-06-23 20:30:00', '2026-06-24 09:00:00'), -(4006, 1006, 6, 3002, '2026-06-24 09:30:00', '2', '2026-06-23 21:30:00', '2026-06-24 09:30:00'), -(4007, 1007, 7, 3003, '2026-06-24 14:00:00', '2', '2026-06-23 22:30:00', '2026-06-24 14:00:00'), -(4008, 1008, 8, 3003, '2026-06-24 14:00:00', '3', '2026-06-24 08:00:00', '2026-06-24 14:00:00'), -(4009, 1009, 9, 3003, '2026-06-24 14:00:00', '2', '2026-06-24 09:00:00', '2026-06-24 14:00:00'), -(4010, 1010, 10, 3001, '2026-06-24 08:00:00', '2', '2026-06-24 07:00:00', '2026-06-24 08:00:00'), -(4011, 1011, 11, 3002, '2026-06-24 09:30:00', '1', '2026-06-24 08:30:00', '2026-06-24 09:00:00'), -(4012, 1012, 12, 3003, '2026-06-24 14:00:00', '2', '2026-06-24 10:00:00', '2026-06-24 14:00:00'); +(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'); -- H3: V14 数据统计预约 - 昨天(6月23日) INSERT INTO group_course_booking (id, member_id, member_card_id, course_id, booking_time, status, created_at, updated_at) VALUES @@ -229,22 +229,22 @@ INSERT INTO group_course_booking (id, member_id, member_card_id, course_id, book (4022, 1004, 4, 3003, '2026-06-22 14:00:00', '3', '2026-06-21 19:00:00', '2026-06-22 14:00:00'); -- ============================================================ --- Section I: 签到记录(来源 V14,基于 2026-06-24) +-- Section I: 签到记录(来源 V14,基于 2026-06-29) -- ============================================================ 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-24 08:00:00', 'QR_CODE', 'SUCCESS', 'MINI_PROGRAM', false), -(2002, 1002, '2026-06-24 08:15:00', 'MANUAL', 'SUCCESS', 'PC_BACKEND', false), -(2003, 1003, '2026-06-24 08:30:00', 'FACE', 'SUCCESS', 'MINI_PROGRAM', false), -(2004, 1004, '2026-06-24 09:00:00', 'QR_CODE', 'SUCCESS', 'MINI_PROGRAM', false), -(2005, 1005, '2026-06-24 09:15:00', 'MANUAL', 'SUCCESS', 'PC_BACKEND', false), -(2006, 1006, '2026-06-24 09:30:00', 'QR_CODE', 'SUCCESS', 'MINI_PROGRAM', false), -(2007, 1007, '2026-06-24 10:00:00', 'FACE', 'SUCCESS', 'MINI_PROGRAM', false), -(2008, 1008, '2026-06-24 10:15:00', 'QR_CODE', 'SUCCESS', 'MINI_PROGRAM', false), -(2009, 1009, '2026-06-24 10:30:00', 'MANUAL', 'SUCCESS', 'PC_BACKEND', false), -(2010, 1010, '2026-06-24 11:00:00', 'QR_CODE', 'SUCCESS', 'MINI_PROGRAM', false), -(2011, 1011, '2026-06-24 11:15:00', 'FACE', 'FAIL', 'MINI_PROGRAM', false), -(2012, 1012, '2026-06-24 11:30:00', 'MANUAL', 'SUCCESS', 'PC_BACKEND', false); +(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); 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), @@ -264,43 +264,43 @@ INSERT INTO sign_in_record (id, member_id, sign_in_time, sign_in_type, sign_in_s (2025, 1005, '2026-06-22 09:00:00', 'MANUAL', 'SUCCESS', 'PC_BACKEND', false); -- ============================================================ --- Section J: 团课推荐测试数据(来源 V18,基于 2026-06-24) +-- Section J: 团课推荐测试数据(来源 V18,基于 2026-06-29) -- ============================================================ 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-24 10:00:00', '2026-06-24 10:00:00'); +((SELECT id FROM group_course WHERE course_name = '极速燃脂单车'), '本周热门推荐', '极速燃脂单车课程,跟随音乐节奏变换阻力和速度,体验爬坡与冲刺的快感,一节课消耗800大卡!', '教练专业,课程内容丰富,深受学员喜爱,燃脂效果显著', 20, true, 'admin', '2026-06-29 10:00:00', '2026-06-29 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-24 11:00:00', '2026-06-24 11:00:00'); +((SELECT id FROM group_course WHERE course_name = '清晨流瑜伽'), '新手友好推荐', '清晨流瑜伽课程,适合有一定基础的学员,通过流畅的体式连接呼吸,唤醒身体能量。', '适合新手入门,教练耐心指导,课程节奏适中', 15, true, 'admin', '2026-06-29 11:00:00', '2026-06-29 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-24 12:00:00', '2026-06-24 12:00:00'); +((SELECT id FROM group_course WHERE course_name = '燃脂搏击'), '高强度燃脂', '燃脂搏击课程,高强度间歇训练,配合音乐快速燃脂,释放压力。', '高强度训练,适合进阶学员,快速燃脂塑形', 10, false, 'admin', '2026-06-29 12:00:00', '2026-06-29 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-24 13:00:00', '2026-06-24 13:00:00'); +((SELECT id FROM group_course WHERE course_name = '哈他瑜伽'), '基础瑜伽推荐', '基础哈他瑜伽课程,适合所有级别学员,通过基础体式练习提升身体柔韧性和平衡能力。', '零基础友好,适合所有健身水平,放松身心', 12, true, 'coach_li', '2026-06-29 13:00:00', '2026-06-29 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-24 09:15:00', '2026-06-24 09:15:00'); +((SELECT id FROM group_course WHERE course_name = '蜜桃臀塑造'), '塑形热门课程', '蜜桃臀塑造课程,针对性训练臀部肌肉群,打造完美曲线。', '专业私教指导,动作标准,效果显著,深受女性学员喜爱', 18, true, 'coach_li', '2026-06-29 09:15:00', '2026-06-29 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-24 09:00:00', '2026-06-24 09:00:00'); +((SELECT id FROM group_course WHERE course_name = '午间冥想放松'), '午间放松推荐', '午间冥想放松课程,通过呼吸和正念冥想,深度放松身心,缓解工作压力。', '适合上班族,午间放松充电,提升下午工作效率', 8, true, 'admin', '2026-06-29 09:00:00', '2026-06-29 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-24 14:00:00', '2026-06-24 14:00:00'); +((SELECT id FROM group_course WHERE course_name = '极速燃脂单车'), '减脂首选课程', '想要快速减脂?极速燃脂单车是你的最佳选择!专业教练带领,科学训练计划。', '减脂效果最佳,课程强度适中,适合想要快速瘦身的学员', 16, true, 'coach_zhang', '2026-06-29 14:00:00', '2026-06-29 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-24 15:00:00', '2026-06-24 15:00:00'); +((SELECT id FROM group_course WHERE course_name = '清晨流瑜伽'), '晨练优选', '清晨流瑜伽,唤醒身体能量,开启活力一天!适合晨练爱好者。', '晨练最佳选择,提升身体活力,改善精神状态', 14, true, 'coach_wang', '2026-06-29 15:00:00', '2026-06-29 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-24 16:00:00', '2026-06-24 16:00:00'); +((SELECT id FROM group_course WHERE course_name = '哈他瑜伽'), '身心平衡推荐', '哈他瑜伽课程,通过体式练习和呼吸控制,达到身心平衡,提升整体健康水平。', '改善身体柔韧性,增强核心力量,提升身体协调性', 11, true, 'coach_li', '2026-06-29 16:00:00', '2026-06-29 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-24 10:00:00', '2026-06-24 10:00:00'); +((SELECT id FROM group_course WHERE course_name = '午间冥想放松'), '职场减压课程', '午间冥想放松,专为职场人士设计,快速缓解工作压力,提升工作状态。', '职场减压首选,课程时间短,效果显著', 9, false, 'admin', '2026-06-29 10:00:00', '2026-06-29 10:00:00'); -- ============================================================ -- Section K: 统计说明 -- ============================================================ --- 今日(2026-06-24)统计预期: +-- 今日(2026-06-29)统计预期: -- 新增会员: 3 (1005, 1006, 1007) -- 签到会员: 12 -- 预约总数: 12 (4001-4012),取消: 2,出席: 8,缺席: 2 diff --git a/gym-manage-api/manage-db/src/main/resources/db/migration/V2__Insert_initial_data.sql b/gym-manage-api/manage-db/src/main/resources/db/migration/V2__Insert_initial_data.sql index 447f2c4..97151be 100644 --- a/gym-manage-api/manage-db/src/main/resources/db/migration/V2__Insert_initial_data.sql +++ b/gym-manage-api/manage-db/src/main/resources/db/migration/V2__Insert_initial_data.sql @@ -218,7 +218,8 @@ VALUES ('主框架页-默认皮肤样式名称', 'sys.index.skinName', 'skin-blue', 'Y', 'system', 'system', NOW(), NOW()), ('用户自助-验证码开关', 'sys.account.captchaEnabled', 'true', 'Y', 'system', 'system', NOW(), NOW()), ('用户自助-是否开启用户注册功能', 'sys.account.registerUser', 'false', 'Y', 'system', 'system', NOW(), NOW()), -('账号自助-密码验证码', 'sys.account.pwdCaptchaEnabled', 'true', 'Y', 'system', 'system', NOW(), NOW()) +('账号自助-密码验证码', 'sys.account.pwdCaptchaEnabled', 'true', 'Y', 'system', 'system', NOW(), NOW()), +('团课-取消预约免手续费次数', 'group_course.cancel_free_limit', '3', 'Y', 'system', 'system', NOW(), NOW()) ON CONFLICT (config_key) DO NOTHING; -- ============================================