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(); } }