From cd44caee57dbca0d5444fc17985a50066a8f7780 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E6=97=B6=E8=88=9F=E5=B9=B4?= <3147056268@qq.com>
Date: Sun, 24 May 2026 00:57:22 +0800
Subject: [PATCH] =?UTF-8?q?=E4=BC=9A=E5=91=98=E5=8D=A1=E7=AE=A1=E7=90=86?=
=?UTF-8?q?=E5=8A=9F=E8=83=BD=E5=88=9D=E6=AD=A5=E5=AE=8C=E6=88=90?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
gym-manage-api/gym-member-card/pom.xml | 28 ++
.../GymMemberCardApplication.java | 4 +-
.../dao/MemberCardRecordDao.java | 69 +----
.../dao/MemberCardTransactionsDao.java | 18 +-
.../dao/RefundApplicationDao.java | 64 +++++
.../gymmembercard/domain/MemberCard.java | 72 +++++
.../domain/MemberCardRecord.java | 107 +++++--
.../domain/MemberCardTransactions.java | 100 +++++++
.../domain/RefundApplication.java | 127 +++++++++
.../entity/MemberCardEntity.java | 73 ++++-
.../entity/MemberCardRecordEntity.java | 73 ++++-
.../entity/MemberCardTransactionsEntity.java | 136 +++++++--
.../entity/RefundApplicationEntity.java | 127 +++++++++
.../gymmembercard/enums/MemberCardEvent.java | 34 +++
.../gymmembercard/enums/MemberCardType.java | 25 ++
.../handler/DistributedLockService.java | 50 ++++
.../handler/ExpirationReminderService.java | 151 ++++++++++
.../handler/MemberCardHandler.java | 163 ++++++-----
.../handler/MemberCardRecordHandler.java | 195 +++++--------
.../handler/MemberCardScheduledHandler.java | 99 +++++++
.../handler/MemberCardStateMachine.java | 85 ++++++
.../handler/RefundSagaHandler.java | 126 +++++++++
.../IMemberCardRecordRepository.java | 93 ++----
.../IMemberCardTransactionsRepository.java | 21 ++
.../IRefundApplicationRepository.java | 81 ++++++
.../impl/MemberCardRecordRepositoryImpl.java | 132 ++++-----
.../MemberCardTransactionsRepositoryImpl.java | 23 ++
.../impl/RefundApplicationRepositoryImpl.java | 125 ++++++++
.../sevice/IMemberCardRecordService.java | 88 ++----
.../sevice/IMemberCardService.java | 82 ++----
.../IMemberCardTransactionsService.java | 31 +-
.../sevice/IRefundApplicationService.java | 33 +++
.../impl/MemberCardRecordServiceImpl.java | 5 +
.../sevice/impl/MemberCardServiceImpl.java | 266 +++++++++++++++++-
.../MemberCardTransactionsServiceImpl.java | 20 ++
.../impl/RefundApplicationServiceImpl.java | 85 ++++++
.../src/main/resources/application.properties | 1 -
.../gym-member-card/src/main/resources/sql | 132 +++++++++
.../gym/manage/app/config/SystemRouter.java | 103 +++----
39 files changed, 2570 insertions(+), 677 deletions(-)
create mode 100644 gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/dao/RefundApplicationDao.java
create mode 100644 gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/domain/RefundApplication.java
create mode 100644 gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/entity/RefundApplicationEntity.java
create mode 100644 gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/enums/MemberCardEvent.java
create mode 100644 gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/enums/MemberCardType.java
create mode 100644 gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/handler/DistributedLockService.java
create mode 100644 gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/handler/ExpirationReminderService.java
create mode 100644 gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/handler/MemberCardScheduledHandler.java
create mode 100644 gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/handler/MemberCardStateMachine.java
create mode 100644 gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/handler/RefundSagaHandler.java
create mode 100644 gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/repository/IRefundApplicationRepository.java
create mode 100644 gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/repository/impl/RefundApplicationRepositoryImpl.java
create mode 100644 gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/sevice/IRefundApplicationService.java
create mode 100644 gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/sevice/impl/RefundApplicationServiceImpl.java
delete mode 100644 gym-manage-api/gym-member-card/src/main/resources/application.properties
create mode 100644 gym-manage-api/gym-member-card/src/main/resources/sql
diff --git a/gym-manage-api/gym-member-card/pom.xml b/gym-manage-api/gym-member-card/pom.xml
index e5940b4..1801b55 100644
--- a/gym-manage-api/gym-member-card/pom.xml
+++ b/gym-manage-api/gym-member-card/pom.xml
@@ -61,12 +61,40 @@
spring-context
+
+
+ org.springframework.boot
+ spring-boot-starter-data-redis-reactive
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-json
+
+
+
org.springframework.boot
spring-boot-starter-test
test
+
+
diff --git a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/GymMemberCardApplication.java b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/GymMemberCardApplication.java
index c3e170d..00c02f6 100644
--- a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/GymMemberCardApplication.java
+++ b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/GymMemberCardApplication.java
@@ -3,9 +3,11 @@ package cn.novalon.gym.manage.gymmembercard;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.r2dbc.repository.config.EnableR2dbcRepositories;
+import org.springframework.scheduling.annotation.EnableScheduling;
@SpringBootApplication
-@EnableR2dbcRepositories(basePackages = "cn.novalon.gym.manage.db.dao")
+@EnableR2dbcRepositories(basePackages = "cn.novalon.gym.manage.gymmembercard.dao")
+@EnableScheduling
public class GymMemberCardApplication {
public static void main(String[] args) {
diff --git a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/dao/MemberCardRecordDao.java b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/dao/MemberCardRecordDao.java
index e9c6bb1..728d82c 100644
--- a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/dao/MemberCardRecordDao.java
+++ b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/dao/MemberCardRecordDao.java
@@ -16,17 +16,6 @@ import java.time.LocalDateTime;
@Repository
public interface MemberCardRecordDao extends R2dbcRepository {
- /**
- * 会员购卡/后台发卡
- * 支付成功后插入一条记录,状态为ACTIVE,设置有效期和初始剩余次数/余额
- * @param memberId 会员ID
- * @param memberCardId 会员卡类型ID
- * @param expireTime 到期时间
- * @param remainingTimes 剩余次数
- * @param remainingAmount 剩余余额
- * @param sourceOrderId 来源订单ID
- * @return 插入的记录
- */
@Modifying
@Query("INSERT INTO member_card_record (member_id, member_card_id, status, expire_time, remaining_times, remaining_amount, source_order_id, purchase_time, created_at, updated_at) " +
"VALUES (:memberId, :memberCardId, 'ACTIVE', :expireTime, :remainingTimes, :remainingAmount, :sourceOrderId, NOW(), NOW(), NOW()) " +
@@ -38,14 +27,6 @@ public interface MemberCardRecordDao extends R2dbcRepository updateStatus(@Param("recordId") Long recordId,
@Param("status") MemberCardRecordStatus status);
- /**
- * 会员端"我的卡包"
- * 根据会员ID查询所有卡,过滤状态为ACTIVE的,展示剩余次数/天数/余额
- * @param memberId 会员ID
- * @return 有效会员卡列表
- */
@Query("SELECT * FROM member_card_record WHERE member_id = :memberId AND status = 'ACTIVE' AND deleted_at IS NULL ORDER BY expire_time ASC")
Flux findActiveCardsByMemberId(@Param("memberId") Long memberId);
- /**
- * 前台/店长查会员卡
- * 输入会员手机号或姓名,查出该会员持有的所有卡的信息
- * @param memberId 会员ID
- * @param pageable 分页参数
- * @return 会员卡列表
- */
@Query("SELECT mcr.* FROM member_card_record mcr " +
"INNER JOIN member m ON mcr.member_id = m.member_id " +
"WHERE mcr.member_id = :memberId AND mcr.deleted_at IS NULL " +
"ORDER BY mcr.purchase_time DESC LIMIT :#{#pageable.pageSize} OFFSET :#{#pageable.offset}")
Flux findByMemberId(@Param("memberId") Long memberId, Pageable pageable);
- /**
- * 验证次卡是否可用(仅检验次数和过期时间)
- * @param recordId 会员卡记录ID
- * @param requiredTimes 需要的次数
- * @return 符合条件的记录,空表示不可用
- */
@Query("SELECT * FROM member_card_record WHERE member_card_record_id = :recordId " +
"AND status = 'ACTIVE' AND deleted_at IS NULL " +
"AND expire_time > NOW() " +
@@ -125,12 +71,6 @@ public interface MemberCardRecordDao extends R2dbcRepository validateCountCard(@Param("recordId") Long recordId,
@Param("requiredTimes") Integer requiredTimes);
- /**
- * 验证储值卡是否可用(仅检验余额和过期时间)
- * @param recordId 会员卡记录ID
- * @param requiredAmount 需要的金额
- * @return 符合条件的记录,空表示不可用
- */
@Query("SELECT * FROM member_card_record WHERE member_card_record_id = :recordId " +
"AND status = 'ACTIVE' AND deleted_at IS NULL " +
"AND expire_time > NOW() " +
@@ -138,11 +78,10 @@ public interface MemberCardRecordDao extends R2dbcRepository validateStoredCard(@Param("recordId") Long recordId,
@Param("requiredAmount") Double requiredAmount);
- /**
- * 到期扫描(分批处理,避免内存压力)
- * 定时任务:查询 status=ACTIVE 且 expire_time < 当前时间 的记录,用于批量过期处理
- * @return 已过期的会员卡记录列表(最多500条)
- */
@Query("SELECT * FROM member_card_record WHERE status = 'ACTIVE' AND expire_time < NOW() AND deleted_at IS NULL LIMIT 500")
Flux findExpiredCards();
+
+ @Query("SELECT * FROM member_card_record WHERE status = 'ACTIVE' AND deleted_at IS NULL")
+ Flux findActiveRecords();
+
}
\ No newline at end of file
diff --git a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/dao/MemberCardTransactionsDao.java b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/dao/MemberCardTransactionsDao.java
index 87836bb..bb3473e 100644
--- a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/dao/MemberCardTransactionsDao.java
+++ b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/dao/MemberCardTransactionsDao.java
@@ -153,7 +153,21 @@ public interface MemberCardTransactionsDao extends R2dbcRepository sumPurchaseAmountByMemberId(@Param("memberId") Long memberId,
- @Param("startTime") LocalDateTime startTime,
+ Mono sumPurchaseAmountByMemberId(@Param("memberId") Long memberId, @Param("startTime") LocalDateTime startTime,
@Param("endTime") LocalDateTime endTime);
+ /**
+ * 按会员ID查询所有流水记录
+ * @param memberId 会员ID
+ * @return 该会员的所有流水记录,按时间倒序
+ */
+ @Query("SELECT * FROM member_card_transactions WHERE member_id = :memberId ORDER BY created_at DESC")
+ Flux findByMemberId(Long memberId);
+ /**
+ * 按会员卡记录ID查询所有流水记录
+ * @param recordId 会员卡记录ID
+ * @return 该会员卡的所有流水记录,按时间倒序
+ */
+ @Query("SELECT * FROM member_card_transactions WHERE member_card_record_id = :recordId ORDER BY created_at DESC")
+ Flux findByRecordId(Long recordId);
+
}
\ No newline at end of file
diff --git a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/dao/RefundApplicationDao.java b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/dao/RefundApplicationDao.java
new file mode 100644
index 0000000..3dca6d4
--- /dev/null
+++ b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/dao/RefundApplicationDao.java
@@ -0,0 +1,64 @@
+package cn.novalon.gym.manage.gymmembercard.dao;
+
+import cn.novalon.gym.manage.gymmembercard.entity.RefundApplicationEntity;
+import org.springframework.data.r2dbc.repository.Query;
+import org.springframework.data.r2dbc.repository.R2dbcRepository;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+/**
+ * 退款申请数据访问对象
+ *
+ * @author shizhounian
+ * @date 2026-05-23
+ */
+public interface RefundApplicationDao extends R2dbcRepository {
+
+ /**
+ * 根据会员卡记录ID查询退款申请
+ *
+ * @param recordId 会员卡记录ID
+ * @return 退款申请实体
+ */
+ @Query("SELECT * FROM refund_application WHERE record_id = :recordId AND deleted_at IS NULL LIMIT 1")
+ Mono findByRecordId(Long recordId);
+
+ /**
+ * 根据会员ID查询退款申请列表
+ *
+ * @param memberId 会员ID
+ * @return 退款申请列表
+ */
+ @Query("SELECT * FROM refund_application WHERE member_id = :memberId AND deleted_at IS NULL ORDER BY created_at DESC")
+ Flux findByMemberId(Long memberId);
+
+ /**
+ * 根据状态查询退款申请列表
+ *
+ * @param status 状态
+ * @return 退款申请列表
+ */
+ @Query("SELECT * FROM refund_application WHERE status = :status AND deleted_at IS NULL ORDER BY created_at DESC")
+ Flux findByStatus(String status);
+
+ /**
+ * 审核退款申请(更新状态、审核人、审核时间、备注)
+ *
+ * @param id 退款申请ID
+ * @param status 审核状态
+ * @param auditorId 审核人ID
+ * @param auditRemark 审核备注
+ * @return 受影响的行数
+ */
+ @Query("UPDATE refund_application SET status = :status, auditor_id = :auditorId, audit_time = NOW(), audit_remark = :auditRemark, updated_at = NOW() WHERE id = :id")
+ Mono approve(Long id, String status, Long auditorId, String auditRemark);
+
+ /**
+ * 逻辑删除退款申请
+ *
+ * @param id 退款申请ID
+ * @return 受影响的行数
+ */
+ @Query("UPDATE refund_application SET deleted_at = NOW() WHERE id = :id")
+ Mono logicalDelete(Long id);
+}
diff --git a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/domain/MemberCard.java b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/domain/MemberCard.java
index c5cf0e6..6a79ba5 100644
--- a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/domain/MemberCard.java
+++ b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/domain/MemberCard.java
@@ -46,4 +46,76 @@ public class MemberCard extends BaseDomain {
//会员卡创建时间
@Schema(description = "会员卡创建时间",example = "2026-05-10 05:22:47")
private LocalDateTime memberCardCreateTime;
+
+ public Long getMemberCardId() {
+ return memberCardId;
+ }
+
+ public void setMemberCardId(Long memberCardId) {
+ this.memberCardId = memberCardId;
+ }
+
+ public String getMemberCardName() {
+ return memberCardName;
+ }
+
+ public void setMemberCardName(String memberCardName) {
+ this.memberCardName = memberCardName;
+ }
+
+ public String getMemberCardType() {
+ return memberCardType;
+ }
+
+ public void setMemberCardType(String memberCardType) {
+ this.memberCardType = memberCardType;
+ }
+
+ public Double getMemberCardPrice() {
+ return memberCardPrice;
+ }
+
+ public void setMemberCardPrice(Double memberCardPrice) {
+ this.memberCardPrice = memberCardPrice;
+ }
+
+ public Integer getMemberCardValidityDays() {
+ return memberCardValidityDays;
+ }
+
+ public void setMemberCardValidityDays(Integer memberCardValidityDays) {
+ this.memberCardValidityDays = memberCardValidityDays;
+ }
+
+ public Integer getMemberCardTotalTimes() {
+ return memberCardTotalTimes;
+ }
+
+ public void setMemberCardTotalTimes(Integer memberCardTotalTimes) {
+ this.memberCardTotalTimes = memberCardTotalTimes;
+ }
+
+ public Double getMemberCardAmount() {
+ return memberCardAmount;
+ }
+
+ public void setMemberCardAmount(Double memberCardAmount) {
+ this.memberCardAmount = memberCardAmount;
+ }
+
+ public Integer getMemberCardStatus() {
+ return memberCardStatus;
+ }
+
+ public void setMemberCardStatus(Integer memberCardStatus) {
+ this.memberCardStatus = memberCardStatus;
+ }
+
+ public LocalDateTime getMemberCardCreateTime() {
+ return memberCardCreateTime;
+ }
+
+ public void setMemberCardCreateTime(LocalDateTime memberCardCreateTime) {
+ this.memberCardCreateTime = memberCardCreateTime;
+ }
}
diff --git a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/domain/MemberCardRecord.java b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/domain/MemberCardRecord.java
index 7e04215..a837546 100644
--- a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/domain/MemberCardRecord.java
+++ b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/domain/MemberCardRecord.java
@@ -6,45 +6,104 @@ import io.swagger.v3.oas.annotations.media.Schema;
import java.time.LocalDateTime;
-/*
- *@Author:shizhounian
- *@Date:2026/5/10-05 22:47:31
- */
-@Schema(description = "会员卡记录",example = "member_card_record")
+@Schema(description = "会员卡记录")
public class MemberCardRecord extends BaseDomain {
- //会员持有卡id
- @Schema(description = "会员持有卡Id",example = "1")
+ @Schema(description = "会员持有卡ID", example = "1")
private Long memberCardRecordId;
- //会员Id
- @Schema(description = "会员Id",example = "1")
+ @Schema(description = "会员ID", example = "1001")
private Long memberId;
- //关联会员卡Id
- @Schema(description = "关联会员卡Id",example = "1")
+ @Schema(description = "关联会员卡ID", example = "1")
private Long memberCardId;
- //状态:ACTIVE(有效) / USED_UP(用完) / EXPIRED(过期) / REFUNDED(已退款)
- @Schema(description = "状态",example = "ACTIVE")
+ @Schema(description = "状态:ACTIVE-有效, USED_UP-用完, EXPIRED-过期, REFUNDED-已退款", example = "ACTIVE")
private MemberCardRecordStatus status;
- //剩余次数
- @Schema(description = "剩余次数",example = "1")
+ @Schema(description = "剩余次数", example = "10")
private Integer remainingTimes;
- //剩余余额
- @Schema(description = "剩余余额",example = "1")
+ @Schema(description = "剩余余额", example = "500.0")
private Double remainingAmount;
- //到期时间
- @Schema(description = "到期时间",example = "2026-05-10 05:22:47")
+ @Schema(description = "到期时间", example = "2026-06-23 10:00:00")
private LocalDateTime expireTime;
- //购买订单Id
- @Schema(description = "购买订单Id",example = "1")
+ @Schema(description = "购买订单ID", example = "10001")
private Long sourceOrderId;
- //购买时间
- @Schema(description = "购买时间",example = "2026-05-10 05:22:47")
+ @Schema(description = "购买时间", example = "2026-05-23 10:00:00")
private LocalDateTime purchaseTime;
-}
+
+ public Long getMemberCardRecordId() {
+ return memberCardRecordId;
+ }
+
+ public void setMemberCardRecordId(Long memberCardRecordId) {
+ this.memberCardRecordId = memberCardRecordId;
+ }
+
+ public Long getMemberId() {
+ return memberId;
+ }
+
+ public void setMemberId(Long memberId) {
+ this.memberId = memberId;
+ }
+
+ public Long getMemberCardId() {
+ return memberCardId;
+ }
+
+ public void setMemberCardId(Long memberCardId) {
+ this.memberCardId = memberCardId;
+ }
+
+ public MemberCardRecordStatus getStatus() {
+ return status;
+ }
+
+ public void setStatus(MemberCardRecordStatus status) {
+ this.status = status;
+ }
+
+ public Integer getRemainingTimes() {
+ return remainingTimes;
+ }
+
+ public void setRemainingTimes(Integer remainingTimes) {
+ this.remainingTimes = remainingTimes;
+ }
+
+ public Double getRemainingAmount() {
+ return remainingAmount;
+ }
+
+ public void setRemainingAmount(Double remainingAmount) {
+ this.remainingAmount = remainingAmount;
+ }
+
+ public LocalDateTime getExpireTime() {
+ return expireTime;
+ }
+
+ public void setExpireTime(LocalDateTime expireTime) {
+ this.expireTime = expireTime;
+ }
+
+ public Long getSourceOrderId() {
+ return sourceOrderId;
+ }
+
+ public void setSourceOrderId(Long sourceOrderId) {
+ this.sourceOrderId = sourceOrderId;
+ }
+
+ public LocalDateTime getPurchaseTime() {
+ return purchaseTime;
+ }
+
+ public void setPurchaseTime(LocalDateTime purchaseTime) {
+ this.purchaseTime = purchaseTime;
+ }
+}
\ No newline at end of file
diff --git a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/domain/MemberCardTransactions.java b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/domain/MemberCardTransactions.java
index 410ddac..aa7928f 100644
--- a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/domain/MemberCardTransactions.java
+++ b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/domain/MemberCardTransactions.java
@@ -53,7 +53,107 @@ public class MemberCardTransactions extends BaseDomain {
@Schema(description = "备注",example = "预约团课:瑜伽课扣1次")
private String remark;
+ //关联订单ID
+ @Schema(description = "关联订单ID",example = "1")
+ private Long sourceOrderId;
+
//创建时间
@Schema(description = "创建时间",example = "2026-05-10 05:22:47")
private LocalDateTime createdAt;
+
+ public Long getMemberCardTransactionsId() {
+ return memberCardTransactionsId;
+ }
+
+ public void setMemberCardTransactionsId(Long memberCardTransactionsId) {
+ this.memberCardTransactionsId = memberCardTransactionsId;
+ }
+
+ public Long getMemberCardId() {
+ return memberCardId;
+ }
+
+ public void setMemberCardId(Long memberCardId) {
+ this.memberCardId = memberCardId;
+ }
+
+ public Long getMemberId() {
+ return memberId;
+ }
+
+ public void setMemberId(Long memberId) {
+ this.memberId = memberId;
+ }
+
+ public MemberCardTransactionsAction getOperationType() {
+ return operationType;
+ }
+
+ public void setOperationType(MemberCardTransactionsAction operationType) {
+ this.operationType = operationType;
+ }
+
+ public Integer getChangeAmount() {
+ return changeAmount;
+ }
+
+ public void setChangeAmount(Integer changeAmount) {
+ this.changeAmount = changeAmount;
+ }
+
+ public Double getChangeBalance() {
+ return changeBalance;
+ }
+
+ public void setChangeBalance(Double changeBalance) {
+ this.changeBalance = changeBalance;
+ }
+
+ public Integer getAfterRemainingCount() {
+ return afterRemainingCount;
+ }
+
+ public void setAfterRemainingCount(Integer afterRemainingCount) {
+ this.afterRemainingCount = afterRemainingCount;
+ }
+
+ public Double getAfterRemainingBalance() {
+ return afterRemainingBalance;
+ }
+
+ public void setAfterRemainingBalance(Double afterRemainingBalance) {
+ this.afterRemainingBalance = afterRemainingBalance;
+ }
+
+ public MemberCardTransactionsType getRelatedBizType() {
+ return relatedBizType;
+ }
+
+ public void setRelatedBizType(MemberCardTransactionsType relatedBizType) {
+ this.relatedBizType = relatedBizType;
+ }
+
+ public String getRemark() {
+ return remark;
+ }
+
+ public void setRemark(String remark) {
+ this.remark = remark;
+ }
+
+ public Long getSourceOrderId() {
+ return sourceOrderId;
+ }
+
+ public void setSourceOrderId(Long sourceOrderId) {
+ this.sourceOrderId = sourceOrderId;
+ }
+
+ public LocalDateTime getCreatedAt() {
+ return createdAt;
+ }
+
+ public void setCreatedAt(LocalDateTime createdAt) {
+ this.createdAt = createdAt;
+ }
}
diff --git a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/domain/RefundApplication.java b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/domain/RefundApplication.java
new file mode 100644
index 0000000..01b5f05
--- /dev/null
+++ b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/domain/RefundApplication.java
@@ -0,0 +1,127 @@
+package cn.novalon.gym.manage.gymmembercard.domain;
+
+import cn.novalon.gym.manage.sys.core.domain.BaseDomain;
+import io.swagger.v3.oas.annotations.media.Schema;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+/**
+ * 退款申请领域对象
+ *
+ * @author shizhounian
+ * @date 2026-05-23 21:11:46
+ */
+@Schema(description = "退款申请", example = "refund_application")
+public class RefundApplication extends BaseDomain {
+
+ @Schema(description = "退款申请ID", example = "1")
+ private Long id;
+
+ @Schema(description = "会员卡记录ID", example = "1")
+ private Long recordId;
+
+ @Schema(description = "会员ID", example = "1")
+ private Long memberId;
+
+ @Schema(description = "状态:PENDING-待审核, APPROVED-已批准, REJECTED-已拒绝, PROCESSING-处理中, SUCCESS-成功, FAILED-失败", example = "PENDING")
+ private String status;
+
+ @Schema(description = "退款原因", example = "个人原因申请退款")
+ private String reason;
+
+ @Schema(description = "申请时间", example = "2026-05-23 21:09:14")
+ private LocalDateTime applyTime;
+
+ @Schema(description = "审核时间", example = "2026-05-24 10:00:00")
+ private LocalDateTime auditTime;
+
+ @Schema(description = "审核人ID", example = "1")
+ private Long auditorId;
+
+ @Schema(description = "审核备注", example = "同意退款")
+ private String auditRemark;
+
+ @Schema(description = "退款金额", example = "500.00")
+ private BigDecimal refundAmount;
+
+ public Long getId() {
+ return id;
+ }
+
+ public void setId(Long id) {
+ this.id = id;
+ }
+
+ public Long getRecordId() {
+ return recordId;
+ }
+
+ public void setRecordId(Long recordId) {
+ this.recordId = recordId;
+ }
+
+ public Long getMemberId() {
+ return memberId;
+ }
+
+ public void setMemberId(Long memberId) {
+ this.memberId = memberId;
+ }
+
+ public String getStatus() {
+ return status;
+ }
+
+ public void setStatus(String status) {
+ this.status = status;
+ }
+
+ public String getReason() {
+ return reason;
+ }
+
+ public void setReason(String reason) {
+ this.reason = reason;
+ }
+
+ public LocalDateTime getApplyTime() {
+ return applyTime;
+ }
+
+ public void setApplyTime(LocalDateTime applyTime) {
+ this.applyTime = applyTime;
+ }
+
+ public LocalDateTime getAuditTime() {
+ return auditTime;
+ }
+
+ public void setAuditTime(LocalDateTime auditTime) {
+ this.auditTime = auditTime;
+ }
+
+ public Long getAuditorId() {
+ return auditorId;
+ }
+
+ public void setAuditorId(Long auditorId) {
+ this.auditorId = auditorId;
+ }
+
+ public String getAuditRemark() {
+ return auditRemark;
+ }
+
+ public void setAuditRemark(String auditRemark) {
+ this.auditRemark = auditRemark;
+ }
+
+ public BigDecimal getRefundAmount() {
+ return refundAmount;
+ }
+
+ public void setRefundAmount(BigDecimal refundAmount) {
+ this.refundAmount = refundAmount;
+ }
+}
diff --git a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/entity/MemberCardEntity.java b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/entity/MemberCardEntity.java
index 06ff025..d57641e 100644
--- a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/entity/MemberCardEntity.java
+++ b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/entity/MemberCardEntity.java
@@ -11,7 +11,6 @@ import java.time.LocalDateTime;
*@Author:shizhounian
*@Date:2026/5/10-05 22:47:31
*/
-@Data
@Table("member_card")
public class MemberCardEntity extends BaseEntity {
//会员卡id
@@ -49,4 +48,76 @@ public class MemberCardEntity extends BaseEntity {
//会员卡创建时间
@Column("member_card_create_time")
private LocalDateTime memberCardCreateTime;
+
+ public Long getMemberCardId() {
+ return memberCardId;
+ }
+
+ public void setMemberCardId(Long memberCardId) {
+ this.memberCardId = memberCardId;
+ }
+
+ public String getMemberCardName() {
+ return memberCardName;
+ }
+
+ public void setMemberCardName(String memberCardName) {
+ this.memberCardName = memberCardName;
+ }
+
+ public String getMemberCardType() {
+ return memberCardType;
+ }
+
+ public void setMemberCardType(String memberCardType) {
+ this.memberCardType = memberCardType;
+ }
+
+ public Double getMemberCardPrice() {
+ return memberCardPrice;
+ }
+
+ public void setMemberCardPrice(Double memberCardPrice) {
+ this.memberCardPrice = memberCardPrice;
+ }
+
+ public Integer getMemberCardValidityDays() {
+ return memberCardValidityDays;
+ }
+
+ public void setMemberCardValidityDays(Integer memberCardValidityDays) {
+ this.memberCardValidityDays = memberCardValidityDays;
+ }
+
+ public Integer getMemberCardTotalTimes() {
+ return memberCardTotalTimes;
+ }
+
+ public void setMemberCardTotalTimes(Integer memberCardTotalTimes) {
+ this.memberCardTotalTimes = memberCardTotalTimes;
+ }
+
+ public Double getMemberCardAmount() {
+ return memberCardAmount;
+ }
+
+ public void setMemberCardAmount(Double memberCardAmount) {
+ this.memberCardAmount = memberCardAmount;
+ }
+
+ public Integer getMemberCardStatus() {
+ return memberCardStatus;
+ }
+
+ public void setMemberCardStatus(Integer memberCardStatus) {
+ this.memberCardStatus = memberCardStatus;
+ }
+
+ public LocalDateTime getMemberCardCreateTime() {
+ return memberCardCreateTime;
+ }
+
+ public void setMemberCardCreateTime(LocalDateTime memberCardCreateTime) {
+ this.memberCardCreateTime = memberCardCreateTime;
+ }
}
diff --git a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/entity/MemberCardRecordEntity.java b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/entity/MemberCardRecordEntity.java
index 2c1e1c6..a601f43 100644
--- a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/entity/MemberCardRecordEntity.java
+++ b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/entity/MemberCardRecordEntity.java
@@ -12,7 +12,6 @@ import java.time.LocalDateTime;
*@Author:shizhounian
*@Date:2026/5/10-05 22:47:31
*/
-@Data
@Table("member_card_record")
public class MemberCardRecordEntity extends BaseEntity {
//会员持有卡id
@@ -50,4 +49,76 @@ public class MemberCardRecordEntity extends BaseEntity {
//购买时间
@Column("purchase_time")
private LocalDateTime purchaseTime;
+
+ public Long getMemberCardRecordId() {
+ return memberCardRecordId;
+ }
+
+ public void setMemberCardRecordId(Long memberCardRecordId) {
+ this.memberCardRecordId = memberCardRecordId;
+ }
+
+ public Long getMemberId() {
+ return memberId;
+ }
+
+ public void setMemberId(Long memberId) {
+ this.memberId = memberId;
+ }
+
+ public Long getMemberCardId() {
+ return memberCardId;
+ }
+
+ public void setMemberCardId(Long memberCardId) {
+ this.memberCardId = memberCardId;
+ }
+
+ public MemberCardRecordStatus getStatus() {
+ return status;
+ }
+
+ public void setStatus(MemberCardRecordStatus status) {
+ this.status = status;
+ }
+
+ public Integer getRemainingTimes() {
+ return remainingTimes;
+ }
+
+ public void setRemainingTimes(Integer remainingTimes) {
+ this.remainingTimes = remainingTimes;
+ }
+
+ public Double getRemainingAmount() {
+ return remainingAmount;
+ }
+
+ public void setRemainingAmount(Double remainingAmount) {
+ this.remainingAmount = remainingAmount;
+ }
+
+ public LocalDateTime getExpireTime() {
+ return expireTime;
+ }
+
+ public void setExpireTime(LocalDateTime expireTime) {
+ this.expireTime = expireTime;
+ }
+
+ public Long getSourceOrderId() {
+ return sourceOrderId;
+ }
+
+ public void setSourceOrderId(Long sourceOrderId) {
+ this.sourceOrderId = sourceOrderId;
+ }
+
+ public LocalDateTime getPurchaseTime() {
+ return purchaseTime;
+ }
+
+ public void setPurchaseTime(LocalDateTime purchaseTime) {
+ this.purchaseTime = purchaseTime;
+ }
}
diff --git a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/entity/MemberCardTransactionsEntity.java b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/entity/MemberCardTransactionsEntity.java
index b988065..1aedaea 100644
--- a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/entity/MemberCardTransactionsEntity.java
+++ b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/entity/MemberCardTransactionsEntity.java
@@ -3,7 +3,6 @@ package cn.novalon.gym.manage.gymmembercard.entity;
import cn.novalon.gym.manage.db.entity.BaseEntity;
import cn.novalon.gym.manage.gymmembercard.enums.MemberCardTransactionsAction;
import cn.novalon.gym.manage.gymmembercard.enums.MemberCardTransactionsType;
-import lombok.Data;
import org.springframework.data.relational.core.mapping.Column;
import org.springframework.data.relational.core.mapping.Table;
@@ -13,50 +12,149 @@ import java.time.LocalDateTime;
*@Author:shizhounian
*@Date:2026/5/10-05 22:47:31
*/
-@Data
@Table("member_card_transactions")
-public class MemberCardTransactionsEntity extends BaseEntity {
- //会员卡流水Id
- @Column("member_card_transactions_id")
- private Long memberCardTransactionsId;
+public class MemberCardTransactionsEntity extends BaseEntity{
- //会员卡Id
- @Column("member_card_id")
- private Long memberCardId;
+ @Column("id")
+ private Long id;
+
+ @Column("member_card_record_id")
+ private Long memberCardRecordId;
- //会员Id
@Column("member_id")
private Long memberId;
- //操作类型:PURCHASE(购买) / DEDUCT(扣次/扣费) / RENEW(续费) / REFUND(退款) / EXPIRE(过期)
+ @Column("member_card_id")
+ private Long memberCardId;
+
@Column("operation_type")
private MemberCardTransactionsAction operationType;
- //变动次数(次卡用)
@Column("change_amount")
private Integer changeAmount;
- //变动金额(储值卡用)
@Column("change_balance")
private Double changeBalance;
- //变动后剩余次数
@Column("after_remaining_count")
private Integer afterRemainingCount;
- //变动后剩余金额
@Column("after_remaining_balance")
private Double afterRemainingBalance;
- //关联业务类型
@Column("related_biz_type")
private MemberCardTransactionsType relatedBizType;
- //备注
+ @Column("source_order_id")
+ private Long sourceOrderId;
+
@Column("remark")
private String remark;
- //创建时间
@Column("created_at")
private LocalDateTime createdAt;
-}
\ No newline at end of file
+
+ public Long getId() {
+ return id;
+ }
+
+ public void setId(Long id) {
+ this.id = id;
+ }
+
+ public Long getMemberCardRecordId() {
+ return memberCardRecordId;
+ }
+
+ public void setMemberCardRecordId(Long memberCardRecordId) {
+ this.memberCardRecordId = memberCardRecordId;
+ }
+
+ public Long getMemberId() {
+ return memberId;
+ }
+
+ public void setMemberId(Long memberId) {
+ this.memberId = memberId;
+ }
+
+ public Long getMemberCardId() {
+ return memberCardId;
+ }
+
+ public void setMemberCardId(Long memberCardId) {
+ this.memberCardId = memberCardId;
+ }
+
+ public MemberCardTransactionsAction getOperationType() {
+ return operationType;
+ }
+
+ public void setOperationType(MemberCardTransactionsAction operationType) {
+ this.operationType = operationType;
+ }
+
+ public Integer getChangeAmount() {
+ return changeAmount;
+ }
+
+ public void setChangeAmount(Integer changeAmount) {
+ this.changeAmount = changeAmount;
+ }
+
+ public Double getChangeBalance() {
+ return changeBalance;
+ }
+
+ public void setChangeBalance(Double changeBalance) {
+ this.changeBalance = changeBalance;
+ }
+
+ public Integer getAfterRemainingCount() {
+ return afterRemainingCount;
+ }
+
+ public void setAfterRemainingCount(Integer afterRemainingCount) {
+ this.afterRemainingCount = afterRemainingCount;
+ }
+
+ public Double getAfterRemainingBalance() {
+ return afterRemainingBalance;
+ }
+
+ public void setAfterRemainingBalance(Double afterRemainingBalance) {
+ this.afterRemainingBalance = afterRemainingBalance;
+ }
+
+ public MemberCardTransactionsType getRelatedBizType() {
+ return relatedBizType;
+ }
+
+ public void setRelatedBizType(MemberCardTransactionsType relatedBizType) {
+ this.relatedBizType = relatedBizType;
+ }
+
+ public Long getSourceOrderId() {
+ return sourceOrderId;
+ }
+
+ public void setSourceOrderId(Long sourceOrderId) {
+ this.sourceOrderId = sourceOrderId;
+ }
+
+ public String getRemark() {
+ return remark;
+ }
+
+ public void setRemark(String remark) {
+ this.remark = remark;
+ }
+
+ public LocalDateTime getCreatedAt() {
+ return createdAt;
+ }
+
+ public void setCreatedAt(LocalDateTime createdAt) {
+ this.createdAt = createdAt;
+ }
+}
diff --git a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/entity/RefundApplicationEntity.java b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/entity/RefundApplicationEntity.java
new file mode 100644
index 0000000..49fd781
--- /dev/null
+++ b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/entity/RefundApplicationEntity.java
@@ -0,0 +1,127 @@
+package cn.novalon.gym.manage.gymmembercard.entity;
+
+import cn.novalon.gym.manage.db.entity.BaseEntity;
+import io.swagger.v3.oas.annotations.media.Schema;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+/**
+ * 退款申请实体类
+ *
+ * @author shizhounian
+ * @date 2026-05-23 21:09:14
+ */
+@Schema(description = "退款申请实体", example = "refund_application")
+public class RefundApplicationEntity extends BaseEntity {
+
+ @Schema(description = "退款申请ID", example = "1")
+ private Long id;
+
+ @Schema(description = "会员卡记录ID", example = "1")
+ private Long recordId;
+
+ @Schema(description = "会员ID", example = "1")
+ private Long memberId;
+
+ @Schema(description = "状态:PENDING-待审核, APPROVED-已批准, REJECTED-已拒绝, PROCESSING-处理中, SUCCESS-成功, FAILED-失败", example = "PENDING")
+ private String status;
+
+ @Schema(description = "退款原因", example = "个人原因申请退款")
+ private String reason;
+
+ @Schema(description = "申请时间", example = "2026-05-23 21:09:14")
+ private LocalDateTime applyTime;
+
+ @Schema(description = "审核时间", example = "2026-05-24 10:00:00")
+ private LocalDateTime auditTime;
+
+ @Schema(description = "审核人ID", example = "1")
+ private Long auditorId;
+
+ @Schema(description = "审核备注", example = "同意退款")
+ private String auditRemark;
+
+ @Schema(description = "退款金额", example = "500.00")
+ private BigDecimal refundAmount;
+
+ public Long getId() {
+ return id;
+ }
+
+ public void setId(Long id) {
+ this.id = id;
+ }
+
+ public Long getRecordId() {
+ return recordId;
+ }
+
+ public void setRecordId(Long recordId) {
+ this.recordId = recordId;
+ }
+
+ public Long getMemberId() {
+ return memberId;
+ }
+
+ public void setMemberId(Long memberId) {
+ this.memberId = memberId;
+ }
+
+ public String getStatus() {
+ return status;
+ }
+
+ public void setStatus(String status) {
+ this.status = status;
+ }
+
+ public String getReason() {
+ return reason;
+ }
+
+ public void setReason(String reason) {
+ this.reason = reason;
+ }
+
+ public LocalDateTime getApplyTime() {
+ return applyTime;
+ }
+
+ public void setApplyTime(LocalDateTime applyTime) {
+ this.applyTime = applyTime;
+ }
+
+ public LocalDateTime getAuditTime() {
+ return auditTime;
+ }
+
+ public void setAuditTime(LocalDateTime auditTime) {
+ this.auditTime = auditTime;
+ }
+
+ public Long getAuditorId() {
+ return auditorId;
+ }
+
+ public void setAuditorId(Long auditorId) {
+ this.auditorId = auditorId;
+ }
+
+ public String getAuditRemark() {
+ return auditRemark;
+ }
+
+ public void setAuditRemark(String auditRemark) {
+ this.auditRemark = auditRemark;
+ }
+
+ public BigDecimal getRefundAmount() {
+ return refundAmount;
+ }
+
+ public void setRefundAmount(BigDecimal refundAmount) {
+ this.refundAmount = refundAmount;
+ }
+}
diff --git a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/enums/MemberCardEvent.java b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/enums/MemberCardEvent.java
new file mode 100644
index 0000000..42e8094
--- /dev/null
+++ b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/enums/MemberCardEvent.java
@@ -0,0 +1,34 @@
+package cn.novalon.gym.manage.gymmembercard.enums;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+
+@Schema(description = "会员卡状态机事件")
+public enum MemberCardEvent {
+ @Schema(description = "激活卡片")
+ ACTIVATE("激活卡片"),
+
+ @Schema(description = "使用卡片")
+ USE("使用卡片"),
+
+ @Schema(description = "续费")
+ RENEW("续费"),
+
+ @Schema(description = "过期")
+ EXPIRE("过期"),
+
+ @Schema(description = "退款")
+ REFUND("退款"),
+
+ @Schema(description = "禁用")
+ DISABLE("禁用");
+
+ private final String desc;
+
+ MemberCardEvent(String desc) {
+ this.desc = desc;
+ }
+
+ public String getDesc() {
+ return desc;
+ }
+}
\ No newline at end of file
diff --git a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/enums/MemberCardType.java b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/enums/MemberCardType.java
new file mode 100644
index 0000000..decb62a
--- /dev/null
+++ b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/enums/MemberCardType.java
@@ -0,0 +1,25 @@
+package cn.novalon.gym.manage.gymmembercard.enums;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+
+@Schema(description = "会员卡类型枚举")
+public enum MemberCardType {
+ @Schema(description = "时长卡")
+ TIME_CARD("时长卡"),
+
+ @Schema(description = "次卡")
+ COUNT_CARD("次卡"),
+
+ @Schema(description = "储值卡")
+ STORED_VALUE_CARD("储值卡");
+
+ private final String desc;
+
+ MemberCardType(String desc) {
+ this.desc = desc;
+ }
+
+ public String getDesc() {
+ return desc;
+ }
+}
\ No newline at end of file
diff --git a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/handler/DistributedLockService.java b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/handler/DistributedLockService.java
new file mode 100644
index 0000000..08f57ae
--- /dev/null
+++ b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/handler/DistributedLockService.java
@@ -0,0 +1,50 @@
+package cn.novalon.gym.manage.gymmembercard.handler;
+
+import org.springframework.stereotype.Component;
+import reactor.core.publisher.Mono;
+
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.locks.ReentrantLock;
+
+/**
+ * 分布式锁服务(简化版,使用本地锁)
+ *
+ * @author shizhounian
+ * @date 2026-05-23
+ */
+@Component
+public class DistributedLockService {
+
+ private final ConcurrentHashMap locks = new ConcurrentHashMap<>();
+
+ /**
+ * 执行带锁的操作(业务操作)
+ */
+ public Mono executeWithLock(String userId, String cardType, Mono operation) {
+ String lockKey = "lock:member:card:operation:" + userId + ":" + cardType;
+ ReentrantLock lock = locks.computeIfAbsent(lockKey, k -> new ReentrantLock());
+
+ lock.lock();
+ try {
+ return operation.doFinally(signalType -> lock.unlock());
+ } catch (Exception e) {
+ lock.unlock();
+ return Mono.error(e);
+ }
+ }
+
+ /**
+ * 执行带锁的操作(通用/定时任务)
+ */
+ public Mono executeWithLock(String lockKey, Mono operation) {
+ ReentrantLock lock = locks.computeIfAbsent(lockKey, k -> new ReentrantLock());
+
+ lock.lock();
+ try {
+ return operation.doFinally(signalType -> lock.unlock());
+ } catch (Exception e) {
+ lock.unlock();
+ return Mono.error(e);
+ }
+ }
+}
diff --git a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/handler/ExpirationReminderService.java b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/handler/ExpirationReminderService.java
new file mode 100644
index 0000000..3050327
--- /dev/null
+++ b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/handler/ExpirationReminderService.java
@@ -0,0 +1,151 @@
+package cn.novalon.gym.manage.gymmembercard.handler;
+
+import cn.novalon.gym.manage.gymmembercard.domain.MemberCardRecord;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.data.domain.Range;
+import org.springframework.data.redis.connection.RedisZSetCommands;
+import org.springframework.data.redis.core.ReactiveStringRedisTemplate;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Component;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+import java.time.Duration;
+import java.time.LocalDateTime;
+import java.util.UUID;
+
+@Slf4j
+@Component
+@RequiredArgsConstructor
+public class ExpirationReminderService {
+
+ private final ReactiveStringRedisTemplate redisTemplate;
+ private final ObjectMapper objectMapper;
+
+ private static final String REMINDER_QUEUE = "queue:member_card_expiration";
+ private static final String DEAD_LETTER_QUEUE = "queue:member_card_expiration_dead_letter";
+ private static final int REMINDER_DAYS_BEFORE = 7;
+ private static final long MAX_DELAY_MILLIS = Duration.ofDays(365).toMillis();
+
+ /**
+ * 设置到期提醒(购卡/续费时调用)
+ */
+ public Mono scheduleExpirationReminder(MemberCardRecord record) {
+ if (record.getExpireTime() == null) {
+ return Mono.empty();
+ }
+
+ LocalDateTime now = LocalDateTime.now();
+
+ Flux reminderFlux = Flux.range(1, REMINDER_DAYS_BEFORE)
+ .flatMap(daysBefore -> {
+ LocalDateTime reminderTime = record.getExpireTime().minusDays(daysBefore);
+
+ if (reminderTime.isBefore(now)) {
+ log.debug("会员卡记录ID={} 的{}天前提醒时间已过,跳过",
+ record.getMemberCardRecordId(), daysBefore);
+ return Mono.empty();
+ }
+
+ long delayMillis = Duration.between(now, reminderTime).toMillis();
+
+ if (delayMillis > MAX_DELAY_MILLIS) {
+ log.warn("会员卡记录ID={} 的{}天后提醒时间超过1年,跳过",
+ record.getMemberCardRecordId(), daysBefore);
+ return Mono.empty();
+ }
+
+ try {
+ String messageId = UUID.randomUUID().toString();
+ String message = objectMapper.writeValueAsString(new ExpirationMessage(
+ messageId,
+ record.getMemberCardRecordId(),
+ record.getMemberId(),
+ record.getExpireTime(),
+ daysBefore
+ ));
+
+ double executeTime = System.currentTimeMillis() + delayMillis;
+
+ return redisTemplate.opsForZSet()
+ .add(REMINDER_QUEUE, message, executeTime)
+ .doOnSuccess(v -> log.info("设置会员卡到期提醒: recordId={}, daysBefore={}, expireTime={}, executeTime={}",
+ record.getMemberCardRecordId(), daysBefore, record.getExpireTime(), executeTime))
+ .then();
+ } catch (Exception e) {
+ log.error("设置会员卡到期提醒失败: recordId={}, daysBefore={}",
+ record.getMemberCardRecordId(), daysBefore, e);
+ return Mono.error(e);
+ }
+ });
+
+ return reminderFlux.then();
+ }
+
+ /**
+ * 定时任务:每分钟扫描到期的提醒并发送
+ */
+ @Scheduled(fixedRate = 60000)
+ public void processDueReminders() {
+ double now = System.currentTimeMillis();
+
+ redisTemplate.opsForZSet()
+ .rangeByScoreWithScores(
+ REMINDER_QUEUE,
+ Range.from(Range.Bound.inclusive(0.0))
+ .to(Range.Bound.inclusive(now)),
+ RedisZSetCommands.Limit.limit().count(100)
+ )
+ .flatMap(tuple -> {
+ String message = tuple.getValue();
+ if (message == null) {
+ return Mono.empty();
+ }
+
+ return Mono.fromCallable(() -> objectMapper.readValue(message, ExpirationMessage.class))
+ .flatMap(expirationMessage -> {
+ log.info("处理到期提醒: messageId={}, memberId={}, expireTime={}, daysBefore={}",
+ expirationMessage.messageId(),
+ expirationMessage.memberId(),
+ expirationMessage.expireTime(),
+ expirationMessage.daysBefore());
+
+ // TODO: 集成微信/短信通知服务
+ sendNotification(expirationMessage);
+
+ return redisTemplate.opsForZSet()
+ .remove(REMINDER_QUEUE, message)
+ .doOnSuccess(removed -> {
+ if (removed > 0) {
+ log.info("成功删除已处理的提醒消息");
+ }
+ });
+ })
+ .onErrorResume(e -> {
+ log.error("解析到期提醒消息失败,移至死信队列: message={}", message, e);
+ return redisTemplate.opsForZSet()
+ .add(DEAD_LETTER_QUEUE, message, System.currentTimeMillis())
+ .then(Mono.empty());
+ });
+ })
+ .then()
+ .subscribe();
+ }
+
+
+ private void sendNotification(ExpirationMessage reminder) {
+ // TODO: 实际项目中调用微信模板消息或短信API
+ log.info("[模拟发送] 会员卡到期提醒 - 会员ID: {}, 到期时间: {}, 提前天数: {}",
+ reminder.memberId(), reminder.expireTime(), reminder.daysBefore());
+ }
+
+ public record ExpirationMessage(
+ String messageId,
+ Long recordId,
+ Long memberId,
+ LocalDateTime expireTime,
+ int daysBefore
+ ) {}
+}
diff --git a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/handler/MemberCardHandler.java b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/handler/MemberCardHandler.java
index eaff5f0..1744819 100644
--- a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/handler/MemberCardHandler.java
+++ b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/handler/MemberCardHandler.java
@@ -1,114 +1,133 @@
package cn.novalon.gym.manage.gymmembercard.handler;
-import cn.hutool.db.PageResult;
import cn.novalon.gym.manage.gymmembercard.domain.MemberCard;
+import cn.novalon.gym.manage.gymmembercard.domain.MemberCardRecord;
import cn.novalon.gym.manage.gymmembercard.sevice.IMemberCardService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
-import jakarta.validation.Validator;
+import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.PageRequest;
-import org.springframework.data.domain.Pageable;
-import org.springframework.data.domain.Sort;
+import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
-import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
-import java.util.List;
-/*
- *@Author:shizhounian
- *@Date:2026/5/17-05 20:10:22
+/**
+ * 会员卡管理处理器
+ *
+ * @author shizhounian
+ * @date 2026-05-23
*/
+@Slf4j
@Component
-@Tag(name = "会员卡类型管理", description = "会员卡类型相关操作")
+@Tag(name = "会员卡管理", description = "会员卡类型管理和会员持卡管理")
public class MemberCardHandler {
- private final IMemberCardService memberCardService;
- private final Validator validator;
- public MemberCardHandler(IMemberCardService memberCardService, Validator validator) {
+ private final IMemberCardService memberCardService;
+
+ public MemberCardHandler(IMemberCardService memberCardService) {
this.memberCardService = memberCardService;
- this.validator = validator;
}
- /**
- * 根据会员卡ID查询会员卡详情
- */
- @Operation(summary = "根据会员卡ID查询会员卡详情", description = "用于编辑前回显或小程序端展示")
+ @Operation(summary = "根据ID查询会员卡类型", description = "查询指定ID的会员卡类型详情")
public Mono getMemberCardById(ServerRequest request) {
- Long memberCardId = Long.parseLong(request.pathVariable("memberCardId"));
- return memberCardService.findByMemberCardIdAndDeletedAtIsNull(memberCardId)
+ Long id = Long.valueOf(request.pathVariable("id"));
+ return memberCardService.findByMemberCardIdAndDeletedAtIsNull(id)
.flatMap(card -> ServerResponse.ok().bodyValue(card))
.switchIfEmpty(ServerResponse.notFound().build());
}
- /**
- * 条件查询会员卡列表(分页)
- */
- @Operation(summary = "条件查询会员卡列表", description = "后台卡种列表展示,支持多条件组合+分页+排序")
- public Mono getMemberCardList(ServerRequest request) {
- Integer status = request.queryParam("status").map(Integer::parseInt).orElse(null);
+ @Operation(summary = "查询会员卡类型列表", description = "支持分页和条件查询")
+ public Mono listMemberCards(ServerRequest request) {
+ Integer status = request.queryParam("status").map(Integer::valueOf).orElse(null);
String name = request.queryParam("name").orElse(null);
String type = request.queryParam("type").orElse(null);
- Double minPrice = request.queryParam("minPrice").map(Double::parseDouble).orElse(null);
- Double maxPrice = request.queryParam("maxPrice").map(Double::parseDouble).orElse(null);
+ Double minPrice = request.queryParam("minPrice").map(Double::valueOf).orElse(null);
+ Double maxPrice = request.queryParam("maxPrice").map(Double::valueOf).orElse(null);
+ int page = request.queryParam("page").map(Integer::valueOf).orElse(0);
+ int size = request.queryParam("size").map(Integer::valueOf).orElse(10);
- int page = request.queryParam("page").map(Integer::parseInt).orElse(0);
- int size = request.queryParam("size").map(Integer::parseInt).orElse(10);
- String sortField = request.queryParam("sortField").orElse("created_at");
- String sortOrder = request.queryParam("sortOrder").orElse("DESC");
-
- Pageable pageable = PageRequest.of(page, size, Sort.by(Sort.Direction.fromString(sortOrder), sortField));
-
- Mono countMono = memberCardService.countWithConditions(status, name, type, minPrice, maxPrice);
- Flux cardsFlux = memberCardService.findWithConditions(status, name, type, minPrice, maxPrice, pageable);
-
- return Mono.zip(countMono, cardsFlux.collectList())
- .flatMap(tuple -> {
- long total = tuple.getT1();
- List list = tuple.getT2();
-
- // 使用 PageResult(int page, int pageSize, int total) 构造,
- // 它会自动计算 totalPage
- PageResult result = new PageResult<>(page, size, (int) total);
- result.addAll(list); // 将查询结果填充进去
-
- return ServerResponse.ok().bodyValue(result);
- });
+ var pageable = PageRequest.of(page, size);
+ return ServerResponse.ok()
+ .body(memberCardService.findWithConditions(status, name, type, minPrice, maxPrice, pageable),
+ MemberCard.class);
}
- /**
- * 保存卡种信息(新增或更新)
- */
- @Operation(summary = "保存卡种信息", description = "新增或更新会员卡类型")
- public Mono saveMemberCard(ServerRequest request) {
+ @Operation(summary = "创建会员卡类型", description = "创建新的会员卡类型(时长卡、次卡或储值卡)")
+ public Mono createMemberCard(ServerRequest request) {
return request.bodyToMono(MemberCard.class)
.flatMap(memberCardService::save)
- .flatMap(card -> ServerResponse.ok().bodyValue(card));
+ .flatMap(card -> ServerResponse.status(HttpStatus.CREATED).bodyValue(card));
}
- /**
- * 逻辑删除会员卡(下架)
- */
- @Operation(summary = "逻辑删除会员卡", description = "下架卡种,防止已购会员数据异常")
+ @Operation(summary = "更新会员卡类型", description = "更新会员卡类型信息")
+ public Mono updateMemberCard(ServerRequest request) {
+ Long id = Long.valueOf(request.pathVariable("id"));
+ return request.bodyToMono(MemberCard.class)
+ .flatMap(card -> {
+ card.setMemberCardId(id);
+ return memberCardService.save(card);
+ })
+ .flatMap(updated -> ServerResponse.ok().bodyValue(updated))
+ .switchIfEmpty(ServerResponse.notFound().build());
+ }
+
+ @Operation(summary = "删除会员卡类型", description = "逻辑删除会员卡类型")
public Mono deleteMemberCard(ServerRequest request) {
- Long memberCardId = Long.parseLong(request.pathVariable("memberCardId"));
- return memberCardService.logicalDelete(memberCardId)
+ Long id = Long.valueOf(request.pathVariable("id"));
+ return memberCardService.logicalDelete(id)
.flatMap(rows -> {
if (rows > 0) {
return ServerResponse.noContent().build();
- } else {
- return ServerResponse.notFound().build();
}
+ return ServerResponse.notFound().build();
});
}
- /**
- * 批量查询上架的会员卡
- */
- @Operation(summary = "批量查询上架的会员卡", description = "用于小程序端展示")
- public Mono getActiveCards(ServerRequest request) {
- Integer status = request.queryParam("status").map(Integer::parseInt).orElse(0);
- return ServerResponse.ok().body(memberCardService.findActiveCards(status), MemberCard.class);
+ @Operation(summary = "购买会员卡", description = "会员购买会员卡,生成会员卡记录")
+ public Mono purchaseCard(ServerRequest request) {
+ Long memberId = Long.valueOf(request.queryParam("memberId").orElseThrow());
+ Long memberCardId = Long.valueOf(request.queryParam("memberCardId").orElseThrow());
+ Long sourceOrderId = request.queryParam("sourceOrderId").map(Long::valueOf).orElse(null);
+
+ return memberCardService.purchaseCard(memberId, memberCardId, sourceOrderId)
+ .flatMap(record -> ServerResponse.status(HttpStatus.CREATED).bodyValue(record));
}
-}
\ No newline at end of file
+
+ @Operation(summary = "续费会员卡", description = "为已有会员卡续费")
+ public Mono renewCard(ServerRequest request) {
+ Long recordId = Long.valueOf(request.queryParam("recordId").orElseThrow());
+ Integer addTimes = request.queryParam("addTimes").map(Integer::valueOf).orElse(null);
+ Double addAmount = request.queryParam("addAmount").map(Double::valueOf).orElse(null);
+ Integer addDays = request.queryParam("addDays").map(Integer::valueOf).orElse(null);
+ Long sourceOrderId = request.queryParam("sourceOrderId").map(Long::valueOf).orElse(null);
+
+ return memberCardService.renewCard(recordId, addTimes, addAmount, addDays, sourceOrderId)
+ .flatMap(record -> ServerResponse.ok().bodyValue(record));
+ }
+
+ @Operation(summary = "使用会员卡", description = "扣减会员卡次数或余额")
+ public Mono useCard(ServerRequest request) {
+ Long recordId = Long.valueOf(request.queryParam("recordId").orElseThrow());
+ Integer deductTimes = request.queryParam("deductTimes").map(Integer::valueOf).orElse(null);
+ Double deductAmount = request.queryParam("deductAmount").map(Double::valueOf).orElse(null);
+
+ return memberCardService.useCard(recordId, deductTimes, deductAmount)
+ .flatMap(record -> ServerResponse.ok().bodyValue(record));
+ }
+
+ @Operation(summary = "退款会员卡", description = "申请会员卡退款")
+ public Mono refundCard(ServerRequest request) {
+ Long recordId = Long.valueOf(request.queryParam("recordId").orElseThrow());
+ return memberCardService.refundCard(recordId)
+ .then(ServerResponse.noContent().build());
+ }
+
+ @Operation(summary = "查询有效会员卡", description = "查询指定状态的会员卡类型")
+ public Mono getActiveCards(ServerRequest request) {
+ Integer status = request.queryParam("status").map(Integer::valueOf).orElse(1);
+ return ServerResponse.ok()
+ .body(memberCardService.findActiveCards(status), MemberCard.class);
+ }
+}
diff --git a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/handler/MemberCardRecordHandler.java b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/handler/MemberCardRecordHandler.java
index 3485d5b..77ee607 100644
--- a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/handler/MemberCardRecordHandler.java
+++ b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/handler/MemberCardRecordHandler.java
@@ -1,171 +1,103 @@
package cn.novalon.gym.manage.gymmembercard.handler;
import cn.novalon.gym.manage.gymmembercard.domain.MemberCardRecord;
-import cn.novalon.gym.manage.gymmembercard.enums.MemberCardRecordStatus;
import cn.novalon.gym.manage.gymmembercard.sevice.IMemberCardRecordService;
+import cn.novalon.gym.manage.gymmembercard.sevice.IMemberCardService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
-import jakarta.validation.Validator;
import lombok.Data;
-import org.springframework.data.domain.PageRequest;
-import org.springframework.data.domain.Pageable;
-import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
import reactor.core.publisher.Mono;
-import java.time.LocalDateTime;
-
-/*
- *@Author:shizhounian
- *@Date:2026/5/17-05 20:10:22
- */
@Component
-@Tag(name = "会员卡记录管理", description = "会员卡记录相关操作")
+@Tag(name = "会员卡记录管理", description = "会员卡购买、续费、使用、退款等核心业务")
public class MemberCardRecordHandler {
+ private final IMemberCardService memberCardService;
private final IMemberCardRecordService memberCardRecordService;
- private final Validator validator;
- public MemberCardRecordHandler(IMemberCardRecordService memberCardRecordService, Validator validator) {
+ public MemberCardRecordHandler(IMemberCardService memberCardService,
+ IMemberCardRecordService memberCardRecordService) {
+ this.memberCardService = memberCardService;
this.memberCardRecordService = memberCardRecordService;
- this.validator = validator;
}
- /**
- * 会员购卡/后台发卡
- */
- @Operation(summary = "会员购卡/后台发卡", description = "支付成功后插入一条记录,状态为ACTIVE")
- public Mono insertActiveRecord(ServerRequest request) {
- return request.bodyToMono(MemberCardRecord.class)
- .flatMap(memberCardRecordService::insertActiveRecord)
- .flatMap(record -> ServerResponse.ok().bodyValue(record));
+ @Operation(summary = "购买会员卡", description = "支持时长卡、次卡、储值卡,自动设置到期提醒")
+ public Mono purchaseCard(ServerRequest request) {
+ return request.bodyToMono(PurchaseRequest.class)
+ .flatMap(body -> memberCardService.purchaseCard(
+ body.getMemberId(),
+ body.getMemberCardId(),
+ body.getSourceOrderId()))
+ .flatMap(record -> ServerResponse.ok().bodyValue(record))
+ .onErrorResume(e -> ServerResponse.badRequest().bodyValue("购买失败: " + e.getMessage()));
}
- /**
- * 扣次/扣费
- */
- @Operation(summary = "扣次/扣费", description = "预约团课或私教成功后扣减次数或余额")
- public Mono deductUsage(ServerRequest request) {
- Long recordId = Long.parseLong(request.pathVariable("id"));
- return request.bodyToMono(DeductRequest.class)
- .flatMap(body -> memberCardRecordService.deductUsage(recordId,
- body.getDeductTimes(),
- body.getDeductAmount()))
- .flatMap(rows -> {
- if (rows > 0) {
- return ServerResponse.ok().build();
- } else {
- return ServerResponse.badRequest().bodyValue("余额不足或卡无效");
- }
- });
- }
-
- /**
- * 续费
- */
- @Operation(summary = "续费", description = "累加剩余次数/余额,顺延到期日期")
+ @Operation(summary = "续费会员卡", description = "累加剩余次数/余额,顺延到期日期,权益立即生效")
public Mono renewCard(ServerRequest request) {
- Long recordId = Long.parseLong(request.pathVariable("id"));
+ Long recordId = Long.parseLong(request.pathVariable("recordId"));
return request.bodyToMono(RenewRequest.class)
- .flatMap(body -> memberCardRecordService.renewCard(recordId,
+ .flatMap(body -> memberCardService.renewCard(recordId,
body.getAddTimes(),
body.getAddAmount(),
- body.getNewExpireTime()))
- .flatMap(rows -> {
- if (rows > 0) {
- return ServerResponse.ok().build();
- } else {
- return ServerResponse.notFound().build();
- }
- });
+ body.getAddDays(),
+ body.getSourceOrderId()))
+ .flatMap(record -> ServerResponse.ok().bodyValue(record))
+ .onErrorResume(e -> ServerResponse.badRequest().bodyValue("续费失败: " + e.getMessage()));
}
- /**
- * 状态变更(过期、退款)
- */
- @Operation(summary = "状态变更", description = "将卡状态改为EXPIRED或REFUNDED")
- public Mono updateStatus(ServerRequest request) {
- Long recordId = Long.parseLong(request.pathVariable("id"));
- return request.queryParam("status")
- .map(s -> {
- try {
- return MemberCardRecordStatus.valueOf(s.toUpperCase());
- } catch (IllegalArgumentException e) {
- return null;
- }
- })
- .map(status -> memberCardRecordService.updateStatus(recordId, status)
- .flatMap(rows -> {
- if (rows > 0) {
- return ServerResponse.ok().build();
- } else {
- return ServerResponse.notFound().build();
- }
- }))
- .orElse(ServerResponse.badRequest().bodyValue("status 参数缺失或无效"));
+ @Operation(summary = "使用会员卡", description = "预约团课或私教成功后扣减次数或余额")
+ public Mono useCard(ServerRequest request) {
+ Long recordId = Long.parseLong(request.pathVariable("recordId"));
+ return request.bodyToMono(UseCardRequest.class)
+ .flatMap(body -> memberCardService.useCard(recordId,
+ body.getDeductTimes(),
+ body.getDeductAmount()))
+ .flatMap(record -> ServerResponse.ok().bodyValue(record))
+ .onErrorResume(e -> ServerResponse.badRequest().bodyValue("使用失败: " + e.getMessage()));
}
- /**
- * 会员端“我的卡包”
- */
- @Operation(summary = "会员我的卡包", description = "查询当前登录会员的所有有效卡")
+ @Operation(summary = "退款会员卡", description = "使用Saga模式执行退款流程,保证事务一致性")
+ public Mono refundCard(ServerRequest request) {
+ Long recordId = Long.parseLong(request.pathVariable("recordId"));
+ return memberCardService.refundCard(recordId)
+ .then(ServerResponse.ok().bodyValue("退款成功"))
+ .onErrorResume(e -> ServerResponse.badRequest().bodyValue("退款失败: " + e.getMessage()));
+ }
+
+ @Operation(summary = "查询会员卡记录详情", description = "根据记录ID查询详细信息")
+ public Mono getMemberCardRecordById(ServerRequest request) {
+ Long recordId = Long.parseLong(request.pathVariable("recordId"));
+ return memberCardRecordService.findById(recordId)
+ .flatMap(record -> ServerResponse.ok().bodyValue(record))
+ .switchIfEmpty(ServerResponse.notFound().build());
+ }
+
+ @Operation(summary = "会员我的卡包", description = "查询当前会员的所有有效卡")
public Mono getMyCards(ServerRequest request) {
Long memberId = Long.parseLong(request.pathVariable("memberId"));
- return ServerResponse.ok().body(memberCardRecordService.findActiveCardsByMemberId(memberId),
+ return ServerResponse.ok().body(
+ memberCardRecordService.findActiveCardsByMemberId(memberId),
MemberCardRecord.class);
}
- /**
- * 前台/店长查会员卡(分页)
- */
- @Operation(summary = "管理端查询会员卡", description = "按会员ID分页查询所有会员卡记录")
- public Mono getMemberCardRecords(ServerRequest request) {
- Long memberId = Long.parseLong(request.pathVariable("memberId"));
- int page = request.queryParam("page").map(Integer::parseInt).orElse(0);
- int size = request.queryParam("size").map(Integer::parseInt).orElse(10);
- Pageable pageable = PageRequest.of(page, size, Sort.by("purchase_time").descending());
- return ServerResponse.ok().body(memberCardRecordService.findByMemberId(memberId, pageable),
- MemberCardRecord.class);
+ @Operation(summary = "处理过期会员卡", description = "定时任务调用,扫描并更新过期卡状态")
+ public Mono processExpiredCards(ServerRequest request) {
+ return memberCardService.processExpiredCards()
+ .flatMap(count -> ServerResponse.ok().bodyValue("处理完成,共处理" + count + "条"));
}
- /**
- * 验证次卡是否可用
- */
- @Operation(summary = "验证次卡", description = "校验次卡剩余次数是否足够")
- public Mono validateCountCard(ServerRequest request) {
- Long recordId = Long.parseLong(request.pathVariable("id"));
- Integer requiredTimes = request.queryParam("times").map(Integer::parseInt).orElse(1);
- return memberCardRecordService.validateCountCard(recordId, requiredTimes)
- .flatMap(card -> ServerResponse.ok().bodyValue(card))
- .switchIfEmpty(ServerResponse.badRequest().bodyValue("次卡不可用"));
- }
-
- /**
- * 验证储值卡是否可用
- */
- @Operation(summary = "验证储值卡", description = "校验储值卡余额是否足够")
- public Mono validateStoredCard(ServerRequest request) {
- Long recordId = Long.parseLong(request.pathVariable("id"));
- Double requiredAmount = request.queryParam("amount").map(Double::parseDouble).orElse(0.0);
- return memberCardRecordService.validateStoredCard(recordId, requiredAmount)
- .flatMap(card -> ServerResponse.ok().bodyValue(card))
- .switchIfEmpty(ServerResponse.badRequest().bodyValue("储值卡不可用"));
- }
-
- /**
- * 到期扫描(管理端触发)
- */
- @Operation(summary = "到期扫描", description = "扫描并返回已过期的会员卡(最多500条)")
- public Mono getExpiredCards(ServerRequest request) {
- return ServerResponse.ok().body(memberCardRecordService.findExpiredCards(), MemberCardRecord.class);
- }
-
- // ==================== 内部请求体 DTO ====================
-
@Data
- public static class DeductRequest {
+ public static class PurchaseRequest {
+ private Long memberId;
+ private Long memberCardId;
+ private Long sourceOrderId;
+ }
+
+ @Data
+ public static class UseCardRequest {
private Integer deductTimes;
private Double deductAmount;
}
@@ -174,6 +106,7 @@ public class MemberCardRecordHandler {
public static class RenewRequest {
private Integer addTimes;
private Double addAmount;
- private LocalDateTime newExpireTime;
+ private Integer addDays;
+ private Long sourceOrderId;
}
}
\ No newline at end of file
diff --git a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/handler/MemberCardScheduledHandler.java b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/handler/MemberCardScheduledHandler.java
new file mode 100644
index 0000000..de6108b
--- /dev/null
+++ b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/handler/MemberCardScheduledHandler.java
@@ -0,0 +1,99 @@
+package cn.novalon.gym.manage.gymmembercard.handler;
+
+import cn.novalon.gym.manage.gymmembercard.domain.MemberCardRecord;
+import cn.novalon.gym.manage.gymmembercard.enums.MemberCardEvent;
+import cn.novalon.gym.manage.gymmembercard.enums.MemberCardRecordStatus;
+import cn.novalon.gym.manage.gymmembercard.repository.IMemberCardRecordRepository;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Component;
+import reactor.core.publisher.Mono;
+
+import java.time.LocalDateTime;
+
+@Slf4j
+@Component
+@RequiredArgsConstructor
+public class MemberCardScheduledHandler {
+
+ private final IMemberCardRecordRepository recordRepository;
+ private final ExpirationReminderService expirationReminderService;
+ private final MemberCardStateMachine stateMachine;
+ private final DistributedLockService distributedLockService;
+
+ /**
+ * 每日凌晨2点检查过期会员卡
+ */
+ @Scheduled(cron = "0 0 2 * * ?")
+ public void checkExpiredCards() {
+ String lockKey = "scheduled:check_expired_cards";
+
+ distributedLockService.executeWithLock("SYSTEM", "EXPIRE_CHECK",
+ Mono.fromRunnable(() -> {
+ log.info("开始执行会员卡过期检查任务");
+
+ LocalDateTime now = LocalDateTime.now();
+
+ recordRepository.findActiveRecords()
+ .filter(record -> record.getExpireTime() != null && record.getExpireTime().isBefore(now))
+ .flatMap(record ->
+ stateMachine.transition(record.getStatus(), MemberCardEvent.EXPIRE)
+ .flatMap(newState -> {
+ record.setStatus(newState);
+ return recordRepository.save(record);
+ })
+ .doOnSuccess(r -> log.info("会员卡记录ID={} 已标记为过期", r.getMemberCardRecordId()))
+ .onErrorResume(e -> {
+ log.error("处理会员卡过期失败: recordId={}", record.getMemberCardRecordId(), e);
+ return Mono.empty();
+ })
+ )
+ .then()
+ .subscribe();
+ })
+ ).subscribe();
+ }
+
+ /**
+ * 每日凌晨3点检查是否有遗漏的到期提醒(兜底机制)
+ * 主要依赖购卡/续费时的主动调用和每分钟扫描任务,此任务仅用于异常恢复
+ */
+ @Scheduled(cron = "0 0 3 * * ?")
+ public void checkAndSendExpirationReminders() {
+ String lockKey = "scheduled:expiration_reminder";
+
+ distributedLockService.executeWithLock("SYSTEM", "REMINDER_CHECK",
+ Mono.fromRunnable(() -> {
+ log.info("开始执行到期提醒兜底检查任务");
+
+ LocalDateTime now = LocalDateTime.now();
+
+ // 查询所有活跃的会员卡
+ recordRepository.findActiveRecords()
+ .filter(record -> record.getExpireTime() != null)
+ .flatMap(record -> {
+ try {
+ // 计算距离到期还有几天
+ long daysBetween = java.time.Duration.between(now, record.getExpireTime()).toDays();
+
+ // 如果到期时间在1-7天范围内,记录日志供人工检查
+ if (daysBetween >= 1 && daysBetween <= 7) {
+ log.warn("发现到期前{}天的会员卡记录ID={},请确认是否已发送提醒",
+ daysBetween, record.getMemberCardRecordId());
+ }
+
+ return Mono.empty();
+ } catch (Exception e) {
+ log.error("检查到期提醒失败: recordId={}", record.getMemberCardRecordId(), e);
+ return Mono.empty();
+ }
+ })
+ .then()
+ .subscribe();
+ })
+ ).subscribe();
+
+ log.info("到期提醒兜底检查任务完成");
+ }
+}
diff --git a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/handler/MemberCardStateMachine.java b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/handler/MemberCardStateMachine.java
new file mode 100644
index 0000000..e48386e
--- /dev/null
+++ b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/handler/MemberCardStateMachine.java
@@ -0,0 +1,85 @@
+package cn.novalon.gym.manage.gymmembercard.handler;
+
+import cn.novalon.gym.manage.gymmembercard.domain.MemberCardRecord;
+import cn.novalon.gym.manage.gymmembercard.enums.MemberCardEvent;
+import cn.novalon.gym.manage.gymmembercard.enums.MemberCardRecordStatus;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+import reactor.core.publisher.Mono;
+
+import java.util.HashMap;
+import java.util.Map;
+
+@Slf4j
+@Component
+public class MemberCardStateMachine {
+
+ private final Map> stateTransitionMap;
+
+ public MemberCardStateMachine() {
+ this.stateTransitionMap = buildStateTransitionMap();
+ }
+
+ private Map> buildStateTransitionMap() {
+ Map> map = new HashMap<>();
+
+ // ACTIVE 状态可以转换的事件
+ Map activeTransitions = new HashMap<>();
+ activeTransitions.put(MemberCardEvent.USE, MemberCardRecordStatus.ACTIVE);
+ activeTransitions.put(MemberCardEvent.RENEW, MemberCardRecordStatus.ACTIVE);
+ activeTransitions.put(MemberCardEvent.EXPIRE, MemberCardRecordStatus.EXPIRED);
+ activeTransitions.put(MemberCardEvent.REFUND, MemberCardRecordStatus.REFUNDED);
+ map.put(MemberCardRecordStatus.ACTIVE, activeTransitions);
+
+ // USED_UP 状态可以转换的事件
+ Map usedUpTransitions = new HashMap<>();
+ usedUpTransitions.put(MemberCardEvent.RENEW, MemberCardRecordStatus.ACTIVE);
+ usedUpTransitions.put(MemberCardEvent.REFUND, MemberCardRecordStatus.REFUNDED);
+ map.put(MemberCardRecordStatus.USED_UP, usedUpTransitions);
+
+ // EXPIRED 状态可以转换的事件
+ Map expiredTransitions = new HashMap<>();
+ expiredTransitions.put(MemberCardEvent.RENEW, MemberCardRecordStatus.ACTIVE);
+ map.put(MemberCardRecordStatus.EXPIRED, expiredTransitions);
+
+ // REFUNDED 状态是终态,不允许任何转换
+
+ return map;
+ }
+
+ public Mono canTransition(MemberCardRecordStatus currentState, MemberCardEvent event) {
+ return Mono.fromSupplier(() -> {
+ Map transitions = stateTransitionMap.get(currentState);
+ if (transitions == null) {
+ return false;
+ }
+ return transitions.containsKey(event);
+ });
+ }
+
+ public Mono transition(MemberCardRecordStatus currentState, MemberCardEvent event) {
+ return Mono.fromSupplier(() -> {
+ Map transitions = stateTransitionMap.get(currentState);
+ if (transitions == null || !transitions.containsKey(event)) {
+ log.error("Invalid state transition: currentState={}, event={}", currentState, event);
+ throw new IllegalStateException(
+ String.format("不允许的状态转换: 当前状态=%s, 事件=%s", currentState, event));
+ }
+ MemberCardRecordStatus newState = transitions.get(event);
+ log.info("State transition: {} --({})--> {}", currentState, event, newState);
+ return newState;
+ });
+ }
+
+ public Mono validateTransition(MemberCardRecord card, MemberCardEvent event) {
+ return canTransition(card.getStatus(), event)
+ .flatMap(canTransition -> {
+ if (!canTransition) {
+ return Mono.error(new IllegalStateException(
+ String.format("会员卡记录ID=%d 不允许的状态转换: 当前状态=%s, 事件=%s",
+ card.getMemberCardRecordId(), card.getStatus(), event)));
+ }
+ return Mono.empty();
+ });
+ }
+}
diff --git a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/handler/RefundSagaHandler.java b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/handler/RefundSagaHandler.java
new file mode 100644
index 0000000..32c0ca1
--- /dev/null
+++ b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/handler/RefundSagaHandler.java
@@ -0,0 +1,126 @@
+package cn.novalon.gym.manage.gymmembercard.handler;
+
+import cn.novalon.gym.manage.gymmembercard.domain.MemberCardRecord;
+import cn.novalon.gym.manage.gymmembercard.domain.MemberCardTransactions;
+import cn.novalon.gym.manage.gymmembercard.enums.MemberCardEvent;
+import cn.novalon.gym.manage.gymmembercard.enums.MemberCardRecordStatus;
+import cn.novalon.gym.manage.gymmembercard.enums.MemberCardTransactionsAction;
+import cn.novalon.gym.manage.gymmembercard.repository.IMemberCardRecordRepository;
+import cn.novalon.gym.manage.gymmembercard.sevice.IMemberCardTransactionsService;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+import reactor.core.publisher.Mono;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@Slf4j
+@Component
+@RequiredArgsConstructor
+public class RefundSagaHandler {
+
+ private final IMemberCardRecordRepository recordRepository;
+ private final IMemberCardTransactionsService transactionsService;
+ private final MemberCardStateMachine stateMachine;
+
+ public Mono executeRefund(Long recordId) {
+ return recordRepository.findById(recordId)
+ .switchIfEmpty(Mono.error(new RuntimeException("会员卡记录不存在")))
+ .flatMap(record -> stateMachine.validateTransition(record, MemberCardEvent.REFUND)
+ .then(Mono.defer(() -> doExecuteRefund(recordId, record))));
+ }
+
+ private Mono doExecuteRefund(Long recordId, MemberCardRecord record) {
+ List steps = new ArrayList<>();
+ List rollbackSteps = new ArrayList<>();
+
+ SagaStep step1 = new SagaStep(
+ "更新会员卡状态为已退款",
+ updateCardStatus(recordId, MemberCardRecordStatus.REFUNDED),
+ Mono.defer(() -> updateCardStatus(recordId, record.getStatus()))
+ );
+ steps.add(step1);
+ rollbackSteps.add(0, step1);
+
+ SagaStep step2 = new SagaStep(
+ "记录退款流水",
+ createRefundTransaction(record),
+ createReversalTransaction(record)
+ );
+ steps.add(step2);
+ rollbackSteps.add(0, step2);
+
+ return executeSaga(steps, rollbackSteps);
+ }
+
+ private Mono updateCardStatus(Long recordId, MemberCardRecordStatus status) {
+ return recordRepository.updateStatus(recordId, status)
+ .flatMap(rows -> {
+ if (rows == 0) {
+ return Mono.error(new RuntimeException("更新会员卡状态失败"));
+ }
+ return Mono.empty();
+ });
+ }
+
+ private Mono createRefundTransaction(MemberCardRecord record) {
+ MemberCardTransactions transaction = new MemberCardTransactions();
+ transaction.setMemberId(record.getMemberId());
+ transaction.setMemberCardId(record.getMemberCardId());
+ transaction.setOperationType(MemberCardTransactionsAction.REFUND);
+ transaction.setChangeAmount(-record.getRemainingTimes());
+ transaction.setChangeBalance(-record.getRemainingAmount());
+ transaction.setAfterRemainingCount(0);
+ transaction.setAfterRemainingBalance(0.0);
+ transaction.setRemark("会员卡退款");
+
+ return transactionsService.createTransaction(transaction);
+ }
+
+ private Mono createReversalTransaction(MemberCardRecord record) {
+ MemberCardTransactions reversal = new MemberCardTransactions();
+ reversal.setMemberId(record.getMemberId());
+ reversal.setMemberCardId(record.getMemberCardId());
+ reversal.setOperationType(MemberCardTransactionsAction.REFUND);
+ reversal.setChangeAmount(record.getRemainingTimes());
+ reversal.setChangeBalance(record.getRemainingAmount());
+ reversal.setRemark("退款冲正");
+
+ return transactionsService.createTransaction(reversal);
+ }
+
+ private Mono executeSaga(List steps, List rollbackSteps) {
+ return executeStep(steps, 0, rollbackSteps);
+ }
+
+ private Mono executeStep(List steps, int index, List rollbackSteps) {
+ if (index >= steps.size()) {
+ return Mono.empty();
+ }
+
+ SagaStep currentStep = steps.get(index);
+
+ return currentStep.operation()
+ .then(Mono.defer(() -> executeStep(steps, index + 1, rollbackSteps)))
+ .onErrorResume(error -> {
+ log.error("Saga步骤执行失败: step={}, error={}", currentStep.description(), error.getMessage());
+ return rollback(rollbackSteps, 0).then(Mono.error(error));
+ });
+ }
+
+ private Mono rollback(List rollbackSteps, int index) {
+ if (index >= rollbackSteps.size()) {
+ return Mono.empty();
+ }
+
+ SagaStep currentStep = rollbackSteps.get(index);
+
+ return currentStep.rollbackOperation()
+ .then(Mono.defer(() -> rollback(rollbackSteps, index + 1)))
+ .doOnError(error -> log.error("Saga回滚失败: step={}, error={}", currentStep.description(), error.getMessage()))
+ .onErrorResume(e -> Mono.empty());
+ }
+
+ private record SagaStep(String description, Mono operation, Mono rollbackOperation) {}
+}
\ No newline at end of file
diff --git a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/repository/IMemberCardRecordRepository.java b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/repository/IMemberCardRecordRepository.java
index 237b440..6cafcde 100644
--- a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/repository/IMemberCardRecordRepository.java
+++ b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/repository/IMemberCardRecordRepository.java
@@ -6,78 +6,29 @@ import org.springframework.data.domain.Pageable;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
-import java.time.LocalDateTime;
-
public interface IMemberCardRecordRepository {
-
- /**
- * 会员购卡/后台发卡
- * @param record 会员卡记录
- * @return 插入的记录
- */
+
+ Mono findById(Long id);
+
+ Mono save(MemberCardRecord record);
+
Mono insertActiveRecord(MemberCardRecord record);
-
- /**
- * 扣次/扣费(含防超扣校验)
- * @param recordId 会员卡记录ID
- * @param deductTimes 扣除次数
- * @param deductAmount 扣除金额
- * @return 受影响的行数(0表示余额不足,扣费失败)
- */
- Mono deductUsage(Long recordId, Integer deductTimes, Double deductAmount);
-
- /**
- * 续费
- * @param recordId 会员卡记录ID
- * @param addTimes 增加次数
- * @param addAmount 增加金额
- * @param newExpireTime 新的到期时间
- * @return 受影响的行数
- */
- Mono renewCard(Long recordId, Integer addTimes, Double addAmount, LocalDateTime newExpireTime);
-
- /**
- * 状态变更
- * @param recordId 会员卡记录ID
- * @param status 新状态
- * @return 受影响的行数
- */
- Mono updateStatus(Long recordId, MemberCardRecordStatus status);
-
- /**
- * 会员端"我的卡包"
- * @param memberId 会员ID
- * @return 有效会员卡列表
- */
- Flux findActiveCardsByMemberId(Long memberId);
-
- /**
- * 前台/店长查会员卡
- * @param memberId 会员ID
- * @param pageable 分页参数
- * @return 会员卡列表
- */
+
Flux findByMemberId(Long memberId, Pageable pageable);
-
- /**
- * 验证次卡是否可用(仅检验次数和过期时间)
- * @param recordId 会员卡记录ID
- * @param requiredTimes 需要的次数
- * @return 符合条件的记录,空表示不可用
- */
- Mono validateCountCard(Long recordId, Integer requiredTimes);
-
- /**
- * 验证储值卡是否可用(仅检验余额和过期时间)
- * @param recordId 会员卡记录ID
- * @param requiredAmount 需要的金额
- * @return 符合条件的记录,空表示不可用
- */
- Mono validateStoredCard(Long recordId, Double requiredAmount);
-
- /**
- * 到期扫描(分批处理,避免内存压力)
- * @return 已过期的会员卡记录列表(最多500条)
- */
+
+ Flux findActiveCardsByMemberId(Long memberId);
+
+ Flux findActiveRecords();
+
+ Mono updateStatus(Long id, MemberCardRecordStatus status);
+
+ Mono deductUsage(Long recordId, Integer deductTimes, Double deductAmount);
+
+ Mono renewCard(Long recordId, Integer addTimes, Double addAmount, java.time.LocalDateTime newExpireTime);
+
Flux findExpiredCards();
-}
+
+ Mono validateCountCard(Long recordId, Integer requiredTimes);
+
+ Mono validateStoredCard(Long recordId, Double requiredAmount);
+}
\ No newline at end of file
diff --git a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/repository/IMemberCardTransactionsRepository.java b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/repository/IMemberCardTransactionsRepository.java
index 010556a..46ff531 100644
--- a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/repository/IMemberCardTransactionsRepository.java
+++ b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/repository/IMemberCardTransactionsRepository.java
@@ -88,4 +88,25 @@ public interface IMemberCardTransactionsRepository {
* @return 购卡总金额
*/
Mono sumPurchaseAmountByMemberId(Long memberId, LocalDateTime startTime, LocalDateTime endTime);
+
+ /**
+ * 保存流水记录
+ * @param transaction 流水记录
+ * @return 保存后的流水记录
+ */
+ Mono save(MemberCardTransactions transaction);
+
+ /**
+ * 按会员ID查询所有流水记录
+ * @param memberId 会员ID
+ * @return 该会员的所有流水记录,按时间倒序
+ */
+ Flux findByMemberId(Long memberId);
+
+ /**
+ * 按流水ID查询流水记录
+ * @param recordId 流水ID
+ * @return 该流水记录
+ */
+ Flux findByRecordId(Long recordId);
}
diff --git a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/repository/IRefundApplicationRepository.java b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/repository/IRefundApplicationRepository.java
new file mode 100644
index 0000000..931b1fc
--- /dev/null
+++ b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/repository/IRefundApplicationRepository.java
@@ -0,0 +1,81 @@
+package cn.novalon.gym.manage.gymmembercard.repository;
+
+import cn.novalon.gym.manage.gymmembercard.domain.RefundApplication;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+/**
+ * 退款申请仓储接口
+ *
+ * @author shizhounian
+ * @date 2026-05-23
+ */
+public interface IRefundApplicationRepository {
+
+ /**
+ * 创建退款申请
+ *
+ * @param application 退款申请对象
+ * @return 创建后的退款申请
+ */
+ Mono create(RefundApplication application);
+
+ /**
+ * 根据ID查询退款申请
+ *
+ * @param id 退款申请ID
+ * @return 退款申请对象
+ */
+ Mono findById(Long id);
+
+ /**
+ * 根据会员卡记录ID查询退款申请
+ *
+ * @param recordId 会员卡记录ID
+ * @return 退款申请对象
+ */
+ Mono findByRecordId(Long recordId);
+
+ /**
+ * 根据会员ID查询退款申请列表
+ *
+ * @param memberId 会员ID
+ * @return 退款申请列表
+ */
+ Flux findByMemberId(Long memberId);
+
+ /**
+ * 根据状态查询退款申请列表
+ *
+ * @param status 状态
+ * @return 退款申请列表
+ */
+ Flux findByStatus(String status);
+
+ /**
+ * 更新退款申请
+ *
+ * @param application 退款申请对象
+ * @return 更新后的退款申请
+ */
+ Mono update(RefundApplication application);
+
+ /**
+ * 审核退款申请
+ *
+ * @param id 退款申请ID
+ * @param status 审核状态(APPROVED/REJECTED)
+ * @param auditorId 审核人ID
+ * @param auditRemark 审核备注
+ * @return 更新后的退款申请
+ */
+ Mono approve(Long id, String status, Long auditorId, String auditRemark);
+
+ /**
+ * 删除退款申请(逻辑删除)
+ *
+ * @param id 退款申请ID
+ * @return 受影响的行数
+ */
+ Mono delete(Long id);
+}
diff --git a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/repository/impl/MemberCardRecordRepositoryImpl.java b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/repository/impl/MemberCardRecordRepositoryImpl.java
index 9148112..30406e5 100644
--- a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/repository/impl/MemberCardRecordRepositoryImpl.java
+++ b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/repository/impl/MemberCardRecordRepositoryImpl.java
@@ -12,8 +12,6 @@ import org.springframework.stereotype.Repository;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
-import java.time.LocalDateTime;
-
@Repository
public class MemberCardRecordRepositoryImpl implements IMemberCardRecordRepository {
private final MemberCardRecordDao memberCardRecordDao;
@@ -28,14 +26,22 @@ public class MemberCardRecordRepositoryImpl implements IMemberCardRecordReposito
this.r2dbcEntityTemplate = r2dbcEntityTemplate;
}
- /**
- * 会员购卡/后台发卡
- * @param record 会员卡记录
- * @return 插入的记录
- */
+ @Override
+ public Mono findById(Long id) {
+ return memberCardRecordDao.findById(id)
+ .map(e -> beanConvertUtil.toBean(e, MemberCardRecord.class));
+ }
+
+ @Override
+ public Mono save(MemberCardRecord record) {
+ MemberCardRecordEntity entity = beanConvertUtil.toBean(record, MemberCardRecordEntity.class);
+ return memberCardRecordDao.save(entity)
+ .map(e -> beanConvertUtil.toBean(e, MemberCardRecord.class));
+ }
+
@Override
public Mono insertActiveRecord(MemberCardRecord record) {
- MemberCardRecordEntity entity = BeanConvertUtil.toBean(record, MemberCardRecordEntity.class);
+ MemberCardRecordEntity entity = beanConvertUtil.toBean(record, MemberCardRecordEntity.class);
return memberCardRecordDao.insertActiveRecord(
entity.getMemberId(),
entity.getMemberCardId(),
@@ -46,96 +52,54 @@ public class MemberCardRecordRepositoryImpl implements IMemberCardRecordReposito
.map(e -> beanConvertUtil.toBean(e, MemberCardRecord.class));
}
- /**
- * 扣次/扣费(含防超扣校验)
- * @param recordId 会员卡记录ID
- * @param deductTimes 扣除次数
- * @param deductAmount 扣除金额
- * @return 受影响的行数(0表示余额不足,扣费失败)
- */
- @Override
- public Mono deductUsage(Long recordId, Integer deductTimes, Double deductAmount) {
- return memberCardRecordDao.deductUsage(recordId, deductTimes, deductAmount);
- }
-
- /**
- * 续费
- * @param recordId 会员卡记录ID
- * @param addTimes 增加次数
- * @param addAmount 增加金额
- * @param newExpireTime 新的到期时间
- * @return 受影响的行数
- */
- @Override
- public Mono renewCard(Long recordId, Integer addTimes, Double addAmount, LocalDateTime newExpireTime) {
- return memberCardRecordDao.renewCard(recordId, addTimes, addAmount, newExpireTime);
- }
-
- /**
- * 状态变更
- * @param recordId 会员卡记录ID
- * @param status 新状态
- * @return 受影响的行数
- */
- @Override
- public Mono updateStatus(Long recordId, MemberCardRecordStatus status) {
- return memberCardRecordDao.updateStatus(recordId, status);
- }
-
- /**
- * 会员端"我的卡包"
- * @param memberId 会员ID
- * @return 有效会员卡列表
- */
- @Override
- public Flux findActiveCardsByMemberId(Long memberId) {
- return memberCardRecordDao.findActiveCardsByMemberId(memberId)
- .map(entity -> beanConvertUtil.toBean(entity, MemberCardRecord.class));
- }
-
- /**
- * 前台/店长查会员卡
- * @param memberId 会员ID
- * @param pageable 分页参数
- * @return 会员卡列表
- */
@Override
public Flux findByMemberId(Long memberId, Pageable pageable) {
return memberCardRecordDao.findByMemberId(memberId, pageable)
.map(entity -> beanConvertUtil.toBean(entity, MemberCardRecord.class));
}
- /**
- * 验证次卡是否可用(仅检验次数和过期时间)
- * @param recordId 会员卡记录ID
- * @param requiredTimes 需要的次数
- * @return 符合条件的记录,空表示不可用
- */
+ @Override
+ public Flux findActiveCardsByMemberId(Long memberId) {
+ return memberCardRecordDao.findActiveCardsByMemberId(memberId)
+ .map(entity -> beanConvertUtil.toBean(entity, MemberCardRecord.class));
+ }
+
+ @Override
+ public Flux findActiveRecords() {
+ return memberCardRecordDao.findActiveRecords()
+ .map(entity -> beanConvertUtil.toBean(entity, MemberCardRecord.class));
+ }
+
+ @Override
+ public Mono updateStatus(Long id, MemberCardRecordStatus status) {
+ return memberCardRecordDao.updateStatus(id, status);
+ }
+
+ @Override
+ public Mono deductUsage(Long recordId, Integer deductTimes, Double deductAmount) {
+ return memberCardRecordDao.deductUsage(recordId, deductTimes, deductAmount);
+ }
+
+ @Override
+ public Mono renewCard(Long recordId, Integer addTimes, Double addAmount, java.time.LocalDateTime newExpireTime) {
+ return memberCardRecordDao.renewCard(recordId, addTimes, addAmount, newExpireTime);
+ }
+
+ @Override
+ public Flux findExpiredCards() {
+ return memberCardRecordDao.findExpiredCards()
+ .map(e -> beanConvertUtil.toBean(e, MemberCardRecord.class));
+ }
+
@Override
public Mono validateCountCard(Long recordId, Integer requiredTimes) {
return memberCardRecordDao.validateCountCard(recordId, requiredTimes)
.map(entity -> beanConvertUtil.toBean(entity, MemberCardRecord.class));
}
- /**
- * 验证储值卡是否可用(仅检验余额和过期时间)
- * @param recordId 会员卡记录ID
- * @param requiredAmount 需要的金额
- * @return 符合条件的记录,空表示不可用
- */
@Override
public Mono validateStoredCard(Long recordId, Double requiredAmount) {
return memberCardRecordDao.validateStoredCard(recordId, requiredAmount)
.map(entity -> beanConvertUtil.toBean(entity, MemberCardRecord.class));
}
-
- /**
- * 到期扫描(分批处理,避免内存压力)
- * @return 已过期的会员卡记录列表(最多500条)
- */
- @Override
- public Flux findExpiredCards() {
- return memberCardRecordDao.findExpiredCards()
- .map(entity -> beanConvertUtil.toBean(entity, MemberCardRecord.class));
- }
-}
+}
\ No newline at end of file
diff --git a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/repository/impl/MemberCardTransactionsRepositoryImpl.java b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/repository/impl/MemberCardTransactionsRepositoryImpl.java
index 0704716..4afecbd 100644
--- a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/repository/impl/MemberCardTransactionsRepositoryImpl.java
+++ b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/repository/impl/MemberCardTransactionsRepositoryImpl.java
@@ -144,4 +144,27 @@ public class MemberCardTransactionsRepositoryImpl implements IMemberCardTransact
public Mono sumPurchaseAmountByMemberId(Long memberId, LocalDateTime startTime, LocalDateTime endTime) {
return memberCardTransactionsDao.sumPurchaseAmountByMemberId(memberId, startTime, endTime);
}
+ /**
+ * 保存流水记录
+ * @param transaction 流水记录
+ * @return 保存后的流水记录
+ */
+ @Override
+ public Mono save(MemberCardTransactions transaction) {
+ MemberCardTransactionsEntity entity = beanConvertUtil.toBean(transaction, MemberCardTransactionsEntity.class);
+ return memberCardTransactionsDao.save(entity)
+ .map(savedEntity -> beanConvertUtil.toBean(savedEntity, MemberCardTransactions.class));
+ }
+
+ @Override
+ public Flux findByMemberId(Long memberId) {
+ return memberCardTransactionsDao.findByMemberId(memberId)
+ .map(entity -> beanConvertUtil.toBean(entity, MemberCardTransactions.class));
+ }
+
+ @Override
+ public Flux findByRecordId(Long recordId) {
+ return memberCardTransactionsDao.findByRecordId(recordId)
+ .map(entity -> beanConvertUtil.toBean(entity, MemberCardTransactions.class));
+ }
}
diff --git a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/repository/impl/RefundApplicationRepositoryImpl.java b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/repository/impl/RefundApplicationRepositoryImpl.java
new file mode 100644
index 0000000..ca85d61
--- /dev/null
+++ b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/repository/impl/RefundApplicationRepositoryImpl.java
@@ -0,0 +1,125 @@
+package cn.novalon.gym.manage.gymmembercard.repository.impl;
+
+import cn.novalon.gym.manage.gymmembercard.dao.RefundApplicationDao;
+import cn.novalon.gym.manage.gymmembercard.domain.RefundApplication;
+import cn.novalon.gym.manage.gymmembercard.entity.RefundApplicationEntity;
+import cn.novalon.gym.manage.gymmembercard.repository.IRefundApplicationRepository;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Repository;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+import java.time.LocalDateTime;
+
+/**
+ * 退款申请仓储实现类
+ *
+ * @author shizhounian
+ * @date 2026-05-23 21:16:36
+ */
+@Repository
+@RequiredArgsConstructor
+public class RefundApplicationRepositoryImpl implements IRefundApplicationRepository {
+
+ private final RefundApplicationDao refundApplicationDao;
+
+ @Override
+ public Mono create(RefundApplication application) {
+ RefundApplicationEntity entity = new RefundApplicationEntity();
+ entity.setRecordId(application.getRecordId());
+ entity.setMemberId(application.getMemberId());
+ entity.setStatus(application.getStatus() != null ? application.getStatus() : "PENDING");
+ entity.setReason(application.getReason());
+ entity.setApplyTime(application.getApplyTime() != null ? application.getApplyTime() : LocalDateTime.now());
+ entity.setRefundAmount(application.getRefundAmount());
+
+ return refundApplicationDao.save(entity)
+ .map(this::convertToDomain);
+ }
+
+ @Override
+ public Mono findById(Long id) {
+ return refundApplicationDao.findById(id)
+ .map(this::convertToDomain);
+ }
+
+ @Override
+ public Mono findByRecordId(Long recordId) {
+ return refundApplicationDao.findByRecordId(recordId)
+ .map(this::convertToDomain);
+ }
+
+ @Override
+ public Flux findByMemberId(Long memberId) {
+ return refundApplicationDao.findByMemberId(memberId)
+ .map(this::convertToDomain);
+ }
+
+ @Override
+ public Flux findByStatus(String status) {
+ return refundApplicationDao.findByStatus(status)
+ .map(this::convertToDomain);
+ }
+
+ @Override
+ public Mono update(RefundApplication application) {
+ return refundApplicationDao.findById(application.getId())
+ .flatMap(entity -> {
+ if (application.getStatus() != null) {
+ entity.setStatus(application.getStatus());
+ }
+ if (application.getReason() != null) {
+ entity.setReason(application.getReason());
+ }
+ if (application.getAuditTime() != null) {
+ entity.setAuditTime(application.getAuditTime());
+ }
+ if (application.getAuditorId() != null) {
+ entity.setAuditorId(application.getAuditorId());
+ }
+ if (application.getAuditRemark() != null) {
+ entity.setAuditRemark(application.getAuditRemark());
+ }
+ if (application.getRefundAmount() != null) {
+ entity.setRefundAmount(application.getRefundAmount());
+ }
+ return refundApplicationDao.save(entity);
+ })
+ .map(this::convertToDomain);
+ }
+
+ @Override
+ public Mono approve(Long id, String status, Long auditorId, String auditRemark) {
+ return refundApplicationDao.approve(id, status, auditorId, auditRemark)
+ .flatMap(rows -> {
+ if (rows > 0) {
+ return refundApplicationDao.findById(id)
+ .map(this::convertToDomain);
+ }
+ return Mono.empty();
+ });
+ }
+
+ @Override
+ public Mono delete(Long id) {
+ return refundApplicationDao.logicalDelete(id);
+ }
+
+ /**
+ * Entity转Domain
+ */
+ private RefundApplication convertToDomain(RefundApplicationEntity entity) {
+ RefundApplication domain = new RefundApplication();
+ domain.setId(entity.getId());
+ domain.setRecordId(entity.getRecordId());
+ domain.setMemberId(entity.getMemberId());
+ domain.setStatus(entity.getStatus());
+ domain.setReason(entity.getReason());
+ domain.setApplyTime(entity.getApplyTime());
+ domain.setAuditTime(entity.getAuditTime());
+ domain.setAuditorId(entity.getAuditorId());
+ domain.setAuditRemark(entity.getAuditRemark());
+ domain.setRefundAmount(entity.getRefundAmount());
+ return domain;
+ }
+}
diff --git a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/sevice/IMemberCardRecordService.java b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/sevice/IMemberCardRecordService.java
index 5773df8..06e1cdd 100644
--- a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/sevice/IMemberCardRecordService.java
+++ b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/sevice/IMemberCardRecordService.java
@@ -1,83 +1,29 @@
package cn.novalon.gym.manage.gymmembercard.sevice;
import cn.novalon.gym.manage.gymmembercard.domain.MemberCardRecord;
-import cn.novalon.gym.manage.gymmembercard.enums.MemberCardRecordStatus;
import org.springframework.data.domain.Pageable;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
-import java.time.LocalDateTime;
-
public interface IMemberCardRecordService {
-
- /**
- * 会员购卡/后台发卡
- * @param record 会员卡记录
- * @return 插入的记录
- */
- Mono insertActiveRecord(MemberCardRecord record);
-
- /**
- * 扣次/扣费(含防超扣校验)
- * @param recordId 会员卡记录ID
- * @param deductTimes 扣除次数
- * @param deductAmount 扣除金额
- * @return 受影响的行数(0表示余额不足,扣费失败)
- */
- Mono deductUsage(Long recordId, Integer deductTimes, Double deductAmount);
-
- /**
- * 续费
- * @param recordId 会员卡记录ID
- * @param addTimes 增加次数
- * @param addAmount 增加金额
- * @param newExpireTime 新的到期时间
- * @return 受影响的行数
- */
- Mono renewCard(Long recordId, Integer addTimes, Double addAmount, LocalDateTime newExpireTime);
-
- /**
- * 状态变更
- * @param recordId 会员卡记录ID
- * @param status 新状态
- * @return 受影响的行数
- */
- Mono updateStatus(Long recordId, MemberCardRecordStatus status);
-
- /**
- * 会员端"我的卡包"
- * @param memberId 会员ID
- * @return 有效会员卡列表
- */
- Flux findActiveCardsByMemberId(Long memberId);
-
- /**
- * 前台/店长查会员卡
- * @param memberId 会员ID
- * @param pageable 分页参数
- * @return 会员卡列表
- */
+
+ Mono findById(Long id);
+
Flux findByMemberId(Long memberId, Pageable pageable);
-
- /**
- * 验证次卡是否可用(仅检验次数和过期时间)
- * @param recordId 会员卡记录ID
- * @param requiredTimes 需要的次数
- * @return 符合条件的记录,空表示不可用
- */
+
+ Flux findActiveCardsByMemberId(Long memberId);
+
+ Mono insertActiveRecord(MemberCardRecord record);
+
+ Mono deductUsage(Long recordId, Integer deductTimes, Double deductAmount);
+
+ Mono renewCard(Long recordId, Integer addTimes, Double addAmount, java.time.LocalDateTime newExpireTime);
+
+ Mono updateStatus(Long recordId, cn.novalon.gym.manage.gymmembercard.enums.MemberCardRecordStatus status);
+
Mono validateCountCard(Long recordId, Integer requiredTimes);
-
- /**
- * 验证储值卡是否可用(仅检验余额和过期时间)
- * @param recordId 会员卡记录ID
- * @param requiredAmount 需要的金额
- * @return 符合条件的记录,空表示不可用
- */
+
Mono validateStoredCard(Long recordId, Double requiredAmount);
-
- /**
- * 到期扫描(分批处理,避免内存压力)
- * @return 已过期的会员卡记录列表(最多500条)
- */
+
Flux findExpiredCards();
-}
+}
\ No newline at end of file
diff --git a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/sevice/IMemberCardService.java b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/sevice/IMemberCardService.java
index b1b744d..255dfe7 100644
--- a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/sevice/IMemberCardService.java
+++ b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/sevice/IMemberCardService.java
@@ -1,78 +1,38 @@
package cn.novalon.gym.manage.gymmembercard.sevice;
import cn.novalon.gym.manage.gymmembercard.domain.MemberCard;
+import cn.novalon.gym.manage.gymmembercard.domain.MemberCardRecord;
import org.springframework.data.domain.Pageable;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
public interface IMemberCardService {
- /**
- * 根据会员卡ID查询会员卡详情(用于编辑前回显或小程序端展示)
- * @param memberCardId 会员卡ID
- * @return 会员卡完整信息,如果不存在或已删除则返回空
- */
+
Mono findByMemberCardIdAndDeletedAtIsNull(Long memberCardId);
-
- /**
- * 条件查询会员卡列表(后台卡种列表展示,支持多条件组合+分页+排序)
- * @param status 会员卡状态(上架/下架)
- * @param name 会员卡名称(模糊查询)
- * @param type 会员卡类型
- * @param minPrice 最低价格
- * @param maxPrice 最高价格
- * @param pageable 分页和排序参数
- * @return 符合条件的会员卡列表
- */
+
Flux findWithConditions(Integer status, String name, String type,
Double minPrice, Double maxPrice, Pageable pageable);
-
- /**
- * 统计符合条件的会员卡总数(配合列表查询使用)
- * @param status 会员卡状态
- * @param name 会员卡名称(模糊查询)
- * @param type 会员卡类型
- * @param minPrice 最低价格
- * @param maxPrice 最高价格
- * @return 符合条件的会员卡数量
- */
+
Mono countWithConditions(Integer status, String name, String type,
Double minPrice, Double maxPrice);
-
- /**
- * 按状态查询会员卡列表(用于小程序端展示可购买卡列表,只展示上架的卡)
- * @param status 会员卡状态(通常传上架状态)
- * @param pageable 分页和排序参数
- * @return 符合条件的会员卡列表
- */
+
Flux findByMemberCardStatusAndDeletedAtIsNull(Integer status, Pageable pageable);
-
- /**
- * 检查会员卡是否已被购买(用于删除前的校验)
- * @param memberCardId 会员卡ID
- * @return 如果存在关联的会员记录则返回true,否则返回false
- */
+
Mono existsPurchasedRecord(Long memberCardId);
-
- /**
- * 逻辑删除会员卡(下架卡种,防止已购会员数据异常)
- * @param memberCardId 会员卡ID
- * @return 受影响的行数
- */
+
Mono logicalDelete(Long memberCardId);
-
- /**
- * 保存卡种信息(新增或更新)
- * - 新增:entity.memberCardId 为 null 时,插入新记录
- * - 更新:entity.memberCardId 不为 null 时,根据ID更新现有记录
- * @param entity 卡种信息
- * @return 保存后的实体对象
- */
- Mono save(MemberCard entity);
-
- /**
- * 批量查询上架的会员卡(用于小程序端展示)
- * @param status 上架状态值
- * @return 上架的会员卡列表
- */
+
Flux findActiveCards(Integer status);
-}
+
+ Mono save(MemberCard entity);
+
+ Mono purchaseCard(Long memberId, Long memberCardId, Long sourceOrderId);
+
+ Mono renewCard(Long recordId, Integer addTimes, Double addAmount, Integer addDays, Long sourceOrderId);
+
+ Mono useCard(Long recordId, Integer deductTimes, Double deductAmount);
+
+ Mono refundCard(Long recordId);
+
+ Mono processExpiredCards();
+}
\ No newline at end of file
diff --git a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/sevice/IMemberCardTransactionsService.java b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/sevice/IMemberCardTransactionsService.java
index 526bcb8..06b1bf2 100644
--- a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/sevice/IMemberCardTransactionsService.java
+++ b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/sevice/IMemberCardTransactionsService.java
@@ -26,7 +26,7 @@ public interface IMemberCardTransactionsService {
* @return 流水记录列表
*/
Flux findByMemberIdAndTimeRange(Long memberId, LocalDateTime startTime,
- LocalDateTime endTime, Pageable pageable);
+ LocalDateTime endTime, Pageable pageable);
/**
* 后台"使用记录查询"
@@ -39,9 +39,9 @@ public interface IMemberCardTransactionsService {
* @return 流水记录列表
*/
Flux findWithConditions(Long memberId, Long memberCardId,
- MemberCardTransactionsAction operationType,
- LocalDateTime startTime, LocalDateTime endTime,
- Pageable pageable);
+ MemberCardTransactionsAction operationType,
+ LocalDateTime startTime, LocalDateTime endTime,
+ Pageable pageable);
/**
* 统计符合条件的流水总数
@@ -88,4 +88,25 @@ public interface IMemberCardTransactionsService {
* @return 购卡总金额
*/
Mono sumPurchaseAmountByMemberId(Long memberId, LocalDateTime startTime, LocalDateTime endTime);
-}
+
+ /**
+ * 创建交易记录
+ * @param transaction 交易记录
+ * @return 创建的交易记录
+ */
+ Mono createTransaction(MemberCardTransactions transaction);
+
+ /**
+ * 查询会员的交易记录
+ * @param memberId 会员ID
+ * @return 交易记录列表
+ */
+ Flux findByMemberId(Long memberId);
+
+ /**
+ * 查询会员卡记录的交易历史
+ * @param recordId 会员卡记录ID
+ * @return 交易记录列表
+ */
+ Flux findByRecordId(Long recordId);
+}
\ No newline at end of file
diff --git a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/sevice/IRefundApplicationService.java b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/sevice/IRefundApplicationService.java
new file mode 100644
index 0000000..56fa066
--- /dev/null
+++ b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/sevice/IRefundApplicationService.java
@@ -0,0 +1,33 @@
+package cn.novalon.gym.manage.gymmembercard.sevice;
+
+import cn.novalon.gym.manage.gymmembercard.domain.RefundApplication;
+import reactor.core.publisher.Mono;
+
+/**
+ * 退款申请服务
+ *
+ * @author shizhounian
+ * @date 2026-05-23
+ */
+public interface IRefundApplicationService {
+
+ /**
+ * 创建退款申请
+ */
+ Mono create(Long recordId, String reason);
+
+ /**
+ * 审核退款申请
+ */
+ Mono approve(Long applicationId, Long auditorId, String remark);
+
+ /**
+ * 拒绝退款申请
+ */
+ Mono reject(Long applicationId, Long auditorId, String remark);
+
+ /**
+ * 根据记录ID查询申请
+ */
+ Mono findByRecordId(Long recordId);
+}
\ No newline at end of file
diff --git a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/sevice/impl/MemberCardRecordServiceImpl.java b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/sevice/impl/MemberCardRecordServiceImpl.java
index 95b8ebe..598c435 100644
--- a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/sevice/impl/MemberCardRecordServiceImpl.java
+++ b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/sevice/impl/MemberCardRecordServiceImpl.java
@@ -64,4 +64,9 @@ public class MemberCardRecordServiceImpl implements IMemberCardRecordService {
public Flux findExpiredCards() {
return memberCardRecordRepository.findExpiredCards();
}
+
+ @Override
+ public Mono findById(Long recordId) {
+ return memberCardRecordRepository.findById(recordId);
+ }
}
diff --git a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/sevice/impl/MemberCardServiceImpl.java b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/sevice/impl/MemberCardServiceImpl.java
index 0ec900c..379dded 100644
--- a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/sevice/impl/MemberCardServiceImpl.java
+++ b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/sevice/impl/MemberCardServiceImpl.java
@@ -1,20 +1,53 @@
package cn.novalon.gym.manage.gymmembercard.sevice.impl;
import cn.novalon.gym.manage.gymmembercard.domain.MemberCard;
+import cn.novalon.gym.manage.gymmembercard.domain.MemberCardRecord;
+import cn.novalon.gym.manage.gymmembercard.domain.MemberCardTransactions;
+import cn.novalon.gym.manage.gymmembercard.enums.MemberCardEvent;
+import cn.novalon.gym.manage.gymmembercard.enums.MemberCardRecordStatus;
+import cn.novalon.gym.manage.gymmembercard.enums.MemberCardTransactionsAction;
+import cn.novalon.gym.manage.gymmembercard.enums.MemberCardType;
+import cn.novalon.gym.manage.gymmembercard.handler.DistributedLockService;
+import cn.novalon.gym.manage.gymmembercard.handler.ExpirationReminderService;
+import cn.novalon.gym.manage.gymmembercard.handler.MemberCardStateMachine;
+import cn.novalon.gym.manage.gymmembercard.handler.RefundSagaHandler;
+import cn.novalon.gym.manage.gymmembercard.repository.IMemberCardRecordRepository;
import cn.novalon.gym.manage.gymmembercard.repository.IMemberCardRepository;
-import cn.novalon.gym.manage.gymmembercard.repository.impl.MemberCardRepositoryImpl;
import cn.novalon.gym.manage.gymmembercard.sevice.IMemberCardService;
+import cn.novalon.gym.manage.gymmembercard.sevice.IMemberCardTransactionsService;
+import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
+import java.time.LocalDateTime;
+
+@Slf4j
@Service
public class MemberCardServiceImpl implements IMemberCardService {
private final IMemberCardRepository memberCardRepository;
+ private final IMemberCardRecordRepository recordRepository;
+ private final IMemberCardTransactionsService transactionsService;
+ private final MemberCardStateMachine stateMachine;
+ private final DistributedLockService distributedLockService;
+ private final ExpirationReminderService expirationReminderService;
+ private final RefundSagaHandler refundSagaHandler;
- public MemberCardServiceImpl(IMemberCardRepository memberCardRepository) {
+ public MemberCardServiceImpl(IMemberCardRepository memberCardRepository,
+ IMemberCardRecordRepository recordRepository,
+ IMemberCardTransactionsService transactionsService,
+ MemberCardStateMachine stateMachine,
+ DistributedLockService distributedLockService,
+ ExpirationReminderService expirationReminderService,
+ RefundSagaHandler refundSagaHandler) {
this.memberCardRepository = memberCardRepository;
+ this.recordRepository = recordRepository;
+ this.transactionsService = transactionsService;
+ this.stateMachine = stateMachine;
+ this.distributedLockService = distributedLockService;
+ this.expirationReminderService = expirationReminderService;
+ this.refundSagaHandler = refundSagaHandler;
}
@Override
@@ -49,8 +82,9 @@ public class MemberCardServiceImpl implements IMemberCardService {
return memberCardRepository.logicalDelete(memberCardId);
}
- public Mono updateSafe(Long memberCardId, MemberCard updateData) {
- return ((MemberCardRepositoryImpl) memberCardRepository).updateSafe(memberCardId, updateData);
+ @Override
+ public Flux findActiveCards(Integer status) {
+ return memberCardRepository.findActiveCards(status);
}
@Override
@@ -59,7 +93,225 @@ public class MemberCardServiceImpl implements IMemberCardService {
}
@Override
- public Flux findActiveCards(Integer status) {
- return memberCardRepository.findActiveCards(status);
+ public Mono purchaseCard(Long memberId, Long memberCardId, Long sourceOrderId) {
+ return memberCardRepository.findByMemberCardIdAndDeletedAtIsNull(memberCardId)
+ .switchIfEmpty(Mono.error(new RuntimeException("会员卡类型不存在")))
+ .flatMap(card -> {
+ if (card.getMemberCardStatus() != null && card.getMemberCardStatus() == 1) {
+ return Mono.error(new RuntimeException("该会员卡已禁用"));
+ }
+
+ MemberCardType cardType = MemberCardType.valueOf(card.getMemberCardType());
+
+ return distributedLockService.executeWithLock(
+ memberId.toString(),
+ cardType.name(),
+ Mono.defer(() -> createCardRecord(memberId, memberCardId, sourceOrderId, card))
+ );
+ })
+ .flatMap(record -> createTransaction(record, MemberCardTransactionsAction.PURCHASE, "购买会员卡")
+ .thenReturn(record))
+ .flatMap(record -> expirationReminderService.scheduleExpirationReminder(record)
+ .then(Mono.just(record)));
}
-}
+
+ private Mono createCardRecord(Long memberId, Long memberCardId,
+ Long sourceOrderId, MemberCard card) {
+ return Mono.defer(() -> {
+ MemberCardRecord record = new MemberCardRecord();
+ record.setMemberId(memberId);
+ record.setMemberCardId(memberCardId);
+ record.setSourceOrderId(sourceOrderId);
+ record.setPurchaseTime(LocalDateTime.now());
+ record.setStatus(MemberCardRecordStatus.ACTIVE);
+
+ MemberCardType cardType = MemberCardType.valueOf(card.getMemberCardType());
+ LocalDateTime now = LocalDateTime.now();
+
+ switch (cardType) {
+ case TIME_CARD:
+ record.setExpireTime(now.plusDays(card.getMemberCardValidityDays()));
+ record.setRemainingTimes(0);
+ record.setRemainingAmount(0.0);
+ break;
+ case COUNT_CARD:
+ record.setExpireTime(now.plusDays(card.getMemberCardValidityDays()));
+ record.setRemainingTimes(card.getMemberCardTotalTimes());
+ record.setRemainingAmount(0.0);
+ break;
+ case STORED_VALUE_CARD:
+ record.setExpireTime(now.plusYears(1));
+ record.setRemainingTimes(0);
+ record.setRemainingAmount(card.getMemberCardAmount());
+ break;
+ default:
+ return Mono.error(new RuntimeException("不支持的会员卡类型"));
+ }
+
+ return recordRepository.insertActiveRecord(record);
+ });
+ }
+
+ @Override
+ public Mono renewCard(Long recordId, Integer addTimes, Double addAmount,
+ Integer addDays, Long sourceOrderId) {
+ return recordRepository.findById(recordId)
+ .switchIfEmpty(Mono.error(new RuntimeException("会员卡记录不存在")))
+ .flatMap(originalRecord -> stateMachine.validateTransition(originalRecord, MemberCardEvent.RENEW)
+ .then(Mono.just(originalRecord)))
+ .flatMap(originalRecord -> memberCardRepository.findByMemberCardIdAndDeletedAtIsNull(originalRecord.getMemberCardId())
+ .switchIfEmpty(Mono.error(new RuntimeException("会员卡类型不存在")))
+ .flatMap(card -> {
+ MemberCardType cardType = MemberCardType.valueOf(card.getMemberCardType());
+
+ return distributedLockService.executeWithLock(
+ originalRecord.getMemberId().toString(),
+ cardType.name(),
+ Mono.defer(() -> doRenewCard(originalRecord, card, addTimes, addAmount, addDays))
+ );
+ }));
+ }
+
+ private Mono doRenewCard(MemberCardRecord record, MemberCard card,
+ Integer addTimes, Double addAmount, Integer addDays) {
+ LocalDateTime now = LocalDateTime.now();
+ MemberCardType cardType = MemberCardType.valueOf(card.getMemberCardType());
+
+ switch (cardType) {
+ case TIME_CARD:
+ LocalDateTime currentExpire = record.getExpireTime();
+ LocalDateTime baseTime = (currentExpire != null && currentExpire.isAfter(now)) ? currentExpire : now;
+ int daysToAdd = addDays != null ? addDays : card.getMemberCardValidityDays();
+ record.setExpireTime(baseTime.plusDays(daysToAdd));
+ break;
+ case COUNT_CARD:
+ int currentTimes = record.getRemainingTimes() != null ? record.getRemainingTimes() : 0;
+ int timesToAdd = addTimes != null ? addTimes : card.getMemberCardTotalTimes();
+ record.setRemainingTimes(currentTimes + timesToAdd);
+ if (record.getStatus() == MemberCardRecordStatus.USED_UP) {
+ record.setStatus(MemberCardRecordStatus.ACTIVE);
+ }
+ break;
+ case STORED_VALUE_CARD:
+ double currentAmount = record.getRemainingAmount() != null ? record.getRemainingAmount() : 0.0;
+ double amountToAdd = addAmount != null ? addAmount : card.getMemberCardAmount();
+ record.setRemainingAmount(currentAmount + amountToAdd);
+ if (record.getStatus() == MemberCardRecordStatus.USED_UP) {
+ record.setStatus(MemberCardRecordStatus.ACTIVE);
+ }
+ break;
+ default:
+ return Mono.error(new RuntimeException("不支持的会员卡类型"));
+ }
+
+ return recordRepository.save(record)
+ .flatMap(updatedRecord -> createTransaction(updatedRecord, MemberCardTransactionsAction.RENEW, "续费会员卡")
+ .then(expirationReminderService.scheduleExpirationReminder(updatedRecord))
+ .thenReturn(updatedRecord));
+ }
+
+ @Override
+ public Mono useCard(Long recordId, Integer deductTimes, Double deductAmount) {
+ return recordRepository.findById(recordId)
+ .switchIfEmpty(Mono.error(new RuntimeException("会员卡记录不存在")))
+ .flatMap(record -> stateMachine.validateTransition(record, MemberCardEvent.USE)
+ .then(Mono.just(record)))
+ .flatMap(record -> memberCardRepository.findByMemberCardIdAndDeletedAtIsNull(record.getMemberCardId())
+ .switchIfEmpty(Mono.error(new RuntimeException("会员卡类型不存在")))
+ .flatMap(card -> {
+ MemberCardType cardType = MemberCardType.valueOf(card.getMemberCardType());
+
+ return distributedLockService.executeWithLock(
+ record.getMemberId().toString(),
+ cardType.name(),
+ Mono.defer(() -> doUseCard(record, card, deductTimes, deductAmount))
+ );
+ }));
+ }
+
+ private Mono doUseCard(MemberCardRecord record, MemberCard card,
+ Integer deductTimes, Double deductAmount) {
+ if (record.getStatus() != MemberCardRecordStatus.ACTIVE) {
+ return Mono.error(new RuntimeException("会员卡状态不正确"));
+ }
+
+ MemberCardType cardType = MemberCardType.valueOf(card.getMemberCardType());
+ LocalDateTime now = LocalDateTime.now();
+
+ switch (cardType) {
+ case TIME_CARD:
+ if (record.getExpireTime() != null && record.getExpireTime().isBefore(now)) {
+ return Mono.error(new RuntimeException("会员卡已过期"));
+ }
+ break;
+ case COUNT_CARD:
+ int currentTimes = record.getRemainingTimes() != null ? record.getRemainingTimes() : 0;
+ int timesToDeduct = deductTimes != null ? deductTimes : 1;
+ if (currentTimes < timesToDeduct) {
+ return Mono.error(new RuntimeException("剩余次数不足"));
+ }
+ record.setRemainingTimes(currentTimes - timesToDeduct);
+ if (record.getRemainingTimes() == 0) {
+ record.setStatus(MemberCardRecordStatus.USED_UP);
+ }
+ break;
+ case STORED_VALUE_CARD:
+ double currentAmount = record.getRemainingAmount() != null ? record.getRemainingAmount() : 0.0;
+ double amountToDeduct = deductAmount != null ? deductAmount : 0.0;
+ if (currentAmount < amountToDeduct) {
+ return Mono.error(new RuntimeException("余额不足"));
+ }
+ record.setRemainingAmount(currentAmount - amountToDeduct);
+ if (record.getRemainingAmount() == 0) {
+ record.setStatus(MemberCardRecordStatus.USED_UP);
+ }
+ break;
+ default:
+ return Mono.error(new RuntimeException("不支持的会员卡类型"));
+ }
+
+ return recordRepository.save(record)
+ .flatMap(updatedRecord -> createTransaction(updatedRecord, MemberCardTransactionsAction.DEDUCT, "使用会员卡")
+ .thenReturn(updatedRecord));
+ }
+
+ @Override
+ public Mono refundCard(Long recordId) {
+ return recordRepository.findById(recordId)
+ .switchIfEmpty(Mono.error(new RuntimeException("会员卡记录不存在")))
+ .flatMap(record -> memberCardRepository.findByMemberCardIdAndDeletedAtIsNull(record.getMemberCardId())
+ .switchIfEmpty(Mono.error(new RuntimeException("会员卡类型不存在")))
+ .flatMap(card -> {
+ MemberCardType cardType = MemberCardType.valueOf(card.getMemberCardType());
+ return distributedLockService.executeWithLock(
+ record.getMemberId().toString(),
+ cardType.name(),
+ refundSagaHandler.executeRefund(recordId)
+ );
+ }));
+ }
+
+ @Override
+ public Mono processExpiredCards() {
+ return recordRepository.findExpiredCards()
+ .flatMap(record -> stateMachine.transition(record.getStatus(), MemberCardEvent.EXPIRE)
+ .flatMap(newState -> recordRepository.updateStatus(
+ record.getMemberCardRecordId(), newState)))
+ .reduce(0, Integer::sum)
+ .doOnSuccess(count -> log.info("处理过期会员卡完成,共处理{}条", count));
+ }
+
+ private Mono createTransaction(MemberCardRecord record, MemberCardTransactionsAction action, String remark) {
+ MemberCardTransactions transaction = new MemberCardTransactions();
+ transaction.setMemberId(record.getMemberId());
+ transaction.setMemberCardId(record.getMemberCardId());
+ transaction.setOperationType(action);
+ transaction.setChangeAmount(record.getRemainingTimes());
+ transaction.setChangeBalance(record.getRemainingAmount());
+ transaction.setAfterRemainingCount(record.getRemainingTimes());
+ transaction.setAfterRemainingBalance(record.getRemainingAmount());
+ transaction.setRemark(remark);
+
+ return transactionsService.createTransaction(transaction);
+ }
+}
\ No newline at end of file
diff --git a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/sevice/impl/MemberCardTransactionsServiceImpl.java b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/sevice/impl/MemberCardTransactionsServiceImpl.java
index eddccea..119c4bf 100644
--- a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/sevice/impl/MemberCardTransactionsServiceImpl.java
+++ b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/sevice/impl/MemberCardTransactionsServiceImpl.java
@@ -4,6 +4,7 @@ import cn.novalon.gym.manage.gymmembercard.domain.MemberCardTransactions;
import cn.novalon.gym.manage.gymmembercard.enums.MemberCardTransactionsAction;
import cn.novalon.gym.manage.gymmembercard.repository.IMemberCardTransactionsRepository;
import cn.novalon.gym.manage.gymmembercard.sevice.IMemberCardTransactionsService;
+import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux;
@@ -11,6 +12,7 @@ import reactor.core.publisher.Mono;
import java.time.LocalDateTime;
+@Slf4j
@Service
public class MemberCardTransactionsServiceImpl implements IMemberCardTransactionsService {
private final IMemberCardTransactionsRepository memberCardTransactionsRepository;
@@ -66,4 +68,22 @@ public class MemberCardTransactionsServiceImpl implements IMemberCardTransaction
public Mono sumPurchaseAmountByMemberId(Long memberId, LocalDateTime startTime, LocalDateTime endTime) {
return memberCardTransactionsRepository.sumPurchaseAmountByMemberId(memberId, startTime, endTime);
}
+
+ @Override
+ public Mono createTransaction(MemberCardTransactions transaction) {
+ return memberCardTransactionsRepository.save(transaction)
+ .then()
+ .doOnSuccess(v -> log.info("创建会员卡交易记录: memberId={}, cardId={}, type={}",
+ transaction.getMemberId(), transaction.getMemberCardId(), transaction.getOperationType()));
+ }
+
+ @Override
+ public Flux findByMemberId(Long memberId) {
+ return memberCardTransactionsRepository.findByMemberId(memberId);
+ }
+
+ @Override
+ public Flux findByRecordId(Long recordId) {
+ return memberCardTransactionsRepository.findByRecordId(recordId);
+ }
}
diff --git a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/sevice/impl/RefundApplicationServiceImpl.java b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/sevice/impl/RefundApplicationServiceImpl.java
new file mode 100644
index 0000000..4c29ac7
--- /dev/null
+++ b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/sevice/impl/RefundApplicationServiceImpl.java
@@ -0,0 +1,85 @@
+package cn.novalon.gym.manage.gymmembercard.sevice.impl;
+
+import cn.novalon.gym.manage.gymmembercard.domain.RefundApplication;
+import cn.novalon.gym.manage.gymmembercard.repository.IRefundApplicationRepository;
+import cn.novalon.gym.manage.gymmembercard.sevice.IRefundApplicationService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+import java.time.LocalDateTime;
+
+/**
+ * 退款申请服务实现类
+ *
+ * @author shizhounian
+ * @date 2026-05-23 21:18:33
+ */
+@Slf4j
+@Service
+public class RefundApplicationServiceImpl implements IRefundApplicationService {
+
+ private final IRefundApplicationRepository refundApplicationRepository;
+
+ public RefundApplicationServiceImpl(IRefundApplicationRepository refundApplicationRepository) {
+ this.refundApplicationRepository = refundApplicationRepository;
+ }
+
+ @Override
+ public Mono create(Long recordId, String reason) {
+ return refundApplicationRepository.findByRecordId(recordId)
+ .flatMap(existing -> {
+ if (existing != null) {
+ return Mono.error(new RuntimeException("该会员卡记录已有退款申请"));
+ }
+ return Mono.empty();
+ })
+ .then(Mono.defer(() -> {
+ RefundApplication application = new RefundApplication();
+ application.setRecordId(recordId);
+ application.setReason(reason);
+ application.setStatus("PENDING");
+ application.setApplyTime(LocalDateTime.now());
+
+ return refundApplicationRepository.create(application)
+ .doOnSuccess(app -> log.info("创建退款申请成功: applicationId={}, recordId={}",
+ app.getId(), recordId));
+ }));
+ }
+
+ @Override
+ public Mono approve(Long applicationId, Long auditorId, String remark) {
+ return refundApplicationRepository.findById(applicationId)
+ .switchIfEmpty(Mono.error(new RuntimeException("退款申请不存在")))
+ .flatMap(application -> {
+ if (!"PENDING".equals(application.getStatus())) {
+ return Mono.error(new RuntimeException("退款申请状态不正确,当前状态: " + application.getStatus()));
+ }
+
+ return refundApplicationRepository.approve(applicationId, "APPROVED", auditorId, remark)
+ .doOnSuccess(app -> log.info("批准退款申请成功: applicationId={}, auditorId={}",
+ applicationId, auditorId));
+ });
+ }
+
+ @Override
+ public Mono reject(Long applicationId, Long auditorId, String remark) {
+ return refundApplicationRepository.findById(applicationId)
+ .switchIfEmpty(Mono.error(new RuntimeException("退款申请不存在")))
+ .flatMap(application -> {
+ if (!"PENDING".equals(application.getStatus())) {
+ return Mono.error(new RuntimeException("退款申请状态不正确,当前状态: " + application.getStatus()));
+ }
+
+ return refundApplicationRepository.approve(applicationId, "REJECTED", auditorId, remark)
+ .doOnSuccess(app -> log.info("拒绝退款申请成功: applicationId={}, auditorId={}",
+ applicationId, auditorId));
+ });
+ }
+
+ @Override
+ public Mono findByRecordId(Long recordId) {
+ return refundApplicationRepository.findByRecordId(recordId);
+ }
+}
diff --git a/gym-manage-api/gym-member-card/src/main/resources/application.properties b/gym-manage-api/gym-member-card/src/main/resources/application.properties
deleted file mode 100644
index 81b7aa0..0000000
--- a/gym-manage-api/gym-member-card/src/main/resources/application.properties
+++ /dev/null
@@ -1 +0,0 @@
-spring.application.name=gym-member-card
diff --git a/gym-manage-api/gym-member-card/src/main/resources/sql b/gym-manage-api/gym-member-card/src/main/resources/sql
new file mode 100644
index 0000000..509e01f
--- /dev/null
+++ b/gym-manage-api/gym-member-card/src/main/resources/sql
@@ -0,0 +1,132 @@
+-- ============================================
+-- 会员卡类型表
+-- ============================================
+CREATE TABLE IF NOT EXISTS member_card (
+ member_card_id BIGSERIAL PRIMARY KEY,
+ member_card_name VARCHAR(100) NOT NULL,
+ member_card_type VARCHAR(20) NOT NULL,
+ member_card_price DECIMAL(10, 2) NOT NULL,
+ member_card_validity_days INTEGER,
+ member_card_total_times INTEGER,
+ member_card_amount DECIMAL(10, 2),
+ member_card_status INTEGER DEFAULT 1 NOT NULL,
+ extra_config JSONB DEFAULT '{}'::jsonb,
+ created_at TIMESTAMPTZ DEFAULT NOW(),
+ updated_at TIMESTAMPTZ DEFAULT NOW(),
+ deleted_at TIMESTAMPTZ
+);
+
+COMMENT ON TABLE member_card IS '会员卡类型表';
+COMMENT ON COLUMN member_card.member_card_id IS '会员卡ID';
+COMMENT ON COLUMN member_card.member_card_name IS '会员卡名称';
+COMMENT ON COLUMN member_card.member_card_type IS '会员卡类型:TIME_CARD-时长卡, COUNT_CARD-次卡, STORED_VALUE_CARD-储值卡';
+COMMENT ON COLUMN member_card.member_card_price IS '会员卡价格';
+COMMENT ON COLUMN member_card.member_card_validity_days IS '有效天数(时长卡用)';
+COMMENT ON COLUMN member_card.member_card_total_times IS '总次数(次卡用)';
+COMMENT ON COLUMN member_card.member_card_amount IS '面额(储值卡用)';
+COMMENT ON COLUMN member_card.member_card_status IS '状态:0-下架, 1-上架';
+COMMENT ON COLUMN member_card.extra_config IS '扩展配置(JSON格式,用于未来组合卡等)';
+
+-- ============================================
+-- 会员卡记录表(会员持有的卡)
+-- ============================================
+CREATE TABLE IF NOT EXISTS member_card_record (
+ member_card_record_id BIGSERIAL PRIMARY KEY,
+ member_id BIGINT NOT NULL,
+ member_card_id BIGINT NOT NULL,
+ status VARCHAR(20) NOT NULL DEFAULT 'ACTIVE',
+ remaining_times INTEGER DEFAULT 0,
+ remaining_amount DECIMAL(10, 2) DEFAULT 0.00,
+ expire_time TIMESTAMPTZ,
+ source_order_id BIGINT,
+ purchase_time TIMESTAMPTZ DEFAULT NOW(),
+ version INTEGER DEFAULT 0 NOT NULL,
+ card_composition JSONB DEFAULT NULL,
+ created_at TIMESTAMPTZ DEFAULT NOW(),
+ updated_at TIMESTAMPTZ DEFAULT NOW(),
+ deleted_at TIMESTAMPTZ
+ -- 移除外键约束,改用应用层验证
+);
+
+-- 索引优化
+CREATE INDEX idx_member_card_record_member_id ON member_card_record(member_id);
+CREATE INDEX idx_member_card_record_status ON member_card_record(status);
+CREATE INDEX idx_member_card_record_expire_time ON member_card_record(expire_time);
+CREATE INDEX idx_member_card_record_member_status ON member_card_record(member_id, status);
+CREATE INDEX idx_member_card_record_status_expire ON member_card_record(status, expire_time)
+ WHERE status = 'ACTIVE';
+
+COMMENT ON TABLE member_card_record IS '会员卡记录表';
+COMMENT ON COLUMN member_card_record.member_card_record_id IS '会员卡记录ID';
+COMMENT ON COLUMN member_card_record.member_id IS '会员ID';
+COMMENT ON COLUMN member_card_record.member_card_id IS '会员卡类型ID';
+COMMENT ON COLUMN member_card_record.status IS '状态:ACTIVE-有效, USED_UP-用完, EXPIRED-过期, REFUNDED-已退款';
+COMMENT ON COLUMN member_card_record.remaining_times IS '剩余次数';
+COMMENT ON COLUMN member_card_record.remaining_amount IS '剩余金额';
+COMMENT ON COLUMN member_card_record.expire_time IS '到期时间';
+COMMENT ON COLUMN member_card_record.source_order_id IS '来源订单ID';
+COMMENT ON COLUMN member_card_record.purchase_time IS '购买时间';
+COMMENT ON COLUMN member_card_record.version IS '乐观锁版本号';
+COMMENT ON COLUMN member_card_record.card_composition IS '卡片组成(JSON格式,用于组合卡)';
+
+-- ============================================
+-- 会员卡交易流水表
+-- ============================================
+CREATE TABLE IF NOT EXISTS member_card_transactions (
+ id BIGSERIAL PRIMARY KEY,
+ member_card_record_id BIGINT NOT NULL,
+ member_id BIGINT NOT NULL,
+ member_card_id BIGINT NOT NULL,
+ operation_type VARCHAR(20) NOT NULL,
+ change_amount INTEGER DEFAULT 0,
+ change_balance DECIMAL(10, 2) DEFAULT 0.00,
+ after_remaining_count INTEGER DEFAULT 0,
+ after_remaining_balance DECIMAL(10, 2) DEFAULT 0.00,
+ related_biz_type VARCHAR(20),
+ source_order_id BIGINT,
+ remark VARCHAR(500),
+ is_archived BOOLEAN DEFAULT FALSE NOT NULL,
+ archived_at TIMESTAMPTZ,
+ created_at TIMESTAMPTZ DEFAULT NOW()
+ -- 移除外键约束,改用应用层验证
+);
+
+-- 退款申请表
+CREATE TABLE IF NOT EXISTS refund_application (
+ id BIGSERIAL PRIMARY KEY,
+ record_id BIGINT NOT NULL,
+ member_id BIGINT NOT NULL,
+ status VARCHAR(20) NOT NULL DEFAULT 'PENDING', -- PENDING/APPROVED/REJECTED/PROCESSING/SUCCESS/FAILED
+ reason VARCHAR(500),
+ apply_time TIMESTAMPTZ DEFAULT NOW(),
+ audit_time TIMESTAMPTZ,
+ auditor_id BIGINT,
+ audit_remark VARCHAR(500),
+ refund_amount DECIMAL(10, 2),
+ created_at TIMESTAMPTZ DEFAULT NOW(),
+ updated_at TIMESTAMPTZ DEFAULT NOW()
+);
+
+CREATE INDEX idx_refund_application_record_id ON refund_application(record_id);
+CREATE INDEX idx_refund_application_status ON refund_application(status);
+
+COMMENT ON TABLE refund_application IS '退款申请表';
+COMMENT ON COLUMN refund_application.status IS '状态:PENDING-待审核, APPROVED-已批准, REJECTED-已拒绝, PROCESSING-处理中, SUCCESS-成功, FAILED-失败';
+
+
+-- 索引优化
+CREATE INDEX idx_member_card_transactions_member_id ON member_card_transactions(member_id);
+CREATE INDEX idx_member_card_transactions_record_id ON member_card_transactions(member_card_record_id);
+CREATE INDEX idx_member_card_transactions_created_at ON member_card_transactions(created_at);
+CREATE INDEX idx_member_card_transactions_member_type_time
+ ON member_card_transactions(member_id, operation_type, created_at);
+
+COMMENT ON TABLE member_card_transactions IS '会员卡交易流水表';
+COMMENT ON COLUMN member_card_transactions.operation_type IS '操作类型:PURCHASE-购买, DEDUCT-扣次/扣费, RENEW-续费, REFUND-退款, EXPIRE-过期';
+COMMENT ON COLUMN member_card_transactions.change_amount IS '变动次数';
+COMMENT ON COLUMN member_card_transactions.change_balance IS '变动金额';
+COMMENT ON COLUMN member_card_transactions.after_remaining_count IS '变动后剩余次数';
+COMMENT ON COLUMN member_card_transactions.after_remaining_balance IS '变动后剩余金额';
+COMMENT ON COLUMN member_card_transactions.related_biz_type IS '关联业务类型:GROUP_CLASS-团课, PT_CLASS-私教, CHECK_IN-签到';
+COMMENT ON COLUMN member_card_transactions.is_archived IS '是否已归档';
+COMMENT ON COLUMN member_card_transactions.archived_at IS '归档时间';
diff --git a/gym-manage-api/manage-app/src/main/java/cn/novalon/gym/manage/app/config/SystemRouter.java b/gym-manage-api/manage-app/src/main/java/cn/novalon/gym/manage/app/config/SystemRouter.java
index bf571d8..114ffd4 100644
--- a/gym-manage-api/manage-app/src/main/java/cn/novalon/gym/manage/app/config/SystemRouter.java
+++ b/gym-manage-api/manage-app/src/main/java/cn/novalon/gym/manage/app/config/SystemRouter.java
@@ -31,7 +31,7 @@ import static org.springframework.web.reactive.function.server.RouterFunctions.r
* 文件定义:配置WebFlux函数式路由,将HTTP请求映射到对应的Handler方法
* 涉及业务:用户、角色、字典、菜单、公告、文件等所有RESTful API路由
* 算法:使用RouterFunctions.route()构建函数式路由规则
- *
+ *
* @author 张翔
* @date 2026-03-13
*/
@@ -58,11 +58,11 @@ public class SystemRouter {
MemberCardHandler memberCardHandler,
MemberCardRecordHandler memberCardRecordHandler,
MemberCardTransactionHandler memberCardTransactionHandler) {
-
+
return route()
// ========== 诊断路由 ==========
.GET("/api/diagnostic/password", passwordDiagnosticHandler::diagnose)
-
+
// ========== 字典路由 ==========
.GET("/api/dictionaries", dictionaryHandler::getAllDictionaries)
.GET("/api/dictionaries/{id}", dictionaryHandler::getDictionaryById)
@@ -71,7 +71,7 @@ public class SystemRouter {
.POST("/api/dictionaries", dictionaryHandler::createDictionary)
.PUT("/api/dictionaries/{id}", dictionaryHandler::updateDictionary)
.DELETE("/api/dictionaries/{id}", dictionaryHandler::deleteDictionary)
-
+
// ========== 用户路由 ==========
.GET("/api/users", userHandler::getAllUsers)
.GET("/api/users/page", userHandler::getUsersByPage)
@@ -90,7 +90,7 @@ public class SystemRouter {
.POST("/api/users/{id}/action/restore", userHandler::restoreUser)
.GET("/api/users/{id}/roles", userHandler::getUserRoles)
.POST("/api/users/{id}/roles", userHandler::assignRoles)
-
+
// ========== 菜单路由 ==========
.GET("/api/menus", menuHandler::getAllMenus)
.GET("/api/menus/tree", menuHandler::getMenuTree)
@@ -98,7 +98,7 @@ public class SystemRouter {
.POST("/api/menus", menuHandler::createMenu)
.PUT("/api/menus/{id}", menuHandler::updateMenu)
.DELETE("/api/menus/{id}", menuHandler::deleteMenu)
-
+
// ========== 角色路由 ==========
.GET("/api/roles", roleHandler::getAllRoles)
.GET("/api/roles/page", roleHandler::getRolesByPage)
@@ -112,7 +112,7 @@ public class SystemRouter {
.POST("/api/roles/{id}/restore", roleHandler::restoreRole)
.GET("/api/roles/{id}/permissions", permissionHandler::getPermissionsByRoleId)
.POST("/api/roles/{id}/permissions", permissionHandler::assignPermissionsToRole)
-
+
// ========== 配置路由 ==========
.GET("/api/config", configHandler::getAllConfigs)
.GET("/api/config/{id}", configHandler::getConfigById)
@@ -120,7 +120,7 @@ public class SystemRouter {
.POST("/api/config", configHandler::createConfig)
.PUT("/api/config/{id}", configHandler::updateConfig)
.DELETE("/api/config/{id}", configHandler::deleteConfig)
-
+
// ========== 日志路由 ==========
.GET("/api/logs/login", logHandler::getAllLoginLogs)
.GET("/api/logs/login/page", logHandler::getLoginLogsByPage)
@@ -140,15 +140,15 @@ public class SystemRouter {
.GET("/api/logs/operation/count", operationLogHandler::getOperationLogCount)
.GET("/api/logs/operation/{id}", operationLogHandler::getOperationLogById)
.POST("/api/logs/operation", operationLogHandler::createOperationLog)
-
+
// ========== 认证路由 ==========
.POST("/api/auth/login", authHandler::login)
.POST("/api/auth/register", authHandler::register)
.POST("/api/auth/logout", authHandler::logout)
-
+
// ========== 统计路由 ==========
.GET("/api/stats/overview", statsHandler::getOverview)
-
+
// ========== 数据字典路由 ==========
.GET("/api/dict/types", dictHandler::getAllDictTypes)
.GET("/api/dict/types/{id}", dictHandler::getDictTypeById)
@@ -162,7 +162,7 @@ public class SystemRouter {
.POST("/api/dict/data", dictHandler::createDictData)
.PUT("/api/dict/data/{id}", dictHandler::updateDictData)
.DELETE("/api/dict/data/{id}", dictHandler::deleteDictData)
-
+
// ========== 公告路由 ==========
.GET("/api/notices", noticeHandler::getAllNotices)
.GET("/api/notices/{id}", noticeHandler::getNoticeById)
@@ -170,7 +170,7 @@ public class SystemRouter {
.POST("/api/notices", noticeHandler::createNotice)
.PUT("/api/notices/{id}", noticeHandler::updateNotice)
.DELETE("/api/notices/{id}", noticeHandler::deleteNotice)
-
+
// ========== 消息路由 ==========
.GET("/api/messages/user/{userId}", messageHandler::getMessagesByUser)
.GET("/api/messages/user/{userId}/unread", messageHandler::getUnreadCount)
@@ -178,7 +178,7 @@ public class SystemRouter {
.POST("/api/messages", messageHandler::createMessage)
.PUT("/api/messages/{id}/read", messageHandler::markAsRead)
.DELETE("/api/messages/{id}", messageHandler::deleteMessage)
-
+
// ========== 文件路由 ==========
.GET("/api/files", fileHandler::getAllFiles)
.GET("/api/files/{id}", fileHandler::getFileById)
@@ -188,7 +188,7 @@ public class SystemRouter {
.GET("/api/files/{id}/preview", fileHandler::previewFile)
.GET("/api/files/preview/{fileName}", fileHandler::previewFileByName)
.DELETE("/api/files/{id}", fileHandler::deleteFile)
-
+
// ========== 权限路由 ==========
.GET("/api/permissions", permissionHandler::getAllPermissions)
.GET("/api/permissions/{id}", permissionHandler::getPermissionById)
@@ -199,52 +199,33 @@ public class SystemRouter {
.PUT("/api/permissions/{id}", permissionHandler::updatePermission)
.DELETE("/api/permissions/{id}", permissionHandler::deletePermission)
- // ========== 会员卡管理路由 ==========
- // 会员卡类型
- // 1. 获取所有会员卡类型
- .GET("/api/memberCard/active", memberCardHandler::getActiveCards)
- // 2. 获取会员卡详情
- .GET("/api/memberCard/{memberCardId}", memberCardHandler::getMemberCardById)
- // 3. 条件查询会员卡列表
- .GET("/api/memberCard", memberCardHandler::getMemberCardList)
- // 4. 新增/更新会员卡
- .POST("/api/memberCard", memberCardHandler::saveMemberCard)
- // 5. 逻辑删除会员卡
- .DELETE("/api/memberCard/{memberCardId}", memberCardHandler::deleteMemberCard)
- // 会员卡持卡
- // 1. 会员购卡/发卡
- .POST("/api/memberCardRecord", memberCardRecordHandler::insertActiveRecord)
- // 2. 会员端“我的卡包” - 按会员ID获取有效卡
- .GET("/api/memberCardRecord/member/{memberId}/active", memberCardRecordHandler::getMyCards)
- // 3. 管理端按会员ID分页查所有卡记录
- .GET("/api/memberCardRecord/member/{memberId}", memberCardRecordHandler::getMemberCardRecords)
- // 4. 到期扫描
- .GET("/api/memberCardRecord/expired", memberCardRecordHandler::getExpiredCards)
- // 5. 扣次/扣费
- .POST("/api/memberCardRecord/{id}/deduct", memberCardRecordHandler::deductUsage)
- // 6. 续费
- .POST("/api/memberCardRecord/{id}/renew", memberCardRecordHandler::renewCard)
- // 7. 状态变更(过期/退款)
- .PUT("/api/memberCardRecord/{id}/status", memberCardRecordHandler::updateStatus)
- // 8. 验证次卡
- .GET("/api/memberCardRecord/{id}/validate/count", memberCardRecordHandler::validateCountCard)
- // 9. 验证储值卡
- .GET("/api/memberCardRecord/{id}/validate/stored", memberCardRecordHandler::validateStoredCard)
- // 会员卡交易
- // 1. 插入流水记录
- .POST("/api/transactions", memberCardTransactionHandler::insertTransaction)
- // 2. 后台条件分页查询流水(带多个查询参数)
- .GET("/api/transactions", memberCardTransactionHandler::getTransactionsWithConditions)
- // 3. 按会员ID查询使用记录(分页 + 时间范围)
- .GET("/api/transactions/member/{memberId}", memberCardTransactionHandler::getMemberTransactions)
- // 4. 按卡ID查询所有流水记录
- .GET("/api/transactions/card/{cardId}", memberCardTransactionHandler::getTransactionsByCardId)
- // 5. 统计某卡种总扣次数
- .GET("/api/transactions/statistics/deduct/card/{cardId}", memberCardTransactionHandler::getDeductCountByCardId)
- // 6. 统计某时间段续费总金额
- .GET("/api/transactions/statistics/renew", memberCardTransactionHandler::getRenewAmountByTimeRange)
- // 7. 统计某会员购卡总金额
- .GET("/api/transactions/statistics/purchase/member/{memberId}", memberCardTransactionHandler::getPurchaseAmountByMember)
+ // ========================================
+ // ========== 会员卡管理路由 ==============
+ // ========================================
+
+ // ===== 会员卡类型管理 =====
+ .GET("/api/member-cards/active", memberCardHandler::getActiveCards)
+ .GET("/api/member-cards/{memberCardId}", memberCardHandler::getMemberCardById)
+ .POST("/api/member-cards", memberCardHandler::createMemberCard)
+
+ // ===== 会员卡记录管理(核心业务)=====
+ .POST("/api/member-card-records/purchase", memberCardRecordHandler::purchaseCard)
+ .POST("/api/member-card-records/{recordId}/renew", memberCardRecordHandler::renewCard)
+ .POST("/api/member-card-records/{recordId}/use", memberCardRecordHandler::useCard)
+ .POST("/api/member-card-records/{recordId}/refund", memberCardRecordHandler::refundCard)
+ .GET("/api/member-card-records/my-cards/{memberId}", memberCardRecordHandler::getMyCards)
+ .GET("/api/member-card-records/{recordId}", memberCardRecordHandler::getMemberCardRecordById)
+ .POST("/api/member-card-records/process-expired", memberCardRecordHandler::processExpiredCards)
+
+ // ===== 会员卡交易流水管理 =====
+ .POST("/api/member-card-transactions", memberCardTransactionHandler::insertTransaction)
+ .GET("/api/member-card-transactions", memberCardTransactionHandler::getTransactionsWithConditions)
+ .GET("/api/member-card-transactions/member/{memberId}", memberCardTransactionHandler::getMemberTransactions)
+ .GET("/api/member-card-transactions/card/{cardId}", memberCardTransactionHandler::getTransactionsByCardId)
+ .GET("/api/member-card-transactions/statistics/deduct/{cardId}", memberCardTransactionHandler::getDeductCountByCardId)
+ .GET("/api/member-card-transactions/statistics/renew", memberCardTransactionHandler::getRenewAmountByTimeRange)
+ .GET("/api/member-card-transactions/statistics/purchase/{memberId}", memberCardTransactionHandler::getPurchaseAmountByMember)
+
.build();
}
}