修复多处问题 #46

Merged
liwentao merged 1 commits from feature/fix-Refill into dev 2026-06-29 03:02:24 +08:00
14 changed files with 386 additions and 204 deletions
Showing only changes of commit efd4d03037 - Show all commits
@@ -61,6 +61,12 @@ public interface GroupCourseBookingDao extends R2dbcRepository<GroupCourseBookin
*/ */
Mono<Long> countByCourseIdAndStatusAndDeletedAtIsNull(Long courseId, String status); Mono<Long> 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<Long> countCancelledByMemberId(Long memberId);
/** /**
* 查询会员是否有时间冲突的预约(状态为已预约且未取消) * 查询会员是否有时间冲突的预约(状态为已预约且未取消)
* 时间冲突条件:新课程的开始时间 < 已预约课程的结束时间 且 新课程的结束时间 > 已预约课程的开始时间 * 时间冲突条件:新课程的开始时间 < 已预约课程的结束时间 且 新课程的结束时间 > 已预约课程的开始时间
@@ -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.enums.MemberCardType;
import cn.novalon.gym.manage.member.repository.MemberCardRepository; import cn.novalon.gym.manage.member.repository.MemberCardRepository;
import cn.novalon.gym.manage.member.service.IMemberCardRecordService; import cn.novalon.gym.manage.member.service.IMemberCardRecordService;
import cn.novalon.gym.manage.member.service.IMemberStoredCardService;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
import java.math.BigDecimal;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
@@ -34,6 +36,7 @@ public class BookingSagaHandler {
private final IGroupCourseBookingRepository bookingRepository; private final IGroupCourseBookingRepository bookingRepository;
private final IGroupCourseRepository courseRepository; private final IGroupCourseRepository courseRepository;
private final IMemberCardRecordService memberCardRecordService; private final IMemberCardRecordService memberCardRecordService;
private final IMemberStoredCardService memberStoredCardService;
private final MemberCardRepository memberCardRepository; private final MemberCardRepository memberCardRepository;
private final GroupCourseRedisService redisService; private final GroupCourseRedisService redisService;
@@ -44,10 +47,10 @@ public class BookingSagaHandler {
* *
* 步骤: * 步骤:
* 1. 保存预约记录 * 1. 保存预约记录
* 2. 扣减会员卡权益 * 2. 扣减储值卡余额(储值卡类型)
* 3. 更新课程当前人数 * 3. 更新课程当前人数
*/ */
public Mono<GroupCourseBooking> executeBooking(GroupCourseBooking booking, Long recordId) { public Mono<GroupCourseBooking> executeBooking(GroupCourseBooking booking, Long recordId, BigDecimal storedValueAmount) {
List<SagaStep> steps = new ArrayList<>(); List<SagaStep> steps = new ArrayList<>();
List<SagaStep> rollbackSteps = new ArrayList<>(); List<SagaStep> rollbackSteps = new ArrayList<>();
@@ -60,11 +63,11 @@ public class BookingSagaHandler {
steps.add(step1); steps.add(step1);
rollbackSteps.add(0, step1); rollbackSteps.add(0, step1);
// 步骤2:扣减会员卡权益(根据卡类型决定扣除次数还是金额 // 步骤2:扣减权益(根据卡类型)
SagaStep step2 = new SagaStep( SagaStep step2 = new SagaStep(
"扣减会员卡权益", "扣减权益",
deductCardUsageByCardType(booking.getMemberId(), recordId), deductCardUsageByCardType(booking.getMemberId(), recordId, storedValueAmount),
Mono.defer(() -> restoreCardUsageByCardType(booking.getMemberId(), recordId)) Mono.defer(() -> restoreCardUsageByCardType(booking.getMemberId(), recordId, storedValueAmount))
); );
steps.add(step2); steps.add(step2);
rollbackSteps.add(0, step2); rollbackSteps.add(0, step2);
@@ -94,7 +97,7 @@ public class BookingSagaHandler {
/** /**
* 根据会员卡类型扣减权益 * 根据会员卡类型扣减权益
*/ */
private Mono<Void> deductCardUsageByCardType(Long memberId, Long recordId) { private Mono<Void> deductCardUsageByCardType(Long memberId, Long recordId, BigDecimal storedValueAmount) {
return memberCardRecordService.findById(recordId) return memberCardRecordService.findById(recordId)
.switchIfEmpty(Mono.error(new RuntimeException("会员卡记录不存在"))) .switchIfEmpty(Mono.error(new RuntimeException("会员卡记录不存在")))
.flatMap(record -> { .flatMap(record -> {
@@ -109,14 +112,13 @@ public class BookingSagaHandler {
switch (cardType) { switch (cardType) {
case COUNT_CARD: case COUNT_CARD:
// 次数卡不再支持团课预约 return Mono.error(new RuntimeException("团课预约仅支持储值卡和时长卡支付"));
return Mono.error(new RuntimeException("团课预约仅支持储值卡支付"));
case STORED_VALUE_CARD: case STORED_VALUE_CARD:
// 储值卡扣除金额 return deductStoredCardBalance(memberId, storedValueAmount);
return deductCardUsage(recordId, 0, DEFAULT_GROUP_COURSE_PRICE);
case TIME_CARD: case TIME_CARD:
// 时长卡不扣除,但需验证有效期 // 时长卡验证有效期后,仍须从储值卡扣款
return validateTimeCard(record); return validateTimeCard(record)
.then(deductStoredCardBalance(memberId, storedValueAmount));
default: default:
return Mono.error(new RuntimeException("不支持的会员卡类型: " + cardType)); return Mono.error(new RuntimeException("不支持的会员卡类型: " + cardType));
} }
@@ -153,11 +155,10 @@ public class BookingSagaHandler {
/** /**
* 根据会员卡类型恢复权益 * 根据会员卡类型恢复权益
*/ */
private Mono<Void> restoreCardUsageByCardType(Long memberId, Long recordId) { private Mono<Void> restoreCardUsageByCardType(Long memberId, Long recordId, BigDecimal storedValueAmount) {
return memberCardRecordService.findById(recordId) return memberCardRecordService.findById(recordId)
.switchIfEmpty(Mono.error(new RuntimeException("会员卡记录不存在,无法恢复权益"))) .switchIfEmpty(Mono.error(new RuntimeException("会员卡记录不存在,无法恢复权益")))
.flatMap(record -> { .flatMap(record -> {
// 验证会员卡归属(memberId为null时跳过验证)
if (memberId != null && !record.getMemberId().equals(memberId)) { if (memberId != null && !record.getMemberId().equals(memberId)) {
return Mono.error(new RuntimeException("会员卡不归属当前用户")); return Mono.error(new RuntimeException("会员卡不归属当前用户"));
} }
@@ -168,12 +169,29 @@ public class BookingSagaHandler {
switch (cardType) { switch (cardType) {
case COUNT_CARD: case COUNT_CARD:
// 次数卡不再支持团课预约,无需恢复
return Mono.empty(); return Mono.empty();
case STORED_VALUE_CARD: 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: 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: default:
return Mono.error(new RuntimeException("不支持的会员卡类型: " + cardType)); return Mono.error(new RuntimeException("不支持的会员卡类型: " + cardType));
} }
@@ -184,7 +202,7 @@ public class BookingSagaHandler {
/** /**
* 执行取消预约事务 * 执行取消预约事务
*/ */
public Mono<GroupCourseBooking> executeCancelBooking(Long bookingId, Long courseId, Long recordId, Long memberId) { public Mono<GroupCourseBooking> executeCancelBooking(Long bookingId, Long courseId, Long recordId, Long memberId, BigDecimal storedValueAmount, long cancelCount) {
List<SagaStep> steps = new ArrayList<>(); List<SagaStep> steps = new ArrayList<>();
List<SagaStep> rollbackSteps = new ArrayList<>(); List<SagaStep> rollbackSteps = new ArrayList<>();
@@ -197,11 +215,12 @@ public class BookingSagaHandler {
steps.add(step1); steps.add(step1);
rollbackSteps.add(0, step1); rollbackSteps.add(0, step1);
// 步骤2恢复会员卡权益 // 步骤2返还储值卡余额(含手续费逻辑)
SagaStep step2 = new SagaStep( SagaStep step2 = new SagaStep(
"恢复会员卡权益", "返还储值卡余额",
restoreCardUsageByCardType(memberId, recordId), refundStoredCardWithFee(memberId, storedValueAmount, cancelCount),
Mono.defer(() -> deductCardUsageByCardType(memberId, recordId)) // 回滚时重新扣减
Mono.defer(() -> deductCardUsageByCardType(memberId, recordId, storedValueAmount))
); );
steps.add(step2); steps.add(step2);
rollbackSteps.add(0, step2); rollbackSteps.add(0, step2);
@@ -253,54 +272,32 @@ public class BookingSagaHandler {
return bookingRepository.deleteById(bookingId); return bookingRepository.deleteById(bookingId);
} }
private Mono<Void> deductCardUsage(Long recordId, Integer deductTimes, Double deductAmount) { /**
if (deductTimes == 0 && deductAmount == 0.0) { * 从储值卡扣减余额
return Mono.empty(); */
} private Mono<Void> deductStoredCardBalance(Long memberId, BigDecimal storedValueAmount) {
BigDecimal amount = storedValueAmount != null && storedValueAmount.compareTo(BigDecimal.ZERO) > 0
return memberCardRecordService.findById(recordId) ? storedValueAmount : BigDecimal.valueOf(DEFAULT_GROUP_COURSE_PRICE);
.switchIfEmpty(Mono.error(new RuntimeException("会员卡记录不存在"))) return memberStoredCardService.consume(memberId, amount)
.flatMap(record -> { .flatMap(rows -> {
cn.novalon.gym.manage.member.enums.MemberCardRecordStatus status = record.getStatus(); if (rows == 0) {
if (status != cn.novalon.gym.manage.member.enums.MemberCardRecordStatus.ACTIVE) { return Mono.error(new RuntimeException("储值卡扣减失败,余额不足"));
return Mono.error(new RuntimeException("会员卡状态无效,当前状态: " + (status != null ? status.getDesc() : "未知")));
} }
java.time.LocalDateTime expireTime = record.getExpireTime(); log.info("储值卡扣减成功: memberId={}, amount={}", memberId, amount);
if (expireTime != null && expireTime.isBefore(java.time.LocalDateTime.now())) { return Mono.empty();
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();
});
}); });
} }
private Mono<Void> restoreCardUsage(Long recordId, Integer addTimes, Double addAmount) { /**
if (addTimes == 0 && addAmount == 0.0) { * 返还储值卡余额(含手续费逻辑,仅用于取消预约)
return Mono.empty(); */
} private Mono<Void> refundStoredCardWithFee(Long memberId, BigDecimal storedValueAmount, long cancelCount) {
BigDecimal amount = storedValueAmount != null && storedValueAmount.compareTo(BigDecimal.ZERO) > 0
return memberCardRecordService.findById(recordId) ? storedValueAmount : BigDecimal.valueOf(DEFAULT_GROUP_COURSE_PRICE);
.switchIfEmpty(Mono.error(new RuntimeException("会员卡记录不存在,无法恢复权益"))) return memberStoredCardService.refundBalanceWithFee(memberId, amount, cancelCount)
.flatMap(record -> { .flatMap(refundedAmount -> {
// 使用当前记录的过期时间,避免清空过期时间 log.info("储值卡退款成功(含手续费): memberId={}, cancelCount={}, 退款金额={}", memberId, cancelCount, refundedAmount);
return memberCardRecordService.renewCard(recordId, addTimes, addAmount, record.getExpireTime()) return Mono.empty();
.flatMap(rows -> {
if (rows == 0) {
return Mono.error(new RuntimeException("恢复会员卡权益失败,请重试"));
}
return Mono.empty();
});
}); });
} }
@@ -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.domain.GroupCourseBooking;
import cn.novalon.gym.manage.groupcourse.service.IGroupCourseBookingService; 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.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
@@ -23,9 +24,12 @@ import java.util.Map;
public class GroupCourseBookingHandler { public class GroupCourseBookingHandler {
private final IGroupCourseBookingService bookingService; private final IGroupCourseBookingService bookingService;
private final IMemberStoredCardService memberStoredCardService;
public GroupCourseBookingHandler(IGroupCourseBookingService bookingService) { public GroupCourseBookingHandler(IGroupCourseBookingService bookingService,
IMemberStoredCardService memberStoredCardService) {
this.bookingService = bookingService; this.bookingService = bookingService;
this.memberStoredCardService = memberStoredCardService;
} }
/** /**
@@ -45,24 +49,35 @@ public class GroupCourseBookingHandler {
if (body.get("memberCardRecordId") == null) { if (body.get("memberCardRecordId") == null) {
return buildErrorResponse("请提供会员卡记录ID"); return buildErrorResponse("请提供会员卡记录ID");
} }
if (body.get("payPassword") == null || body.get("payPassword").toString().isEmpty()) {
return buildErrorResponse("请输入支付密码");
}
Long courseId = ((Number) body.get("courseId")).longValue(); Long courseId = toLong(body.get("courseId"), "courseId");
Long memberId = ((Number) body.get("memberId")).longValue(); Long memberId = toLong(body.get("memberId"), "memberId");
Long memberCardRecordId = ((Number) body.get("memberCardRecordId")).longValue(); Long memberCardRecordId = toLong(body.get("memberCardRecordId"), "memberCardRecordId");
String payPassword = body.get("payPassword").toString();
return bookingService.bookCourse(courseId, memberId, memberCardRecordId) // 验证支付密码
.flatMap(booking -> { return memberStoredCardService.verifyPayPassword(memberId, payPassword)
Map<String, Object> response = new HashMap<>(); .flatMap(passwordValid -> {
response.put("success", true); if (!passwordValid) {
response.put("message", "预约成功"); return buildErrorResponse("支付密码错误");
response.put("data", booking); }
return ServerResponse.ok().bodyValue(response); return bookingService.bookCourse(courseId, memberId, memberCardRecordId)
}) .flatMap(booking -> {
.onErrorResume(error -> { Map<String, Object> response = new HashMap<>();
Map<String, Object> response = new HashMap<>(); response.put("success", true);
response.put("success", false); response.put("message", "预约成功");
response.put("message", error.getMessage()); response.put("data", booking);
return ServerResponse.badRequest().bodyValue(response); return ServerResponse.ok().bodyValue(response);
})
.onErrorResume(error -> {
Map<String, Object> 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) return request.bodyToMono(Map.class)
.flatMap(body -> { .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 -> { return memberStoredCardService.verifyPayPassword(memberId, payPassword)
Map<String, Object> response = new HashMap<>(); .flatMap(passwordValid -> {
response.put("success", true); if (!passwordValid) {
response.put("message", "取消成功"); return buildErrorResponse("支付密码错误");
response.put("data", booking); }
return ServerResponse.ok().bodyValue(response); return bookingService.cancelBooking(bookingId, memberId)
}) .flatMap(booking -> {
.onErrorResume(error -> { Map<String, Object> response = new HashMap<>();
Map<String, Object> response = new HashMap<>(); response.put("success", true);
response.put("success", false); response.put("message", "取消成功");
response.put("message", error.getMessage()); response.put("data", booking);
return ServerResponse.badRequest().bodyValue(response); return ServerResponse.ok().bodyValue(response);
})
.onErrorResume(error -> {
Map<String, Object> 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); .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 + " 类型不正确");
}
/** /**
* 构建错误响应 * 构建错误响应
*/ */
@@ -89,4 +89,11 @@ public interface IGroupCourseBookingRepository {
* @return 更新记录数 * @return 更新记录数
*/ */
Mono<Integer> updateToAbsent(Long bookingId); Mono<Integer> updateToAbsent(Long bookingId);
/**
* 统计会员取消的预约次数
* @param memberId 会员ID
* @return 取消次数
*/
Mono<Long> countCancelledByMemberId(Long memberId);
} }
@@ -112,4 +112,9 @@ public class GroupCourseBookingRepository implements IGroupCourseBookingReposito
java.time.LocalDateTime now = java.time.LocalDateTime.now(); java.time.LocalDateTime now = java.time.LocalDateTime.now();
return groupCourseBookingDao.updateToAbsent(bookingId, now); return groupCourseBookingDao.updateToAbsent(bookingId, now);
} }
@Override
public Mono<Long> countCancelledByMemberId(Long memberId) {
return groupCourseBookingDao.countCancelledByMemberId(memberId);
}
} }
@@ -13,6 +13,7 @@ import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux; import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
import java.math.BigDecimal;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit; import java.time.temporal.ChronoUnit;
import java.util.UUID; import java.util.UUID;
@@ -157,7 +158,8 @@ public class GroupCourseBookingService implements IGroupCourseBookingService {
booking.setLocation(course.getLocation()); booking.setLocation(course.getLocation());
// 10. 使用Saga事务执行预约(包含权益扣减) // 10. 使用Saga事务执行预约(包含权益扣减)
return bookingSagaHandler.executeBooking(booking, memberCardRecordId) BigDecimal courseAmount = course.getStoredValueAmount() != null ? course.getStoredValueAmount() : BigDecimal.ZERO;
return bookingSagaHandler.executeBooking(booking, memberCardRecordId, courseAmount)
.flatMap(savedBooking -> { .flatMap(savedBooking -> {
// 11. 释放锁 // 11. 释放锁
return redisService.releaseLock(courseId, requestId) return redisService.releaseLock(courseId, requestId)
@@ -290,8 +292,15 @@ public class GroupCourseBookingService implements IGroupCourseBookingService {
"需在课程开始前" + CANCEL_MIN_ADVANCE_HOURS + "小时取消"); "需在课程开始前" + CANCEL_MIN_ADVANCE_HOURS + "小时取消");
} }
// 5. 使用Saga事务执行取消预约(包含权益恢复) // 5. 查询课程金额和已取消次数,使用Saga事务执行取消预约
return bookingSagaHandler.executeCancelBooking(bookingId, booking.getCourseId(), booking.getMemberCardRecordId(), memberId) 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 -> { .flatMap(updatedBooking -> {
// 6. 释放锁 // 6. 释放锁
return redisService.releaseLock(bookingId, requestId) return redisService.releaseLock(bookingId, requestId)
@@ -34,6 +34,14 @@ public interface MemberStoredCardRepository extends R2dbcRepository<MemberStored
"WHERE member_id = :memberId AND deleted_at IS NULL") "WHERE member_id = :memberId AND deleted_at IS NULL")
Mono<Integer> rechargeBalance(Long memberId, BigDecimal addAmount); Mono<Integer> 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<Integer> refundBalance(Long memberId, BigDecimal addAmount);
/** /**
* 储值卡消费 - 扣减余额 * 储值卡消费 - 扣减余额
*/ */
@@ -52,6 +52,17 @@ public interface IMemberStoredCardService {
*/ */
Mono<PayResult> payWithStoredCard(Long memberId, String password, BigDecimal amount); Mono<PayResult> payWithStoredCard(Long memberId, String password, BigDecimal amount);
/**
* 储值卡返还(取消预约退款,仅增加余额不修改充值总额)
*/
Mono<PayResult> refundToStoredCard(Long memberId, String password, BigDecimal amount, long cancelCount);
/**
* 储值卡返还(内部调用,无需密码,含手续费逻辑)
* @return 实际返还金额
*/
Mono<BigDecimal> refundBalanceWithFee(Long memberId, BigDecimal amount, long cancelCount);
/** /**
* 查询会员的充值记录列表 * 查询会员的充值记录列表
*/ */
@@ -213,9 +213,9 @@ public class MemberCardRecordServiceImpl implements IMemberCardRecordService {
.filter(record -> record.getMemberCardType() == null || .filter(record -> record.getMemberCardType() == null ||
!record.getMemberCardType().equals("STORED_VALUE_CARD")) !record.getMemberCardType().equals("STORED_VALUE_CARD"))
.collectList() .collectList()
.map(records -> { .flatMap(records -> {
if (records.isEmpty()) { if (records.isEmpty()) {
return null; return Mono.empty();
} }
// 优先找临期卡(剩余1-3天) // 优先找临期卡(剩余1-3天)
@@ -230,7 +230,7 @@ public class MemberCardRecordServiceImpl implements IMemberCardRecordService {
.orElse(null); .orElse(null);
if (expiringCard != null) { if (expiringCard != null) {
return expiringCard; return Mono.just(expiringCard);
} }
// 再找普通有效卡(剩余>3天) // 再找普通有效卡(剩余>3天)
@@ -245,11 +245,11 @@ public class MemberCardRecordServiceImpl implements IMemberCardRecordService {
.orElse(null); .orElse(null);
if (activeCard != null) { if (activeCard != null) {
return activeCard; return Mono.just(activeCard);
} }
// 最后返回第一张卡 // 最后返回第一张卡
return records.get(0); return Mono.just(records.get(0));
}); });
} }
@@ -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.MemberStoredCardRechargeRepository;
import cn.novalon.gym.manage.member.repository.MemberStoredCardRepository; import cn.novalon.gym.manage.member.repository.MemberStoredCardRepository;
import cn.novalon.gym.manage.member.service.IMemberStoredCardService; import cn.novalon.gym.manage.member.service.IMemberStoredCardService;
import cn.novalon.gym.manage.sys.core.service.ISysConfigService;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@@ -22,12 +23,15 @@ public class MemberStoredCardServiceImpl implements IMemberStoredCardService {
private final MemberStoredCardRepository memberStoredCardRepository; private final MemberStoredCardRepository memberStoredCardRepository;
private final MemberStoredCardRechargeRepository memberStoredCardRechargeRepository; private final MemberStoredCardRechargeRepository memberStoredCardRechargeRepository;
private final ISysConfigService sysConfigService;
private final BCryptPasswordEncoder passwordEncoder; private final BCryptPasswordEncoder passwordEncoder;
public MemberStoredCardServiceImpl(MemberStoredCardRepository memberStoredCardRepository, public MemberStoredCardServiceImpl(MemberStoredCardRepository memberStoredCardRepository,
MemberStoredCardRechargeRepository memberStoredCardRechargeRepository) { MemberStoredCardRechargeRepository memberStoredCardRechargeRepository,
ISysConfigService sysConfigService) {
this.memberStoredCardRepository = memberStoredCardRepository; this.memberStoredCardRepository = memberStoredCardRepository;
this.memberStoredCardRechargeRepository = memberStoredCardRechargeRepository; this.memberStoredCardRechargeRepository = memberStoredCardRechargeRepository;
this.sysConfigService = sysConfigService;
this.passwordEncoder = new BCryptPasswordEncoder(); this.passwordEncoder = new BCryptPasswordEncoder();
} }
@@ -137,12 +141,94 @@ public class MemberStoredCardServiceImpl implements IMemberStoredCardService {
})); }));
} }
@Override
public Mono<PayResult> refundToStoredCard(Long memberId, String password, BigDecimal amount, long cancelCount) {
// 从系统配置读取免手续费次数阈值,默认3次
Mono<Long> 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 @Override
public Flux<MemberStoredCardRecharge> getRechargeRecords(Long memberId, int page, int size) { public Flux<MemberStoredCardRecharge> getRechargeRecords(Long memberId, int page, int size) {
int offset = (page - 1) * size; int offset = (page - 1) * size;
return memberStoredCardRechargeRepository.findByMemberId(memberId, size, offset); return memberStoredCardRechargeRepository.findByMemberId(memberId, size, offset);
} }
@Override
public Mono<BigDecimal> 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 @Override
public Mono<Long> countRechargeRecords(Long memberId) { public Mono<Long> countRechargeRecords(Long memberId) {
return memberStoredCardRechargeRepository.countByMemberIdAndDeletedAtIsNull(memberId); return memberStoredCardRechargeRepository.countByMemberIdAndDeletedAtIsNull(memberId);
@@ -222,14 +222,15 @@ public class PaymentServiceImpl implements PaymentService {
if ("00000000".equals(respCode)) { if ("00000000".equals(respCode)) {
if ("S".equals(transStat)) { if ("S".equals(transStat)) {
if (!"SUCCESS".equals(order.getPayStatus())) { if (!"SUCCESS".equals(order.getPayStatus())) {
log.info("[Huifu] 查询到支付成功,更新订单状态(业务处理由汇付异步回调触发), orderNo={}", order.getOrderNo()); log.info("[Huifu] 查询到支付成功,更新订单状态并触发业务通知, orderNo={}", order.getOrderNo());
order.setPayStatus("SUCCESS"); order.setPayStatus("SUCCESS");
order.setHfSeqId(String.valueOf(response.getOrDefault("hf_seq_id", order.getHfSeqId()))); order.setHfSeqId(String.valueOf(response.getOrDefault("hf_seq_id", order.getHfSeqId())));
order.setPayTime(LocalDateTime.now()); order.setPayTime(LocalDateTime.now());
return paymentOrderRepository.save(order) return paymentOrderRepository.save(order)
.map(savedOrder -> { .flatMap(savedOrder -> {
log.info("[Huifu] 订单状态已更新为SUCCESS, orderNo={}", savedOrder.getOrderNo()); log.info("[Huifu] 订单状态已更新为SUCCESS, orderNo={}", savedOrder.getOrderNo());
return convertToResponse(savedOrder); return paymentNotifyService.notifyPaymentStatus(savedOrder.getOrderNo(), "SUCCESS", savedOrder.getMemberId())
.thenReturn(convertToResponse(savedOrder));
}); });
} else { } else {
log.info("[Huifu] 订单已处理过,跳过, orderNo={}", order.getOrderNo()); log.info("[Huifu] 订单已处理过,跳过, orderNo={}", order.getOrderNo());
@@ -127,6 +127,18 @@ public class PaymentNotifyServiceImpl implements PaymentNotifyService {
} }
private Mono<Void> handleStoredCardRecharge(PaymentOrder order) { private Mono<Void> handleStoredCardRecharge(PaymentOrder order) {
String orderNo = order.getOrderNo();
return memberStoredCardRechargeRepository.findByOrderNo(orderNo)
.flatMap(existing -> {
log.info("[PaymentNotify] 充值记录已存在,跳过重复处理, orderNo={}, status={}", orderNo, existing.getStatus());
return Mono.<Void>empty();
})
.switchIfEmpty(Mono.defer(() -> doHandleStoredCardRecharge(order)))
.then();
}
private Mono<Void> doHandleStoredCardRecharge(PaymentOrder order) {
String remark = order.getRemark(); String remark = order.getRemark();
log.info("[PaymentNotify] 开始处理储值卡充值, orderNo={}, memberId={}, orderType={}, remark={}", log.info("[PaymentNotify] 开始处理储值卡充值, orderNo={}, memberId={}, orderType={}, remark={}",
order.getOrderNo(), order.getMemberId(), order.getOrderType(), remark); order.getOrderNo(), order.getMemberId(), order.getOrderType(), remark);
@@ -1,7 +1,7 @@
-- ============================================================ -- ============================================================
-- V13: 整合所有测试数据(基于 2026-06-16 17:13 -- V13: 整合所有测试数据(基于 2026-06-29 17:13
-- 说明: 系统初始数据由 V2 处理,本文件仅包含业务测试数据 -- 说明: 系统初始数据由 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 -- E1: 数据统计测试会员(V14,显式 ID 1001-1012
INSERT INTO member_user (id, member_no, nickname, phone, created_at, updated_at, is_deleted) VALUES 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), (1001, 'M20260629001', '张三', '13800138001', '2026-06-29 08:00:00', '2026-06-29 08:00:00', false),
(1002, 'M20260616002', '李四', '13800138002', '2026-06-16 09:00:00', '2026-06-16 09:00:00', false), (1002, 'M20260629002', '李四', '13800138002', '2026-06-29 09:00:00', '2026-06-29 09:00:00', false),
(1003, 'M20260615003', '王五', '13800138003', '2026-06-15 10:00:00', '2026-06-15 10:00:00', false), (1003, 'M20260628003', '王五', '13800138003', '2026-06-28 10:00:00', '2026-06-28 10:00:00', false),
(1004, 'M20260614004', '赵六', '13800138004', '2026-06-14 11:00:00', '2026-06-14 11:00:00', false), (1004, 'M20260627004', '赵六', '13800138004', '2026-06-27 11:00:00', '2026-06-27 11:00:00', false),
(1005, 'M20260616005', '钱七', '13800138005', '2026-06-16 08:00:00', '2026-06-16 08:00:00', false), (1005, 'M20260629005', '钱七', '13800138005', '2026-06-29 08:00:00', '2026-06-29 08:00:00', false),
(1006, 'M20260616006', '孙八', '13800138006', '2026-06-16 09:00:00', '2026-06-16 09:00:00', false), (1006, 'M20260629006', '孙八', '13800138006', '2026-06-29 09:00:00', '2026-06-29 09:00:00', false),
(1007, 'M20260616007', '周九', '13800138007', '2026-06-16 10:00:00', '2026-06-16 10:00:00', false), (1007, 'M20260629007', '周九', '13800138007', '2026-06-29 10:00:00', '2026-06-29 10:00:00', false),
(1008, 'M20260613008', '吴十', '13800138008', '2026-06-13 14:00:00', '2026-06-13 14:00:00', false), (1008, 'M20260626008', '吴十', '13800138008', '2026-06-26 14:00:00', '2026-06-26 14:00:00', false),
(1009, 'M20260612009', '郑十一', '13800138009', '2026-06-12 15:00:00', '2026-06-12 15:00:00', false), (1009, 'M20260625009', '郑十一', '13800138009', '2026-06-25 15:00:00', '2026-06-25 15:00:00', false),
(1010, 'M20260611010', '王十二', '13800138010', '2026-06-11 16:00:00', '2026-06-11 16:00:00', false), (1010, 'M20260624010', '王十二', '13800138010', '2026-06-24 16:00:00', '2026-06-24 16:00:00', false),
(1011, 'M20260610011', '陈十三', '13800138011', '2026-06-10 17:00:00', '2026-06-10 17:00:00', false), (1011, 'M20260623011', '陈十三', '13800138011', '2026-06-23 17:00:00', '2026-06-23 17:00:00', false),
(1012, 'M20260609012', '刘十四', '13800138012', '2026-06-09 18:00:00', '2026-06-09 18:00:00', false); (1012, 'M20260622012', '刘十四', '13800138012', '2026-06-22 18:00:00', '2026-06-22 18:00:00', false);
-- E2: 预约场景测试会员(V10: A-G) -- E2: 预约场景测试会员(V10: A-G)
INSERT INTO member_user (member_no, nickname, phone, gender, is_deleted, created_at, updated_at) VALUES 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_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-15 10:00:00', '2026-06-15 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-15 10:00:00', '2026-06-15 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-15 10:00:00', '2026-06-15 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-15 10:00:00', '2026-06-15 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-15 10:00:00', '2026-06-15 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-15 10:00:00', '2026-06-15 10:00:00'); ('MEM_TEST_G', '会员G_储值卡余额充足', '13800001007', 2, false, '2026-06-28 10:00:00', '2026-06-28 10:00:00');
-- E3: 取消预约测试会员(V11: 张三/李四/王五) -- E3: 取消预约测试会员(V11: 张三/李四/王五)
INSERT INTO member_user (member_no, nickname, phone, gender, is_deleted, created_at, updated_at) VALUES 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_ZHANG', '张三_时长卡用户', '13800002001', 1, false, '2026-06-28 10:00:00', '2026-06-28 10:00:00'),
('MEM_TEST_LI', '李四_次数卡用户', '13800002002', 1, false, '2026-06-15 10:00:00', '2026-06-15 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-15 10:00:00', '2026-06-15 10:00:00'); ('MEM_TEST_WANG', '王五_储值卡用户', '13800002003', 2, false, '2026-06-28 10:00:00', '2026-06-28 10:00:00');
-- E4: 时间冲突测试会员(V12) -- E4: 时间冲突测试会员(V12)
INSERT INTO member_user (member_no, nickname, phone, gender, is_deleted, created_at, updated_at) VALUES 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: 会员卡记录 -- Section F: 会员卡记录
@@ -123,67 +123,67 @@ INSERT INTO member_user (member_no, nickname, phone, gender, is_deleted, created
-- F1: V10 预约场景 - 6条会员卡记录 -- 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 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 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 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 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 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 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条 -- 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 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_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-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_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-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_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条 -- 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 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种场景团课 -- 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 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'), ('极速燃脂单车', 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-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'), ('清晨流瑜伽', 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-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'), ('燃脂搏击', 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-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-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-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'), ('周末冥想修复', 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-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'), ('蜜桃臀塑造', 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-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'); ('午间冥想放松', 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 预约场景(已取消次数卡支付,仅保留储值卡) -- 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 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'), ('燃脂搏击_储值测试', 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-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'); ('高端普拉提_储值卡课程', 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 取消预约场景 -- 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 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 时间冲突场景 -- 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 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'), ('时间冲突测试_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-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'), ('时间冲突测试_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-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'); ('时间冲突测试_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 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'), (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-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'), (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-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'); (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: 团课预约记录 -- Section H: 团课预约记录
@@ -191,26 +191,26 @@ INSERT INTO group_course (id, course_name, coach_id, course_type, start_time, en
-- H1: V11 取消预约测试 -- 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 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_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-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_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-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_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 = '晚间瑜伽_取消测试'; UPDATE group_course SET current_members = current_members + 3 WHERE course_name = '晚间瑜伽_取消测试';
-- H2: V14 数据统计预约 - 今天(6月24日) -- 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 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'), (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-24 08:00:00', '2', '2026-06-23 21:00:00', '2026-06-24 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-24 08:00:00', '3', '2026-06-23 22:00:00', '2026-06-24 09:00: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-24 09:30:00', '2', '2026-06-23 19:00:00', '2026-06-24 09:30: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-24 09:30:00', '1', '2026-06-23 20:30:00', '2026-06-24 09:00: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-24 09:30:00', '2', '2026-06-23 21:30:00', '2026-06-24 09:30: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-24 14:00:00', '2', '2026-06-23 22:30:00', '2026-06-24 14:00: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-24 14:00:00', '3', '2026-06-24 08:00:00', '2026-06-24 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-24 14:00:00', '2', '2026-06-24 09:00:00', '2026-06-24 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-24 08:00:00', '2', '2026-06-24 07:00:00', '2026-06-24 08: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-24 09:30:00', '1', '2026-06-24 08:30:00', '2026-06-24 09: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-24 14:00:00', '2', '2026-06-24 10:00:00', '2026-06-24 14: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日) -- 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 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'); (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 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), (2001, 1001, '2026-06-29 08:00:00', 'QR_CODE', 'SUCCESS', 'MINI_PROGRAM', false),
(2002, 1002, '2026-06-24 08:15:00', 'MANUAL', 'SUCCESS', 'PC_BACKEND', false), (2002, 1002, '2026-06-29 08:15:00', 'MANUAL', 'SUCCESS', 'PC_BACKEND', false),
(2003, 1003, '2026-06-24 08:30:00', 'FACE', 'SUCCESS', 'MINI_PROGRAM', false), (2003, 1003, '2026-06-29 08:30:00', 'FACE', 'SUCCESS', 'MINI_PROGRAM', false),
(2004, 1004, '2026-06-24 09:00:00', 'QR_CODE', 'SUCCESS', 'MINI_PROGRAM', false), (2004, 1004, '2026-06-29 09:00:00', 'QR_CODE', 'SUCCESS', 'MINI_PROGRAM', false),
(2005, 1005, '2026-06-24 09:15:00', 'MANUAL', 'SUCCESS', 'PC_BACKEND', false), (2005, 1005, '2026-06-29 09:15:00', 'MANUAL', 'SUCCESS', 'PC_BACKEND', false),
(2006, 1006, '2026-06-24 09:30:00', 'QR_CODE', 'SUCCESS', 'MINI_PROGRAM', false), (2006, 1006, '2026-06-29 09:30:00', 'QR_CODE', 'SUCCESS', 'MINI_PROGRAM', false),
(2007, 1007, '2026-06-24 10:00:00', 'FACE', 'SUCCESS', 'MINI_PROGRAM', false), (2007, 1007, '2026-06-29 10:00:00', 'FACE', 'SUCCESS', 'MINI_PROGRAM', false),
(2008, 1008, '2026-06-24 10:15:00', 'QR_CODE', 'SUCCESS', 'MINI_PROGRAM', false), (2008, 1008, '2026-06-29 10:15:00', 'QR_CODE', 'SUCCESS', 'MINI_PROGRAM', false),
(2009, 1009, '2026-06-24 10:30:00', 'MANUAL', 'SUCCESS', 'PC_BACKEND', false), (2009, 1009, '2026-06-29 10:30:00', 'MANUAL', 'SUCCESS', 'PC_BACKEND', false),
(2010, 1010, '2026-06-24 11:00:00', 'QR_CODE', 'SUCCESS', 'MINI_PROGRAM', false), (2010, 1010, '2026-06-29 11:00:00', 'QR_CODE', 'SUCCESS', 'MINI_PROGRAM', false),
(2011, 1011, '2026-06-24 11:15:00', 'FACE', 'FAIL', 'MINI_PROGRAM', false), (2011, 1011, '2026-06-29 11:15:00', 'FACE', 'FAIL', 'MINI_PROGRAM', false),
(2012, 1012, '2026-06-24 11:30:00', 'MANUAL', 'SUCCESS', 'PC_BACKEND', 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 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), (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); (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 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 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 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 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 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 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 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 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 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 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: 统计说明 -- Section K: 统计说明
-- ============================================================ -- ============================================================
-- 今日(2026-06-24)统计预期: -- 今日(2026-06-29)统计预期:
-- 新增会员: 3 (1005, 1006, 1007) -- 新增会员: 3 (1005, 1006, 1007)
-- 签到会员: 12 -- 签到会员: 12
-- 预约总数: 12 (4001-4012),取消: 2,出席: 8,缺席: 2 -- 预约总数: 12 (4001-4012),取消: 2,出席: 8,缺席: 2
@@ -218,7 +218,8 @@ VALUES
('主框架页-默认皮肤样式名称', 'sys.index.skinName', 'skin-blue', 'Y', 'system', 'system', NOW(), NOW()), ('主框架页-默认皮肤样式名称', 'sys.index.skinName', 'skin-blue', 'Y', 'system', 'system', NOW(), NOW()),
('用户自助-验证码开关', 'sys.account.captchaEnabled', 'true', 'Y', 'system', 'system', NOW(), NOW()), ('用户自助-验证码开关', 'sys.account.captchaEnabled', 'true', 'Y', 'system', 'system', NOW(), NOW()),
('用户自助-是否开启用户注册功能', 'sys.account.registerUser', 'false', '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; ON CONFLICT (config_key) DO NOTHING;
-- ============================================ -- ============================================