diff --git a/gym-manage-api/gym-member-card/pom.xml b/gym-manage-api/gym-member-card/pom.xml deleted file mode 100644 index 1801b55..0000000 --- a/gym-manage-api/gym-member-card/pom.xml +++ /dev/null @@ -1,112 +0,0 @@ - - - 4.0.0 - - org.springframework.boot - spring-boot-starter-parent - 3.5.13 - - - cn.novalon.gym.manage - gym-member-card - 0.0.1-SNAPSHOT - gym-member-card - gym-member-card - - - 21 - - - - - - org.springframework.boot - spring-boot-starter-webflux - - - - - org.postgresql - postgresql - runtime - - - - - cn.novalon.gym.manage - manage-db - 1.0.0 - compile - - - - - org.projectlombok - lombok - 1.18.36 - provided - - - - - cn.hutool - hutool-all - 5.8.22 - - - - - org.springframework - 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 - - - - - - - - - org.springframework.boot - spring-boot-maven-plugin - - - exec - - - - - \ No newline at end of file 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 deleted file mode 100644 index 00c02f6..0000000 --- a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/GymMemberCardApplication.java +++ /dev/null @@ -1,17 +0,0 @@ -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.gymmembercard.dao") -@EnableScheduling -public class GymMemberCardApplication { - - public static void main(String[] args) { - SpringApplication.run(GymMemberCardApplication.class, args); - } - -} diff --git a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/dao/MemberCardDao.java b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/dao/MemberCardDao.java deleted file mode 100644 index 8877d8d..0000000 --- a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/dao/MemberCardDao.java +++ /dev/null @@ -1,130 +0,0 @@ -package cn.novalon.gym.manage.gymmembercard.dao; - -import cn.novalon.gym.manage.gymmembercard.entity.MemberCardEntity; -import org.springframework.data.domain.Pageable; -import org.springframework.data.r2dbc.repository.Modifying; -import org.springframework.data.r2dbc.repository.Query; -import org.springframework.data.r2dbc.repository.R2dbcRepository; -import org.springframework.stereotype.Repository; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; - -@Repository -public interface MemberCardDao extends R2dbcRepository { - - /** - * 根据会员卡ID查询会员卡详情(用于编辑前回显或小程序端展示) - * @param memberCardId 会员卡ID - * @return 会员卡完整信息,如果不存在或已删除则返回空 - */ - Mono findByMemberCardIdAndDeletedAtIsNull(Long memberCardId); - - /** - * 条件查询会员卡列表(后台卡种列表展示,支持多条件组合+分页+排序) - * 注意:模糊查询使用前后缀通配符,若数据量较大可能影响索引效率,建议后期引入全文索引或改用后缀模糊 - * @param status 会员卡状态(上架/下架) - * @param name 会员卡名称(模糊查询) - * @param type 会员卡类型 - * @param minPrice 最低价格 - * @param maxPrice 最高价格 - * @param pageable 分页和排序参数 - * @return 符合条件的会员卡列表 - */ - @Query("SELECT * FROM member_card WHERE deleted_at IS NULL " + - "AND (:status IS NULL OR member_card_status = :status) " + - "AND (:name IS NULL OR member_card_name LIKE CONCAT('%', :name, '%')) " + - "AND (:type IS NULL OR member_card_type = :type) " + - "AND (:minPrice IS NULL OR member_card_price >= :minPrice) " + - "AND (:maxPrice IS NULL OR member_card_price <= :maxPrice) " + - "ORDER BY created_at DESC LIMIT :#{#pageable.pageSize} OFFSET :#{#pageable.offset}") - 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 符合条件的会员卡数量 - */ - @Query("SELECT COUNT(*) FROM member_card WHERE deleted_at IS NULL " + - "AND (:status IS NULL OR member_card_status = :status) " + - "AND (:name IS NULL OR member_card_name LIKE CONCAT('%', :name, '%')) " + - "AND (:type IS NULL OR member_card_type = :type) " + - "AND (:minPrice IS NULL OR member_card_price >= :minPrice) " + - "AND (:maxPrice IS NULL OR member_card_price <= :maxPrice)") - Mono countWithConditions(Integer status, String name, String type, - Double minPrice, Double maxPrice); - - /** - * 按状态查询会员卡列表(用于小程序端展示可购买卡列表,只展示上架的卡) - * @param status 会员卡状态(通常传上架状态) - * @param pageable 分页和排序参数 - * @return 符合条件的会员卡列表 - */ - Flux findByMemberCardStatusAndDeletedAtIsNull(Integer status, Pageable pageable); - - /** - * 检查会员卡是否已被购买(用于删除前的校验) - * 注意:此查询关联到 member_card_record 表,建议后续独立至 MemberCardRecordDao - * @param memberCardId 会员卡ID - * @return 如果存在关联的会员记录则返回true,否则返回false - */ - @Query("SELECT EXISTS(SELECT 1 FROM member_card_record WHERE member_card_id = :memberCardId AND deleted_at IS NULL LIMIT 1)") - Mono existsPurchasedRecord(Long memberCardId); - - /** - * 逻辑删除会员卡(下架卡种,防止已购会员数据异常) - * @param memberCardId 会员卡ID - * @return 受影响的行数 - */ - @Modifying - @Query("UPDATE member_card SET deleted_at = NOW() WHERE member_card_id = :memberCardId AND deleted_at IS NULL") - Mono logicalDelete(Long memberCardId); - - /** - * 【新增】安全更新会员卡信息(仅允许修改业务相关字段,防止覆盖敏感字段) - * @param memberCardId 会员卡ID - * @param name 卡种名称 - * @param price 价格 - * @param durationDays 有效天数 - * @param totalCount 总次数 - * @param denomination 面额 - * @param status 状态 - * @return 受影响的行数 - */ - @Modifying - @Query("UPDATE member_card SET " + - "member_card_name = COALESCE(:name, member_card_name), " + - "member_card_price = COALESCE(:price, member_card_price), " + - "duration_days = COALESCE(:durationDays, duration_days), " + - "total_count = COALESCE(:totalCount, total_count), " + - "denomination = COALESCE(:denomination, denomination), " + - "member_card_status = COALESCE(:status, member_card_status), " + - "updated_at = NOW() " + - "WHERE member_card_id = :memberCardId AND deleted_at IS NULL") - Mono updateSafe(Long memberCardId, String name, Double price, - Integer durationDays, Integer totalCount, - Double denomination, Integer status); - - /** - * 保存卡种信息(新增或更新) - * - 新增:entity.memberCardId 为 null 时,插入新记录 - * - 更新:entity.memberCardId 不为 null 时,根据ID更新现有记录 - * 注意:直接 save 会更新所有字段,如需安全更新请调用 updateSafe 方法 - * @param entity 卡种信息 - * @return 保存后的实体对象 - */ - @Override - Mono save(S entity); - - /** - * 批量查询上架的会员卡(用于小程序端展示) - * @param status 上架状态值 - * @return 上架的会员卡列表 - */ - @Query("SELECT * FROM member_card WHERE deleted_at IS NULL AND member_card_status = :status ORDER BY member_card_price ASC") - Flux findActiveCards(Integer status); -} \ 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 deleted file mode 100644 index bb3473e..0000000 --- a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/dao/MemberCardTransactionsDao.java +++ /dev/null @@ -1,173 +0,0 @@ -package cn.novalon.gym.manage.gymmembercard.dao; - -import cn.novalon.gym.manage.gymmembercard.entity.MemberCardTransactionsEntity; -import cn.novalon.gym.manage.gymmembercard.enums.MemberCardTransactionsAction; -import cn.novalon.gym.manage.gymmembercard.enums.MemberCardTransactionsType; -import org.springframework.data.domain.Pageable; -import org.springframework.data.r2dbc.repository.Modifying; -import org.springframework.data.r2dbc.repository.Query; -import org.springframework.data.r2dbc.repository.R2dbcRepository; -import org.springframework.data.repository.query.Param; -import org.springframework.stereotype.Repository; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; - -import java.time.LocalDateTime; - -@Repository -public interface MemberCardTransactionsDao extends R2dbcRepository { - /** - * 记录每一次变动 - * 购卡、扣次、续费、退款、过期,均插入一条流水,记录变动前后快照 - * 注意:返回值依赖 PostgreSQL 的 RETURNING 语法,若使用 MySQL 请删除 RETURNING * 并改用 save() - * @param memberCardId 会员卡ID - * @param memberId 会员ID - * @param operationType 操作类型 - * @param changeAmount 变动次数(次卡) - * @param changeBalance 变动金额(储值卡) - * @param afterRemainingCount 变动后剩余次数 - * @param afterRemainingBalance 变动后剩余余额 - * @param relatedBizType 关联业务类型 - * @param remark 备注 - * @return 插入的流水记录 - */ - @Modifying - @Query("INSERT INTO member_card_transactions (member_card_id, member_id, operation_type, change_amount, " + - "change_balance, after_remaining_count, after_remaining_balance, related_biz_type, remark, created_at, updated_at) " + - "VALUES (:memberCardId, :memberId, :operationType, :changeAmount, :changeBalance, " + - ":afterRemainingCount, :afterRemainingBalance, :relatedBizType, :remark, NOW(), NOW()) " + - "RETURNING *") - Mono insertTransaction(@Param("memberCardId") Long memberCardId, - @Param("memberId") Long memberId, - @Param("operationType") MemberCardTransactionsAction operationType, - @Param("changeAmount") Integer changeAmount, - @Param("changeBalance") Double changeBalance, - @Param("afterRemainingCount") Integer afterRemainingCount, - @Param("afterRemainingBalance") Double afterRemainingBalance, - @Param("relatedBizType") MemberCardTransactionsType relatedBizType, - @Param("remark") String remark); - - /** - * 会员端"使用记录" - * 按会员ID和时间范围查询,按时间倒序,显示每次变动明细 - * @param memberId 会员ID - * @param startTime 开始时间 - * @param endTime 结束时间 - * @param pageable 分页参数 - * @return 流水记录列表 - */ - @Query("SELECT * FROM member_card_transactions WHERE member_id = :memberId " + - "AND created_at BETWEEN :startTime AND :endTime AND deleted_at IS NULL " + - "ORDER BY created_at DESC LIMIT :#{#pageable.pageSize} OFFSET :#{#pageable.offset}") - Flux findByMemberIdAndTimeRange(@Param("memberId") Long memberId, - @Param("startTime") LocalDateTime startTime, - @Param("endTime") LocalDateTime endTime, - Pageable pageable); - - /** - * 后台"使用记录查询" - * 按会员、卡号、操作类型、时间范围等条件组合查询 - * @param memberId 会员ID - * @param memberCardId 会员卡ID - * @param operationType 操作类型 - * @param startTime 开始时间 - * @param endTime 结束时间 - * @param pageable 分页参数 - * @return 流水记录列表 - */ - @Query("SELECT * FROM member_card_transactions WHERE deleted_at IS NULL " + - "AND (:memberId IS NULL OR member_id = :memberId) " + - "AND (:memberCardId IS NULL OR member_card_id = :memberCardId) " + - "AND (:operationType IS NULL OR operation_type = :operationType) " + - "AND (:startTime IS NULL OR created_at >= :startTime) " + - "AND (:endTime IS NULL OR created_at <= :endTime) " + - "ORDER BY created_at DESC LIMIT :#{#pageable.pageSize} OFFSET :#{#pageable.offset}") - Flux findWithConditions(@Param("memberId") Long memberId, - @Param("memberCardId") Long memberCardId, - @Param("operationType") MemberCardTransactionsAction operationType, - @Param("startTime") LocalDateTime startTime, - @Param("endTime") LocalDateTime endTime, - Pageable pageable); - - /** - * 统计符合条件的流水总数 - * @param memberId 会员ID - * @param memberCardId 会员卡ID - * @param operationType 操作类型 - * @param startTime 开始时间 - * @param endTime 结束时间 - * @return 流水记录数量 - */ - @Query("SELECT COUNT(*) FROM member_card_transactions WHERE deleted_at IS NULL " + - "AND (:memberId IS NULL OR member_id = :memberId) " + - "AND (:memberCardId IS NULL OR member_card_id = :memberCardId) " + - "AND (:operationType IS NULL OR operation_type = :operationType) " + - "AND (:startTime IS NULL OR created_at >= :startTime) " + - "AND (:endTime IS NULL OR created_at <= :endTime)") - Mono countWithConditions(@Param("memberId") Long memberId, - @Param("memberCardId") Long memberCardId, - @Param("operationType") MemberCardTransactionsAction operationType, - @Param("startTime") LocalDateTime startTime, - @Param("endTime") LocalDateTime endTime); - - /** - * 按会员卡ID查询所有流水记录(补充常用方法) - * @param memberCardId 会员卡ID - * @return 该卡的所有流水记录,按时间倒序 - */ - @Query("SELECT * FROM member_card_transactions WHERE member_card_id = :memberCardId AND deleted_at IS NULL ORDER BY created_at DESC") - Flux findByMemberCardId(@Param("memberCardId") Long memberCardId); - - /** - * 数据统计 - 统计某卡种的总扣次数 - * @param memberCardId 会员卡ID - * @param startTime 开始时间 - * @param endTime 结束时间 - * @return 总扣次数 - */ - @Query("SELECT COALESCE(SUM(change_amount), 0) FROM member_card_transactions " + - "WHERE member_card_id = :memberCardId AND operation_type = 'DEDUCT' " + - "AND created_at BETWEEN :startTime AND :endTime AND deleted_at IS NULL") - Mono sumDeductCountByCardId(@Param("memberCardId") Long memberCardId, - @Param("startTime") LocalDateTime startTime, - @Param("endTime") LocalDateTime endTime); - - /** - * 数据统计 - 统计某时间段的续费总金额 - * @param startTime 开始时间 - * @param endTime 结束时间 - * @return 续费总金额 - */ - @Query("SELECT COALESCE(SUM(change_balance), 0) FROM member_card_transactions " + - "WHERE operation_type = 'RENEW' AND created_at BETWEEN :startTime AND :endTime AND deleted_at IS NULL") - Mono sumRenewAmountByTimeRange(@Param("startTime") LocalDateTime startTime, - @Param("endTime") LocalDateTime endTime); - - /** - * 数据统计 - 统计某会员的购卡总金额 - * @param memberId 会员ID - * @param startTime 开始时间 - * @param endTime 结束时间 - * @return 购卡总金额 - */ - @Query("SELECT COALESCE(SUM(change_balance), 0) FROM member_card_transactions " + - "WHERE member_id = :memberId AND operation_type = 'PURCHASE' " + - "AND created_at BETWEEN :startTime AND :endTime AND deleted_at IS NULL") - 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/domain/MemberCard.java b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/domain/MemberCard.java deleted file mode 100644 index 6a79ba5..0000000 --- a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/domain/MemberCard.java +++ /dev/null @@ -1,121 +0,0 @@ -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.time.LocalDateTime; - -/* - *@Author:shizhounian - *@Date:2026/5/10-05 22:47:31 - */ -@Schema(description = "会员卡类型表") -public class MemberCard extends BaseDomain { - //会员卡id - @Schema(description = "会员卡Id",example = "1") - private Long memberCardId; - - //会员卡名称 - @Schema(description = "会员卡名称",example = "月卡") - private String memberCardName; - - //会员卡类型 - @Schema(description = "会员卡类型",example = "TIME_CARD") - private String memberCardType; - - //会员卡价格 - @Schema(description = "会员卡价格",example = "199.0") - private Double memberCardPrice; - - //会员卡有效天数(时长卡用) - @Schema(description = "会员卡有效天数",example = "30") - private Integer memberCardValidityDays; - - //会员卡总次数(次卡用) - @Schema(description = "会员卡总次数",example = "10") - private Integer memberCardTotalTimes; - - //会员卡面额(储值卡用) - @Schema(description = "会员卡面额",example = "500.0") - private Double memberCardAmount; - - //会员卡状态:0-正常,1-禁用 - @Schema(description = "会员卡状态",example = "0") - private Integer memberCardStatus; - - //会员卡创建时间 - @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 deleted file mode 100644 index a837546..0000000 --- a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/domain/MemberCardRecord.java +++ /dev/null @@ -1,109 +0,0 @@ -package cn.novalon.gym.manage.gymmembercard.domain; - -import cn.novalon.gym.manage.gymmembercard.enums.MemberCardRecordStatus; -import cn.novalon.gym.manage.sys.core.domain.BaseDomain; -import io.swagger.v3.oas.annotations.media.Schema; - -import java.time.LocalDateTime; - -@Schema(description = "会员卡记录") -public class MemberCardRecord extends BaseDomain { - @Schema(description = "会员持有卡ID", example = "1") - private Long memberCardRecordId; - - @Schema(description = "会员ID", example = "1001") - private Long memberId; - - @Schema(description = "关联会员卡ID", example = "1") - private Long memberCardId; - - @Schema(description = "状态:ACTIVE-有效, USED_UP-用完, EXPIRED-过期, REFUNDED-已退款", example = "ACTIVE") - private MemberCardRecordStatus status; - - @Schema(description = "剩余次数", example = "10") - private Integer remainingTimes; - - @Schema(description = "剩余余额", example = "500.0") - private Double remainingAmount; - - @Schema(description = "到期时间", example = "2026-06-23 10:00:00") - private LocalDateTime expireTime; - - @Schema(description = "购买订单ID", example = "10001") - private Long sourceOrderId; - - @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 deleted file mode 100644 index aa7928f..0000000 --- a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/domain/MemberCardTransactions.java +++ /dev/null @@ -1,159 +0,0 @@ -package cn.novalon.gym.manage.gymmembercard.domain; - -import cn.novalon.gym.manage.gymmembercard.enums.MemberCardTransactionsAction; -import cn.novalon.gym.manage.gymmembercard.enums.MemberCardTransactionsType; -import cn.novalon.gym.manage.sys.core.domain.BaseDomain; -import io.swagger.v3.oas.annotations.media.Schema; - -import java.time.LocalDateTime; - -/* - *@Author:shizhounian - *@Date:2026/5/10-05 22:47:31 - */ -@Schema(description = "会员卡流水") -public class MemberCardTransactions extends BaseDomain { - //会员卡流水Id - @Schema(description = "会员卡流水Id",example = "1") - private Long memberCardTransactionsId; - - //会员卡Id - @Schema(description = "会员卡Id",example = "1") - private Long memberCardId; - - //会员Id - @Schema(description = "会员Id",example = "1") - private Long memberId; - - //操作类型:PURCHASE(购买) / DEDUCT(扣次/扣费) / RENEW(续费) / REFUND(退款) / EXPIRE(过期) - @Schema(description = "操作类型",example = "PURCHASE") - private MemberCardTransactionsAction operationType; - - //变动次数(次卡用) - @Schema(description = "变动次数(次卡用)",example = "1") - private Integer changeAmount; - - //变动金额(储值卡用) - @Schema(description = "变动金额(储值卡用)",example = "1") - private Double changeBalance; - - //变动后剩余次数 - @Schema(description = "变动后剩余次数",example = "1") - private Integer afterRemainingCount; - - //变动后剩余金额 - @Schema(description = "变动后剩余金额",example = "500.0") - private Double afterRemainingBalance; - - //关联业务类型 - @Schema(description = "关联业务类型",example = "GROUP_CLASS") - private MemberCardTransactionsType relatedBizType; - - //备注 - @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 deleted file mode 100644 index 01b5f05..0000000 --- a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/domain/RefundApplication.java +++ /dev/null @@ -1,127 +0,0 @@ -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 deleted file mode 100644 index d57641e..0000000 --- a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/entity/MemberCardEntity.java +++ /dev/null @@ -1,123 +0,0 @@ -package cn.novalon.gym.manage.gymmembercard.entity; - -import cn.novalon.gym.manage.db.entity.BaseEntity; -import lombok.Data; -import org.springframework.data.relational.core.mapping.Column; -import org.springframework.data.relational.core.mapping.Table; - -import java.time.LocalDateTime; - -/* - *@Author:shizhounian - *@Date:2026/5/10-05 22:47:31 - */ -@Table("member_card") -public class MemberCardEntity extends BaseEntity { - //会员卡id - @Column("member_card_id") - private Long memberCardId; - - //会员卡名称 - @Column("member_card_name") - private String memberCardName; - - //会员卡类型 - @Column("member_card_type") - private String memberCardType; - - //会员卡价格 - @Column("member_card_price") - private Double memberCardPrice; - - //会员卡有效天数(时长卡用) - @Column("member_card_validity_days") - private Integer memberCardValidityDays; - - //会员卡总次数(次卡用) - @Column("member_card_total_times") - private Integer memberCardTotalTimes; - - //会员卡面额(储值卡用) - @Column("member_card_amount") - private Double memberCardAmount; - - //会员卡状态:0-正常,1-禁用 - @Column("member_card_status") - private Integer memberCardStatus; - - //会员卡创建时间 - @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 deleted file mode 100644 index a601f43..0000000 --- a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/entity/MemberCardRecordEntity.java +++ /dev/null @@ -1,124 +0,0 @@ -package cn.novalon.gym.manage.gymmembercard.entity; - -import cn.novalon.gym.manage.db.entity.BaseEntity; -import cn.novalon.gym.manage.gymmembercard.enums.MemberCardRecordStatus; -import lombok.Data; -import org.springframework.data.relational.core.mapping.Column; -import org.springframework.data.relational.core.mapping.Table; - -import java.time.LocalDateTime; - -/* - *@Author:shizhounian - *@Date:2026/5/10-05 22:47:31 - */ -@Table("member_card_record") -public class MemberCardRecordEntity extends BaseEntity { - //会员持有卡id - @Column("member_card_record_id") - private Long memberCardRecordId; - - //会员Id - @Column("member_id") - private Long memberId; - - //关联会员卡Id - @Column("member_card_id") - private Long memberCardId; - - //状态:ACTIVE(有效) / USED_UP(用完) / EXPIRED(过期) / REFUNDED(已退款) - @Column("status") - private MemberCardRecordStatus status; - - //剩余次数 - @Column("remaining_times") - private Integer remainingTimes; - - //剩余余额 - @Column("remaining_amount") - private Double remainingAmount; - - //到期时间 - @Column("expire_time") - private LocalDateTime expireTime; - - //购买订单Id - @Column("source_order_id") - private Long sourceOrderId; - - //购买时间 - @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 deleted file mode 100644 index 1aedaea..0000000 --- a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/entity/MemberCardTransactionsEntity.java +++ /dev/null @@ -1,160 +0,0 @@ -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 org.springframework.data.relational.core.mapping.Column; -import org.springframework.data.relational.core.mapping.Table; - -import java.time.LocalDateTime; - -/* - *@Author:shizhounian - *@Date:2026/5/10-05 22:47:31 - */ -@Table("member_card_transactions") -public class MemberCardTransactionsEntity extends BaseEntity{ - - @Column("id") - private Long id; - - @Column("member_card_record_id") - private Long memberCardRecordId; - - @Column("member_id") - private Long memberId; - - @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; - - 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 deleted file mode 100644 index 49fd781..0000000 --- a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/entity/RefundApplicationEntity.java +++ /dev/null @@ -1,127 +0,0 @@ -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 deleted file mode 100644 index 42e8094..0000000 --- a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/enums/MemberCardEvent.java +++ /dev/null @@ -1,34 +0,0 @@ -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/MemberCardRecordStatus.java b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/enums/MemberCardRecordStatus.java deleted file mode 100644 index b4fb006..0000000 --- a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/enums/MemberCardRecordStatus.java +++ /dev/null @@ -1,36 +0,0 @@ -package cn.novalon.gym.manage.gymmembercard.enums; - -import io.swagger.v3.oas.annotations.media.Schema; - -/* - *@Author:shizhounian - *@Date:2026/5/10-05 23:59:29 - */ -@Schema(description = "会员卡状态枚举") -public enum MemberCardRecordStatus { - //有效 - @Schema(description = "有效") - ACTIVE("有效"), - - //用完 - @Schema(description = "用完") - USED_UP("用完"), - - //过期 - @Schema(description = "过期") - EXPIRED("过期"), - - //已退款 - @Schema(description = "已退款") - REFUNDED("已退款"); - - private final String desc; - - MemberCardRecordStatus(String desc) { - this.desc = desc; - } - - public String getDesc() { - return desc; - } -} diff --git a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/enums/MemberCardTransactionsAction.java b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/enums/MemberCardTransactionsAction.java deleted file mode 100644 index 6be4fc0..0000000 --- a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/enums/MemberCardTransactionsAction.java +++ /dev/null @@ -1,40 +0,0 @@ -package cn.novalon.gym.manage.gymmembercard.enums; - -import io.swagger.v3.oas.annotations.media.Schema; - -/* - *@Author:shizhounian - *@Date:2026/5/10-05 23:59:29 - */ -@Schema(description = "会员卡流水操作枚举") -public enum MemberCardTransactionsAction { - //购买 - @Schema(description = "购买") - PURCHASE("购买"), - - //扣次/扣费 - @Schema(description = "扣次/扣费") - DEDUCT("扣次/扣费"), - - //续费 - @Schema(description = "续费") - RENEW("续费"), - - //退款 - @Schema(description = "退款") - REFUND("退款"), - - //过期 - @Schema(description = "过期") - EXPIRE("过期"); - - private final String desc; - - MemberCardTransactionsAction(String desc) { - this.desc = desc; - } - - public String getDesc() { - return desc; - } -} diff --git a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/enums/MemberCardTransactionsType.java b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/enums/MemberCardTransactionsType.java deleted file mode 100644 index ae55fe9..0000000 --- a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/enums/MemberCardTransactionsType.java +++ /dev/null @@ -1,32 +0,0 @@ -package cn.novalon.gym.manage.gymmembercard.enums; - -import io.swagger.v3.oas.annotations.media.Schema; - -/* - *@Author:shizhounian - *@Date:2026/5/10-05 23:59:29 - */ -@Schema(description = "会员卡流水关联业务类型枚举") -public enum MemberCardTransactionsType { - //团课 - @Schema(description = "团课") - GROUP_CLASS("团课"), - - //私教 - @Schema(description = "私教") - PT_CLASS("私教"), - - //签到 - @Schema(description = "签到") - CHECK_IN("签到"); - - private final String desc; - - MemberCardTransactionsType(String desc) { - this.desc = desc; - } - - public String getDesc() { - return desc; - } -} 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 deleted file mode 100644 index 6cafcde..0000000 --- a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/repository/IMemberCardRecordRepository.java +++ /dev/null @@ -1,34 +0,0 @@ -package cn.novalon.gym.manage.gymmembercard.repository; - -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; - -public interface IMemberCardRecordRepository { - - Mono findById(Long id); - - Mono save(MemberCardRecord record); - - Mono insertActiveRecord(MemberCardRecord record); - - Flux findByMemberId(Long memberId, Pageable pageable); - - 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/IMemberCardRepository.java b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/repository/IMemberCardRepository.java deleted file mode 100644 index 8b24397..0000000 --- a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/repository/IMemberCardRepository.java +++ /dev/null @@ -1,78 +0,0 @@ -package cn.novalon.gym.manage.gymmembercard.repository; - -import cn.novalon.gym.manage.gymmembercard.domain.MemberCard; -import org.springframework.data.domain.Pageable; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; - -public interface IMemberCardRepository { - /** - * 根据会员卡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); -} 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 deleted file mode 100644 index 46ff531..0000000 --- a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/repository/IMemberCardTransactionsRepository.java +++ /dev/null @@ -1,112 +0,0 @@ -package cn.novalon.gym.manage.gymmembercard.repository; - -import cn.novalon.gym.manage.gymmembercard.domain.MemberCardTransactions; -import cn.novalon.gym.manage.gymmembercard.enums.MemberCardTransactionsAction; -import org.springframework.data.domain.Pageable; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; - -import java.time.LocalDateTime; - -public interface IMemberCardTransactionsRepository { - - /** - * 记录每一次变动 - * @param transactions 流水记录 - * @return 插入的流水记录 - */ - Mono insertTransaction(MemberCardTransactions transactions); - - /** - * 会员端"使用记录" - * @param memberId 会员ID - * @param startTime 开始时间 - * @param endTime 结束时间 - * @param pageable 分页参数 - * @return 流水记录列表 - */ - Flux findByMemberIdAndTimeRange(Long memberId, LocalDateTime startTime, - LocalDateTime endTime, Pageable pageable); - - /** - * 后台"使用记录查询" - * @param memberId 会员ID - * @param memberCardId 会员卡ID - * @param operationType 操作类型 - * @param startTime 开始时间 - * @param endTime 结束时间 - * @param pageable 分页参数 - * @return 流水记录列表 - */ - Flux findWithConditions(Long memberId, Long memberCardId, - MemberCardTransactionsAction operationType, - LocalDateTime startTime, LocalDateTime endTime, - Pageable pageable); - - /** - * 统计符合条件的流水总数 - * @param memberId 会员ID - * @param memberCardId 会员卡ID - * @param operationType 操作类型 - * @param startTime 开始时间 - * @param endTime 结束时间 - * @return 流水记录数量 - */ - Mono countWithConditions(Long memberId, Long memberCardId, - MemberCardTransactionsAction operationType, - LocalDateTime startTime, LocalDateTime endTime); - - /** - * 按会员卡ID查询所有流水记录 - * @param memberCardId 会员卡ID - * @return 该卡的所有流水记录,按时间倒序 - */ - Flux findByMemberCardId(Long memberCardId); - - /** - * 数据统计 - 统计某卡种的总扣次数 - * @param memberCardId 会员卡ID - * @param startTime 开始时间 - * @param endTime 结束时间 - * @return 总扣次数 - */ - Mono sumDeductCountByCardId(Long memberCardId, LocalDateTime startTime, LocalDateTime endTime); - - /** - * 数据统计 - 统计某时间段的续费总金额 - * @param startTime 开始时间 - * @param endTime 结束时间 - * @return 续费总金额 - */ - Mono sumRenewAmountByTimeRange(LocalDateTime startTime, LocalDateTime endTime); - - /** - * 数据统计 - 统计某会员的购卡总金额 - * @param memberId 会员ID - * @param startTime 开始时间 - * @param endTime 结束时间 - * @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 deleted file mode 100644 index 931b1fc..0000000 --- a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/repository/IRefundApplicationRepository.java +++ /dev/null @@ -1,81 +0,0 @@ -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 deleted file mode 100644 index 30406e5..0000000 --- a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/repository/impl/MemberCardRecordRepositoryImpl.java +++ /dev/null @@ -1,105 +0,0 @@ -package cn.novalon.gym.manage.gymmembercard.repository.impl; - -import cn.novalon.gym.manage.gymmembercard.dao.MemberCardRecordDao; -import cn.novalon.gym.manage.gymmembercard.domain.MemberCardRecord; -import cn.novalon.gym.manage.gymmembercard.entity.MemberCardRecordEntity; -import cn.novalon.gym.manage.gymmembercard.enums.MemberCardRecordStatus; -import cn.novalon.gym.manage.gymmembercard.repository.IMemberCardRecordRepository; -import cn.novalon.gym.manage.gymmembercard.util.BeanConvertUtil; -import org.springframework.data.domain.Pageable; -import org.springframework.data.r2dbc.core.R2dbcEntityTemplate; -import org.springframework.stereotype.Repository; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; - -@Repository -public class MemberCardRecordRepositoryImpl implements IMemberCardRecordRepository { - private final MemberCardRecordDao memberCardRecordDao; - private final BeanConvertUtil beanConvertUtil; - private final R2dbcEntityTemplate r2dbcEntityTemplate; - - public MemberCardRecordRepositoryImpl(MemberCardRecordDao memberCardRecordDao, - BeanConvertUtil beanConvertUtil, - R2dbcEntityTemplate r2dbcEntityTemplate) { - this.memberCardRecordDao = memberCardRecordDao; - this.beanConvertUtil = beanConvertUtil; - this.r2dbcEntityTemplate = r2dbcEntityTemplate; - } - - @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); - return memberCardRecordDao.insertActiveRecord( - entity.getMemberId(), - entity.getMemberCardId(), - entity.getExpireTime(), - entity.getRemainingTimes(), - entity.getRemainingAmount(), - entity.getSourceOrderId()) - .map(e -> beanConvertUtil.toBean(e, MemberCardRecord.class)); - } - - @Override - public Flux findByMemberId(Long memberId, Pageable pageable) { - return memberCardRecordDao.findByMemberId(memberId, pageable) - .map(entity -> beanConvertUtil.toBean(entity, MemberCardRecord.class)); - } - - @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)); - } - - @Override - public Mono validateStoredCard(Long recordId, Double requiredAmount) { - return memberCardRecordDao.validateStoredCard(recordId, requiredAmount) - .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/MemberCardRepositoryImpl.java b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/repository/impl/MemberCardRepositoryImpl.java deleted file mode 100644 index afc3f97..0000000 --- a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/repository/impl/MemberCardRepositoryImpl.java +++ /dev/null @@ -1,146 +0,0 @@ -package cn.novalon.gym.manage.gymmembercard.repository.impl; - -import cn.novalon.gym.manage.gymmembercard.dao.MemberCardDao; -import cn.novalon.gym.manage.gymmembercard.domain.MemberCard; -import cn.novalon.gym.manage.gymmembercard.entity.MemberCardEntity; -import cn.novalon.gym.manage.gymmembercard.repository.IMemberCardRepository; -import cn.novalon.gym.manage.gymmembercard.util.BeanConvertUtil; -import org.springframework.data.domain.Pageable; -import org.springframework.data.r2dbc.core.R2dbcEntityTemplate; -import org.springframework.stereotype.Repository; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; - -@Repository -public class MemberCardRepositoryImpl implements IMemberCardRepository { - private final MemberCardDao memberCardDao; - private final BeanConvertUtil beanConvertUtil; - private final R2dbcEntityTemplate r2dbcEntityTemplate; - //构造函数,初始化 - public MemberCardRepositoryImpl(MemberCardDao memberCardDao, BeanConvertUtil beanConvertUtil, R2dbcEntityTemplate r2dbcEntityTemplate) { - this.memberCardDao = memberCardDao; - this.beanConvertUtil = beanConvertUtil; - this.r2dbcEntityTemplate = r2dbcEntityTemplate; - } - - /** - * 根据会员卡ID查询会员卡详情(用于编辑前回显或小程序端展示) - * @param memberCardId 会员卡ID - * @return 会员卡完整信息,如果不存在或已删除则返回空 - */ - @Override - public Mono findByMemberCardIdAndDeletedAtIsNull(Long memberCardId) { - return memberCardDao.findByMemberCardIdAndDeletedAtIsNull(memberCardId) - .map(entity -> beanConvertUtil.toBean(entity, MemberCard.class)); - } - - /** - * 条件查询会员卡列表(后台卡种列表展示,支持多条件组合+分页+排序) - * @param status 会员卡状态(上架/下架) - * @param name 会员卡名称(模糊查询) - * @param type 会员卡类型 - * @param minPrice 最低价格 - * @param maxPrice 最高价格 - * @param pageable 分页和排序参数 - * @return 符合条件的会员卡列表 - */ - @Override - public Flux findWithConditions(Integer status, String name, String type, - Double minPrice, Double maxPrice, Pageable pageable) { - return memberCardDao.findWithConditions(status, name, type, minPrice, maxPrice, pageable) - .map(entity -> beanConvertUtil.toBean(entity, MemberCard.class)); - } - - /** - * 统计符合条件的会员卡总数(配合列表查询使用) - * @param status 会员卡状态 - * @param name 会员卡名称(模糊查询) - * @param type 会员卡类型 - * @param minPrice 最低价格 - * @param maxPrice 最高价格 - * @return 符合条件的会员卡数量 - */ - @Override - public Mono countWithConditions(Integer status, String name, String type, - Double minPrice, Double maxPrice) { - return memberCardDao.countWithConditions(status, name, type, minPrice, maxPrice); - } - - /** - * 按状态查询会员卡列表(用于小程序端展示可购买卡列表,只展示上架的卡) - * @param status 会员卡状态(通常传上架状态) - * @param pageable 分页和排序参数 - * @return 符合条件的会员卡列表 - */ - @Override - public Flux findByMemberCardStatusAndDeletedAtIsNull(Integer status, Pageable pageable) { - return memberCardDao.findByMemberCardStatusAndDeletedAtIsNull(status, pageable) - .map(entity -> beanConvertUtil.toBean(entity, MemberCard.class)); - } - - /** - * 检查会员卡是否已被购买(用于删除前的校验) - * @param memberCardId 会员卡ID - * @return 如果存在关联的会员记录则返回true,否则返回false - */ - @Override - public Mono existsPurchasedRecord(Long memberCardId) { - return memberCardDao.existsPurchasedRecord(memberCardId); - } - - /** - * 逻辑删除会员卡(下架卡种,防止已购会员数据异常) - * @param memberCardId 会员卡ID - * @return 受影响的行数 - */ - @Override - public Mono logicalDelete(Long memberCardId) { - return memberCardDao.logicalDelete(memberCardId); - } - - /** - * 安全更新会员卡信息(不覆盖不允许修改的字段) - * @param memberCardId 会员卡ID - * @param updateData 需要更新的卡种信息 - * @return 受影响的行数 - */ - public Mono updateSafe(Long memberCardId, MemberCard updateData) { - MemberCardEntity memberCardEntity = beanConvertUtil.toBean(updateData, MemberCardEntity.class); - return memberCardDao.updateSafe( - memberCardId, - memberCardEntity.getMemberCardName(), - memberCardEntity.getMemberCardPrice(), - memberCardEntity.getMemberCardValidityDays(), - memberCardEntity.getMemberCardTotalTimes(), - memberCardEntity.getMemberCardAmount(), - memberCardEntity.getMemberCardStatus() - ); - } - - - /** - * 保存卡种信息(新增或更新) - * - 新增:entity.memberCardId 为 null 时,插入新记录 - * - 更新:entity.memberCardId 不为 null 时,根据ID更新现有记录 - * 建议:更新时优先使用 updateSafe 方法避免全字段覆盖 - * @param entity 卡种信息 - * @return 保存后的实体对象 - */ - @Override - public Mono save(MemberCard entity) { - MemberCardEntity cardEntity = beanConvertUtil.toBean(entity, MemberCardEntity.class); - return memberCardDao.save(cardEntity) - .map(savedEntity -> beanConvertUtil.toBean(savedEntity, MemberCard.class)); - } - - /** - * 批量查询上架的会员卡(用于小程序端展示) - * @param status 上架状态值 - * @return 上架的会员卡列表 - */ - @Override - public Flux findActiveCards(Integer status) { - return memberCardDao.findActiveCards(status) - .map(entity -> beanConvertUtil.toBean(entity, MemberCard.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 deleted file mode 100644 index 4afecbd..0000000 --- a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/repository/impl/MemberCardTransactionsRepositoryImpl.java +++ /dev/null @@ -1,170 +0,0 @@ -package cn.novalon.gym.manage.gymmembercard.repository.impl; - -import cn.novalon.gym.manage.gymmembercard.dao.MemberCardTransactionsDao; -import cn.novalon.gym.manage.gymmembercard.domain.MemberCardTransactions; -import cn.novalon.gym.manage.gymmembercard.entity.MemberCardTransactionsEntity; -import cn.novalon.gym.manage.gymmembercard.enums.MemberCardTransactionsAction; -import cn.novalon.gym.manage.gymmembercard.repository.IMemberCardTransactionsRepository; -import cn.novalon.gym.manage.gymmembercard.util.BeanConvertUtil; -import org.springframework.data.domain.Pageable; -import org.springframework.data.r2dbc.core.R2dbcEntityTemplate; -import org.springframework.stereotype.Repository; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; - -import java.time.LocalDateTime; - -@Repository -public class MemberCardTransactionsRepositoryImpl implements IMemberCardTransactionsRepository { - private final MemberCardTransactionsDao memberCardTransactionsDao; - private final BeanConvertUtil beanConvertUtil; - private final R2dbcEntityTemplate r2dbcEntityTemplate; - - public MemberCardTransactionsRepositoryImpl(MemberCardTransactionsDao memberCardTransactionsDao, BeanConvertUtil beanConvertUtil, R2dbcEntityTemplate r2dbcEntityTemplate) { - this.memberCardTransactionsDao = memberCardTransactionsDao; - this.beanConvertUtil = beanConvertUtil; - this.r2dbcEntityTemplate = r2dbcEntityTemplate; - } - - /** - * 记录每一次变动 - * @param transactions 流水记录 - * @return 插入的流水记录 - */ - @Override - public Mono insertTransaction(MemberCardTransactions transactions) { - MemberCardTransactionsEntity entity = beanConvertUtil.toBean(transactions, MemberCardTransactionsEntity.class); - return memberCardTransactionsDao.insertTransaction( - entity.getMemberCardId(), - entity.getMemberId(), - entity.getOperationType(), - entity.getChangeAmount(), - entity.getChangeBalance(), - entity.getAfterRemainingCount(), - entity.getAfterRemainingBalance(), - entity.getRelatedBizType(), - entity.getRemark()) - .map(e -> beanConvertUtil.toBean(e, MemberCardTransactions.class)); - } - - /** - * 会员端"使用记录" - * @param memberId 会员ID - * @param startTime 开始时间 - * @param endTime 结束时间 - * @param pageable 分页参数 - * @return 流水记录列表 - */ - @Override - public Flux findByMemberIdAndTimeRange(Long memberId, LocalDateTime startTime, - LocalDateTime endTime, Pageable pageable) { - return memberCardTransactionsDao.findByMemberIdAndTimeRange(memberId, startTime, endTime, pageable) - .map(entity -> beanConvertUtil.toBean(entity, MemberCardTransactions.class)); - } - - /** - * 后台"使用记录查询" - * @param memberId 会员ID - * @param memberCardId 会员卡ID - * @param operationType 操作类型 - * @param startTime 开始时间 - * @param endTime 结束时间 - * @param pageable 分页参数 - * @return 流水记录列表 - */ - @Override - public Flux findWithConditions(Long memberId, Long memberCardId, - MemberCardTransactionsAction operationType, - LocalDateTime startTime, LocalDateTime endTime, - Pageable pageable) { - return memberCardTransactionsDao.findWithConditions(memberId, memberCardId, operationType, - startTime, endTime, pageable) - .map(entity -> beanConvertUtil.toBean(entity, MemberCardTransactions.class)); - } - - /** - * 统计符合条件的流水总数 - * @param memberId 会员ID - * @param memberCardId 会员卡ID - * @param operationType 操作类型 - * @param startTime 开始时间 - * @param endTime 结束时间 - * @return 流水记录数量 - */ - @Override - public Mono countWithConditions(Long memberId, Long memberCardId, - MemberCardTransactionsAction operationType, - LocalDateTime startTime, LocalDateTime endTime) { - return memberCardTransactionsDao.countWithConditions(memberId, memberCardId, - operationType, startTime, endTime); - } - - /** - * 按会员卡ID查询所有流水记录 - * @param memberCardId 会员卡ID - * @return 该卡的所有流水记录,按时间倒序 - */ - @Override - public Flux findByMemberCardId(Long memberCardId) { - return memberCardTransactionsDao.findByMemberCardId(memberCardId) - .map(entity -> beanConvertUtil.toBean(entity, MemberCardTransactions.class)); - } - - /** - * 数据统计 - 统计某卡种的总扣次数 - * @param memberCardId 会员卡ID - * @param startTime 开始时间 - * @param endTime 结束时间 - * @return 总扣次数 - */ - @Override - public Mono sumDeductCountByCardId(Long memberCardId, LocalDateTime startTime, LocalDateTime endTime) { - return memberCardTransactionsDao.sumDeductCountByCardId(memberCardId, startTime, endTime); - } - - /** - * 数据统计 - 统计某时间段的续费总金额 - * @param startTime 开始时间 - * @param endTime 结束时间 - * @return 续费总金额 - */ - @Override - public Mono sumRenewAmountByTimeRange(LocalDateTime startTime, LocalDateTime endTime) { - return memberCardTransactionsDao.sumRenewAmountByTimeRange(startTime, endTime); - } - - /** - * 数据统计 - 统计某会员的购卡总金额 - * @param memberId 会员ID - * @param startTime 开始时间 - * @param endTime 结束时间 - * @return 购卡总金额 - */ - @Override - 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 deleted file mode 100644 index ca85d61..0000000 --- a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/repository/impl/RefundApplicationRepositoryImpl.java +++ /dev/null @@ -1,125 +0,0 @@ -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/IMemberCardTransactionsService.java b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/sevice/IMemberCardTransactionsService.java deleted file mode 100644 index 06b1bf2..0000000 --- a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/sevice/IMemberCardTransactionsService.java +++ /dev/null @@ -1,112 +0,0 @@ -package cn.novalon.gym.manage.gymmembercard.sevice; - -import cn.novalon.gym.manage.gymmembercard.domain.MemberCardTransactions; -import cn.novalon.gym.manage.gymmembercard.enums.MemberCardTransactionsAction; -import org.springframework.data.domain.Pageable; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; - -import java.time.LocalDateTime; - -public interface IMemberCardTransactionsService { - - /** - * 记录每一次变动 - * @param transactions 流水记录 - * @return 插入的流水记录 - */ - Mono insertTransaction(MemberCardTransactions transactions); - - /** - * 会员端"使用记录" - * @param memberId 会员ID - * @param startTime 开始时间 - * @param endTime 结束时间 - * @param pageable 分页参数 - * @return 流水记录列表 - */ - Flux findByMemberIdAndTimeRange(Long memberId, LocalDateTime startTime, - LocalDateTime endTime, Pageable pageable); - - /** - * 后台"使用记录查询" - * @param memberId 会员ID - * @param memberCardId 会员卡ID - * @param operationType 操作类型 - * @param startTime 开始时间 - * @param endTime 结束时间 - * @param pageable 分页参数 - * @return 流水记录列表 - */ - Flux findWithConditions(Long memberId, Long memberCardId, - MemberCardTransactionsAction operationType, - LocalDateTime startTime, LocalDateTime endTime, - Pageable pageable); - - /** - * 统计符合条件的流水总数 - * @param memberId 会员ID - * @param memberCardId 会员卡ID - * @param operationType 操作类型 - * @param startTime 开始时间 - * @param endTime 结束时间 - * @return 流水记录数量 - */ - Mono countWithConditions(Long memberId, Long memberCardId, - MemberCardTransactionsAction operationType, - LocalDateTime startTime, LocalDateTime endTime); - - /** - * 按会员卡ID查询所有流水记录 - * @param memberCardId 会员卡ID - * @return 该卡的所有流水记录,按时间倒序 - */ - Flux findByMemberCardId(Long memberCardId); - - /** - * 数据统计 - 统计某卡种的总扣次数 - * @param memberCardId 会员卡ID - * @param startTime 开始时间 - * @param endTime 结束时间 - * @return 总扣次数 - */ - Mono sumDeductCountByCardId(Long memberCardId, LocalDateTime startTime, LocalDateTime endTime); - - /** - * 数据统计 - 统计某时间段的续费总金额 - * @param startTime 开始时间 - * @param endTime 结束时间 - * @return 续费总金额 - */ - Mono sumRenewAmountByTimeRange(LocalDateTime startTime, LocalDateTime endTime); - - /** - * 数据统计 - 统计某会员的购卡总金额 - * @param memberId 会员ID - * @param startTime 开始时间 - * @param endTime 结束时间 - * @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/impl/MemberCardTransactionsServiceImpl.java b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/sevice/impl/MemberCardTransactionsServiceImpl.java deleted file mode 100644 index 119c4bf..0000000 --- a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/sevice/impl/MemberCardTransactionsServiceImpl.java +++ /dev/null @@ -1,89 +0,0 @@ -package cn.novalon.gym.manage.gymmembercard.sevice.impl; - -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; -import reactor.core.publisher.Mono; - -import java.time.LocalDateTime; - -@Slf4j -@Service -public class MemberCardTransactionsServiceImpl implements IMemberCardTransactionsService { - private final IMemberCardTransactionsRepository memberCardTransactionsRepository; - - public MemberCardTransactionsServiceImpl(IMemberCardTransactionsRepository memberCardTransactionsRepository) { - this.memberCardTransactionsRepository = memberCardTransactionsRepository; - } - - @Override - public Mono insertTransaction(MemberCardTransactions transactions) { - return memberCardTransactionsRepository.insertTransaction(transactions); - } - - @Override - public Flux findByMemberIdAndTimeRange(Long memberId, LocalDateTime startTime, - LocalDateTime endTime, Pageable pageable) { - return memberCardTransactionsRepository.findByMemberIdAndTimeRange(memberId, startTime, endTime, pageable); - } - - @Override - public Flux findWithConditions(Long memberId, Long memberCardId, - MemberCardTransactionsAction operationType, - LocalDateTime startTime, LocalDateTime endTime, - Pageable pageable) { - return memberCardTransactionsRepository.findWithConditions(memberId, memberCardId, operationType, - startTime, endTime, pageable); - } - - @Override - public Mono countWithConditions(Long memberId, Long memberCardId, - MemberCardTransactionsAction operationType, - LocalDateTime startTime, LocalDateTime endTime) { - return memberCardTransactionsRepository.countWithConditions(memberId, memberCardId, - operationType, startTime, endTime); - } - - @Override - public Flux findByMemberCardId(Long memberCardId) { - return memberCardTransactionsRepository.findByMemberCardId(memberCardId); - } - - @Override - public Mono sumDeductCountByCardId(Long memberCardId, LocalDateTime startTime, LocalDateTime endTime) { - return memberCardTransactionsRepository.sumDeductCountByCardId(memberCardId, startTime, endTime); - } - - @Override - public Mono sumRenewAmountByTimeRange(LocalDateTime startTime, LocalDateTime endTime) { - return memberCardTransactionsRepository.sumRenewAmountByTimeRange(startTime, endTime); - } - - @Override - 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/resources/sql b/gym-manage-api/gym-member-card/src/main/resources/sql deleted file mode 100644 index 509e01f..0000000 --- a/gym-manage-api/gym-member-card/src/main/resources/sql +++ /dev/null @@ -1,132 +0,0 @@ --- ============================================ --- 会员卡类型表 --- ============================================ -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/gym-member/.gitignore b/gym-manage-api/gym-member/.gitignore new file mode 100644 index 0000000..9d5d968 --- /dev/null +++ b/gym-manage-api/gym-member/.gitignore @@ -0,0 +1,47 @@ +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ + +### Maven ### +pom.xml.tag +pom.xml.releaseBackup +pom.xml.versionsBackup +pom.xml.next +release.properties +dependency-reduced-pom.xml +buildNumber.properties +.mvn/timing.properties +.mvn/wrapper/maven-wrapper.jar + +### System Files ### +.DS_Store +Thumbs.db diff --git a/gym-manage-api/gym-member/CARD_MERGE_REPORT.md b/gym-manage-api/gym-member/CARD_MERGE_REPORT.md new file mode 100644 index 0000000..9995bbe --- /dev/null +++ b/gym-manage-api/gym-member/CARD_MERGE_REPORT.md @@ -0,0 +1,199 @@ +# gym-member-card 模块合并到 gym-member 完成报告 + +## 合并概述 +将 `gym-member-card` 模块的功能合并到 `gym-member` 模块的 `card` 子包中,保持功能不变,代码风格以 `gym-member` 为基准。 + +## 已完成的工作 + +### 1. 目录结构创建 ✅ +在 `gym-member/src/main/java/cn/novalon/gym/manage/member/card` 下创建了以下子目录: +- `entity/` - 实体类 +- `dto/` - 数据传输对象 +- `vo/` - 视图对象 +- `repository/` - 数据访问层 +- `service/` - 服务接口 +- `service/impl/` - 服务实现 +- `handler/` - 业务处理器 +- `enums/` - 枚举类 +- `util/` - 工具类 + +### 2. 实体类重构(Entity)✅ +按照 gym-member 的风格(使用 Lombok),创建了以下实体类: + +| 原文件 | 新文件 | 说明 | +|--------|--------|------| +| MemberCardEntity.java + MemberCard.java | card/entity/MemberCard.java | 会员卡类型实体 | +| MemberCardRecordEntity.java + MemberCardRecord.java | card/entity/MemberCardRecord.java | 会员持卡记录实体 | +| MemberCardTransactionsEntity.java | card/entity/MemberCardTransaction.java | 交易流水实体 | +| RefundApplicationEntity.java | card/entity/RefundApplication.java | 退款申请实体 | + +**主要改进:** +- 使用 `@Data`, `@Builder`, `@NoArgsConstructor`, `@AllArgsConstructor` Lombok 注解 +- 继承 `cn.novalon.gym.manage.member.entity.BaseEntity` +- 移除手动编写的 getter/setter +- 添加规范的 JavaDoc 注释 + +### 3. 枚举类创建 ✅ +创建了以下枚举类: + +| 原文件 | 新文件 | 说明 | +|--------|--------|------| +| MemberCardType.java | card/enums/MemberCardType.java | 会员卡类型 | +| MemberCardRecordStatus.java | card/enums/MemberCardRecordStatus.java | 卡片状态 | +| MemberCardTransactionsAction.java | card/enums/TransactionType.java | 交易操作类型 | +| MemberCardTransactionsType.java | card/enums/BizType.java | 关联业务类型 | +| MemberCardEvent.java | card/enums/CardEvent.java | 状态机事件 | + +### 4. Repository 层合并 ✅ +将原有的 DAO 接口整合为 Repository 接口,直接继承 `R2dbcRepository`: + +| 原文件 | 新文件 | 说明 | +|--------|--------|------| +| MemberCardDao.java | card/repository/MemberCardRepository.java | 会员卡类型 Repository | +| MemberCardRecordDao.java | card/repository/MemberCardRecordRepository.java | 持卡记录 Repository | +| MemberCardTransactionsDao.java | card/repository/MemberCardTransactionRepository.java | 交易流水 Repository | +| RefundApplicationDao.java | card/repository/RefundApplicationRepository.java | 退款申请 Repository | + +**主要改进:** +- 统一命名规范(移除 Dao,使用 Repository) +- 继承 `R2dbcRepository` +- 保留所有自定义查询方法 +- 更新参数类型为新的实体类 + +### 5. 数据库 Schema 迁移 ✅ +将会员卡相关的表结构添加到 `gym-member/src/main/resources/db/schema.sql`: + +新增表: +- `member_card` - 会员卡类型表 +- `member_card_record` - 会员持卡记录表 +- `member_card_transactions` - 交易流水表 +- `refund_application` - 退款申请表 + +包含完整的索引和注释。 + +### 6. pom.xml 依赖更新 ✅ +在 `gym-member/pom.xml` 中添加了 Redis 响应式支持: +```xml + + org.springframework.boot + spring-boot-starter-data-redis-reactive + +``` + +## 待完成的工作 + +### 需要手动复制的文件(约30个) + +以下文件需要从 `gym-member-card` 复制到 `gym-member/card`,并修改包名: + +#### Handler 层(8个文件) +源路径:`gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/handler/` +目标路径:`gym-member/src/main/java/cn/novalon/gym/manage/member/card/handler/` + +- DistributedLockService.java +- ExpirationReminderService.java +- MemberCardHandler.java +- MemberCardRecordHandler.java +- MemberCardScheduledHandler.java +- MemberCardStateMachine.java +- MemberCardTransactionHandler.java +- RefundSagaHandler.java + +#### Service 接口层(4个文件) +源路径:`gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/sevice/` +目标路径:`gym-member/src/main/java/cn/novalon/gym/manage/member/card/service/` + +注意:目录名从 `sevice`(拼写错误)改为 `service` + +- IMemberCardService.java +- IMemberCardRecordService.java +- IMemberCardTransactionsService.java +- IRefundApplicationService.java + +#### Service 实现层(4个文件) +源路径:`gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/sevice/impl/` +目标路径:`gym-member/src/main/java/cn/novalon/gym/manage/member/card/service/impl/` + +- MemberCardServiceImpl.java +- MemberCardRecordServiceImpl.java +- MemberCardTransactionsServiceImpl.java +- RefundApplicationServiceImpl.java + +#### Util 工具类(1个文件) +源路径:`gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/util/` +目标路径:`gym-member/src/main/java/cn/novalon/gym/manage/member/card/util/` + +- BeanConvertUtil.java + +### 批量替换规则 + +复制每个文件后,需要进行以下文本替换: + +1. **包名替换:** + ``` + package cn.novalon.gym.manage.gymmembercard -> package cn.novalon.gym.manage.member.card + ``` + +2. **导入语句替换:** + ``` + import cn.novalon.gym.manage.gymmembercard -> import cn.novalon.gym.manage.member.card + ``` + +3. **BaseEntity 导入替换:** + ``` + import cn.novalon.gym.manage.db.entity.BaseEntity -> import cn.novalon.gym.manage.member.entity.BaseEntity + ``` + +4. **BaseDomain 处理(如果存在):** + ``` + import cn.novalon.gym.manage.sys.core.domain.BaseDomain -> (注释掉或删除) + ``` + +5. **Repository 引用更新(如果需要):** + ``` + IMemberCardRepository -> MemberCardRepository + IMemberCardRecordRepository -> MemberCardRecordRepository + IMemberCardTransactionsRepository -> MemberCardTransactionRepository + IRefundApplicationRepository -> RefundApplicationRepository + ``` + +## 验证步骤 + +完成文件复制后,执行以下验证: + +1. **编译验证:** + ```bash + cd gym-manage-api/gym-member + mvn clean compile + ``` + +2. **检查是否有编译错误:** + - 修复任何缺失的导入 + - 确认所有依赖类都已正确迁移 + - 检查枚举引用是否正确 + +3. **运行测试(如果有):** + ```bash + mvn test + ``` + +## 注意事项 + +1. **包名一致性:** 确保所有文件中的包声明和导入语句都正确更新 +2. **依赖注入:** Spring 的 `@Autowired` 或构造函数注入应自动工作,因为使用了相同的注解 +3. **数据库兼容性:** schema.sql 已追加新表,不会影响现有表 +4. **Redis 配置:** 确保 application.yml 中有 Redis 配置(如果需要分布式锁功能) +5. **定时任务:** MemberCardScheduledHandler 和 ExpirationReminderService 可能需要启用 `@EnableScheduling` + +## 自动化脚本 + +项目根目录下提供了两个辅助脚本: +- `gym-member-card/migrate_to_member.py` - Python 迁移脚本 +- `gym-member-card/migrate-to-member.ps1` - PowerShell 迁移脚本 + +可以运行这些脚本来自动完成文件复制和包名替换。 + +--- + +**生成时间:** 2026-05-27 +**合并状态:** 基础架构完成,待复制业务逻辑文件 diff --git a/gym-manage-api/gym-member/MEMBER_USER_TABLE_SIMPLE.sql b/gym-manage-api/gym-member/MEMBER_USER_TABLE_SIMPLE.sql new file mode 100644 index 0000000..aef3540 --- /dev/null +++ b/gym-manage-api/gym-member/MEMBER_USER_TABLE_SIMPLE.sql @@ -0,0 +1,58 @@ +-- ============================================ +-- member_user 表 - 简洁版建表语句 +-- ============================================ +-- 用途:直接复制执行,快速创建会员表 +-- ============================================ + +CREATE TABLE IF NOT EXISTS member_user ( + -- 主键和基础字段 + id BIGSERIAL PRIMARY KEY, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + + -- 会员核心字段 + member_no VARCHAR(50) NOT NULL UNIQUE, + nickname VARCHAR(100), + phone VARCHAR(255), + gender INTEGER DEFAULT 0, + birthday TIMESTAMP, + address VARCHAR(500), + avatar VARCHAR(500), + subscribed BOOLEAN DEFAULT FALSE, + last_login_at TIMESTAMP, + + -- 微信相关字段 + union_id VARCHAR(100), + miniapp_open_id VARCHAR(100), + official_open_id VARCHAR(100), + + -- 软删除字段 + is_deleted BOOLEAN DEFAULT FALSE +); + +-- 创建索引 +CREATE UNIQUE INDEX IF NOT EXISTS idx_member_user_member_no ON member_user(member_no); +CREATE INDEX IF NOT EXISTS idx_member_user_union_id ON member_user(union_id); +CREATE INDEX IF NOT EXISTS idx_member_user_miniapp_openid ON member_user(miniapp_open_id); +CREATE INDEX IF NOT EXISTS idx_member_user_official_openid ON member_user(official_open_id); +CREATE INDEX IF NOT EXISTS idx_member_user_phone ON member_user(phone); +CREATE INDEX IF NOT EXISTS idx_member_user_is_deleted ON member_user(is_deleted); + +-- 添加注释 +COMMENT ON TABLE member_user IS '会员表'; +COMMENT ON COLUMN member_user.id IS '主键ID'; +COMMENT ON COLUMN member_user.created_at IS '创建时间'; +COMMENT ON COLUMN member_user.updated_at IS '更新时间'; +COMMENT ON COLUMN member_user.member_no IS '会员编号(唯一)'; +COMMENT ON COLUMN member_user.nickname IS '昵称'; +COMMENT ON COLUMN member_user.phone IS '手机号(AES加密存储)'; +COMMENT ON COLUMN member_user.gender IS '性别:0-未知,1-男,2-女'; +COMMENT ON COLUMN member_user.birthday IS '生日'; +COMMENT ON COLUMN member_user.address IS '地址'; +COMMENT ON COLUMN member_user.avatar IS '头像URL'; +COMMENT ON COLUMN member_user.subscribed IS '是否关注服务号'; +COMMENT ON COLUMN member_user.last_login_at IS '最后登录时间'; +COMMENT ON COLUMN member_user.union_id IS '微信UnionID(跨应用唯一标识)'; +COMMENT ON COLUMN member_user.miniapp_open_id IS '小程序OpenID'; +COMMENT ON COLUMN member_user.official_open_id IS '服务号OpenID'; +COMMENT ON COLUMN member_user.is_deleted IS '是否删除(软删除标记)'; diff --git a/gym-manage-api/gym-member/pom.xml b/gym-manage-api/gym-member/pom.xml new file mode 100644 index 0000000..f62cbb0 --- /dev/null +++ b/gym-manage-api/gym-member/pom.xml @@ -0,0 +1,250 @@ + + + 4.0.0 + + + cn.novalon.gym.manage + gym-manage-api + 1.0.0 + + + gym-member + jar + + Gym Member + Member Management Module - Frontend User Services + + + + cn.novalon.gym.manage + manage-common + ${project.version} + + + cn.novalon.gym.manage + manage-db + ${project.version} + + + cn.novalon.gym.manage + manage-sys + ${project.version} + + + org.springframework.boot + spring-boot-starter-webflux + + + org.springframework.boot + spring-boot-starter-aop + + + org.springdoc + springdoc-openapi-starter-webflux-ui + + + org.springframework.boot + spring-boot-starter-security + + + org.springframework.data + spring-data-commons + + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.security + spring-security-test + test + + + io.projectreactor + reactor-test + test + + + io.github.resilience4j + resilience4j-spring-boot3 + + + io.github.resilience4j + resilience4j-reactor + + + org.testcontainers + testcontainers + 1.21.4 + test + + + org.testcontainers + postgresql + 1.21.4 + test + + + org.testcontainers + junit-jupiter + 1.21.4 + test + + + com.h2database + h2 + test + + + io.r2dbc + r2dbc-h2 + test + + + org.postgresql + r2dbc-postgresql + test + + + + com.github.binarywang + weixin-java-miniapp + 4.6.0 + + + com.github.binarywang + weixin-java-mp + 4.6.0 + + + cn.hutool + hutool-all + 5.8.25 + + + org.springframework.boot + spring-boot-starter-data-elasticsearch + + + + org.springframework.boot + spring-boot-starter-data-redis-reactive + + + + + + + org.apache.maven.plugins + maven-jar-plugin + 3.4.2 + + + default-jar + package + + jar + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.11.0 + + 21 + 21 + + + org.mapstruct + mapstruct-processor + 1.5.5.Final + + + org.projectlombok + lombok + ${lombok.version} + + + org.projectlombok + lombok-mapstruct-binding + 0.2.0 + + + + + + org.jacoco + jacoco-maven-plugin + 0.8.12 + + + prepare-agent + + prepare-agent + + + + report + verify + + report + + + + check + verify + + check + + + + + BUNDLE + + + INSTRUCTION + COVEREDRATIO + 0.60 + + + + + + + + + + com.github.spotbugs + spotbugs-maven-plugin + 4.8.6.0 + + + com.github.spotbugs + spotbugs + 4.8.6 + + + + + spotbugs-check + verify + + check + + + + + Max + High + true + spotbugs-exclude.xml + + + + + diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/card/entity/MemberCard.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/card/entity/MemberCard.java new file mode 100644 index 0000000..684d401 --- /dev/null +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/card/entity/MemberCard.java @@ -0,0 +1,64 @@ +package cn.novalon.gym.manage.member.card.entity; + +import cn.novalon.gym.manage.member.entity.BaseEntity; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import org.springframework.data.relational.core.mapping.Column; +import org.springframework.data.relational.core.mapping.Table; + +import java.time.LocalDateTime; + +/** + * 会员卡类型实体 - 对应 member_card 表 + * + * @author 付嘉 + * @date 2026-05-27 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode(callSuper = true) +@Table("member_card") +public class MemberCard extends BaseEntity { + + // 会员卡ID + @Column("member_card_id") + private Long memberCardId; + + // 会员卡名称 + @Column("member_card_name") + private String memberCardName; + + // 会员卡类型:TIME_CARD-时长卡, COUNT_CARD-次卡, STORED_VALUE_CARD-储值卡 + @Column("member_card_type") + private String memberCardType; + + // 会员卡价格 + @Column("member_card_price") + private Double memberCardPrice; + + // 有效天数(时长卡用) + @Column("member_card_validity_days") + private Integer memberCardValidityDays; + + // 总次数(次卡用) + @Column("member_card_total_times") + private Integer memberCardTotalTimes; + + // 面额(储值卡用) + @Column("member_card_amount") + private Double memberCardAmount; + + // 状态:0-下架, 1-上架 + @Column("member_card_status") + private Integer memberCardStatus; + + // 扩展配置(JSON格式) + @Column("extra_config") + private String extraConfig; + +} diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/card/entity/MemberCardRecord.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/card/entity/MemberCardRecord.java new file mode 100644 index 0000000..98d5e4b --- /dev/null +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/card/entity/MemberCardRecord.java @@ -0,0 +1,73 @@ +package cn.novalon.gym.manage.member.card.entity; + +import cn.novalon.gym.manage.member.card.enums.MemberCardRecordStatus; +import cn.novalon.gym.manage.member.entity.BaseEntity; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import org.springframework.data.relational.core.mapping.Column; +import org.springframework.data.relational.core.mapping.Table; + +import java.time.LocalDateTime; + +/** + * 会员卡记录实体(会员持有的卡)- 对应 member_card_record 表 + * + * @author 付嘉 + * @date 2026-05-27 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode(callSuper = true) +@Table("member_card_record") +public class MemberCardRecord extends BaseEntity { + + // 会员持有卡ID + @Column("member_card_record_id") + private Long memberCardRecordId; + + // 会员ID + @Column("member_id") + private Long memberId; + + // 关联会员卡ID + @Column("member_card_id") + private Long memberCardId; + + // 状态:ACTIVE-有效, USED_UP-用完, EXPIRED-过期, REFUNDED-已退款 + @Column("status") + private MemberCardRecordStatus status; + + // 剩余次数 + @Column("remaining_times") + private Integer remainingTimes; + + // 剩余金额 + @Column("remaining_amount") + private Double remainingAmount; + + // 到期时间 + @Column("expire_time") + private LocalDateTime expireTime; + + // 来源订单ID + @Column("source_order_id") + private Long sourceOrderId; + + // 购买时间 + @Column("purchase_time") + private LocalDateTime purchaseTime; + + // 乐观锁版本号 + @Column("version") + private Integer version; + + // 卡片组成(JSON格式,用于组合卡) + @Column("card_composition") + private String cardComposition; + +} diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/card/entity/MemberCardTransaction.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/card/entity/MemberCardTransaction.java new file mode 100644 index 0000000..29abd08 --- /dev/null +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/card/entity/MemberCardTransaction.java @@ -0,0 +1,78 @@ +package cn.novalon.gym.manage.member.card.entity; + +import cn.novalon.gym.manage.member.entity.BaseEntity; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import org.springframework.data.relational.core.mapping.Column; +import org.springframework.data.relational.core.mapping.Table; + +/** + * 会员卡交易流水实体 - 对应 member_card_transactions 表 + * + * @author 付嘉 + * @date 2026-05-27 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode(callSuper = true) +@Table("member_card_transactions") +public class MemberCardTransaction extends BaseEntity { + + // 交易ID + @Column("member_card_record_id") + private Long memberCardRecordId; + + // 会员ID + @Column("member_id") + private Long memberId; + + // 会员卡ID + @Column("member_card_id") + private Long memberCardId; + + // 操作类型:PURCHASE-购买, DEDUCT-扣次/扣费, RENEW-续费, REFUND-退款, EXPIRE-过期 + @Column("operation_type") + private String 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; + + // 关联业务类型:GROUP_CLASS-团课, PT_CLASS-私教, CHECK_IN-签到 + @Column("related_biz_type") + private String relatedBizType; + + // 来源订单ID + @Column("source_order_id") + private Long sourceOrderId; + + // 备注 + @Column("remark") + private String remark; + + // 是否已归档 + @Column("is_archived") + private Boolean isArchived; + + // 归档时间 + @Column("archived_at") + private java.time.LocalDateTime archivedAt; + +} diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/card/entity/RefundApplication.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/card/entity/RefundApplication.java new file mode 100644 index 0000000..88547bc --- /dev/null +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/card/entity/RefundApplication.java @@ -0,0 +1,66 @@ +package cn.novalon.gym.manage.member.card.entity; + +import cn.novalon.gym.manage.member.card.enums.RefundStatus; +import cn.novalon.gym.manage.member.entity.BaseEntity; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import org.springframework.data.relational.core.mapping.Column; +import org.springframework.data.relational.core.mapping.Table; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +/** + * 退款申请实体 - 对应 refund_application 表 + * + * @author 付嘉 + * @date 2026-05-27 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode(callSuper = true) +@Table("refund_application") +public class RefundApplication extends BaseEntity { + + // 会员卡记录ID + @Column("record_id") + private Long recordId; + + // 会员ID + @Column("member_id") + private Long memberId; + + // 状态:PENDING-待审核, APPROVED-已批准, REJECTED-已拒绝, PROCESSING-处理中, SUCCESS-成功, FAILED-失败 + @Column("status") + private RefundStatus status; + + // 退款原因 + @Column("reason") + private String reason; + + // 申请时间 + @Column("apply_time") + private LocalDateTime applyTime; + + // 审核时间 + @Column("audit_time") + private LocalDateTime auditTime; + + // 审核人ID + @Column("auditor_id") + private Long auditorId; + + // 审核备注 + @Column("audit_remark") + private String auditRemark; + + // 退款金额 + @Column("refund_amount") + private BigDecimal refundAmount; + +} diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/card/enums/BizType.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/card/enums/BizType.java new file mode 100644 index 0000000..b53f839 --- /dev/null +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/card/enums/BizType.java @@ -0,0 +1,24 @@ +package cn.novalon.gym.manage.member.card.enums; + +/** + * 会员卡流水关联业务类型枚举 + * + * @author 付嘉 + * @date 2026-05-27 + */ +public enum BizType { + + GROUP_CLASS("团课"), + PT_CLASS("私教"), + CHECK_IN("签到"); + + private final String desc; + + BizType(String desc) { + this.desc = desc; + } + + public String getDesc() { + return desc; + } +} diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/card/enums/CardEvent.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/card/enums/CardEvent.java new file mode 100644 index 0000000..db3f8ee --- /dev/null +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/card/enums/CardEvent.java @@ -0,0 +1,27 @@ +package cn.novalon.gym.manage.member.card.enums; + +/** + * 会员卡状态机事件枚举 + * + * @author 付嘉 + * @date 2026-05-27 + */ +public enum CardEvent { + + ACTIVATE("激活卡片"), + USE("使用卡片"), + RENEW("续费"), + EXPIRE("过期"), + REFUND("退款"), + DISABLE("禁用"); + + private final String desc; + + CardEvent(String desc) { + this.desc = desc; + } + + public String getDesc() { + return desc; + } +} diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/card/enums/MemberCardRecordStatus.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/card/enums/MemberCardRecordStatus.java new file mode 100644 index 0000000..fe27fe7 --- /dev/null +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/card/enums/MemberCardRecordStatus.java @@ -0,0 +1,25 @@ +package cn.novalon.gym.manage.member.card.enums; + +/** + * 会员卡记录状态枚举 + * + * @author 付嘉 + * @date 2026-05-27 + */ +public enum MemberCardRecordStatus { + + ACTIVE("有效"), + USED_UP("用完"), + EXPIRED("过期"), + REFUNDED("已退款"); + + private final String desc; + + MemberCardRecordStatus(String desc) { + this.desc = desc; + } + + public String getDesc() { + return desc; + } +} 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/src/main/java/cn/novalon/gym/manage/member/card/enums/MemberCardType.java similarity index 50% rename from gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/enums/MemberCardType.java rename to gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/card/enums/MemberCardType.java index decb62a..207d1ff 100644 --- a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/enums/MemberCardType.java +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/card/enums/MemberCardType.java @@ -1,16 +1,15 @@ -package cn.novalon.gym.manage.gymmembercard.enums; +package cn.novalon.gym.manage.member.card.enums; -import io.swagger.v3.oas.annotations.media.Schema; - -@Schema(description = "会员卡类型枚举") +/** + * 会员卡类型枚举 + * + * @author 付嘉 + * @date 2026-05-27 + */ public enum MemberCardType { - @Schema(description = "时长卡") + TIME_CARD("时长卡"), - - @Schema(description = "次卡") COUNT_CARD("次卡"), - - @Schema(description = "储值卡") STORED_VALUE_CARD("储值卡"); private final String desc; @@ -22,4 +21,4 @@ public enum MemberCardType { public String getDesc() { return desc; } -} \ No newline at end of file +} diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/card/enums/RefundStatus.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/card/enums/RefundStatus.java new file mode 100644 index 0000000..c350f31 --- /dev/null +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/card/enums/RefundStatus.java @@ -0,0 +1,27 @@ +package cn.novalon.gym.manage.member.card.enums; + +/** + * 退款申请状态枚举 + * + * @author 付嘉 + * @date 2026-05-27 + */ +public enum RefundStatus { + + PENDING("待审核"), + APPROVED("已批准"), + REJECTED("已拒绝"), + PROCESSING("处理中"), + SUCCESS("成功"), + FAILED("失败"); + + private final String desc; + + RefundStatus(String desc) { + this.desc = desc; + } + + public String getDesc() { + return desc; + } +} diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/card/enums/TransactionType.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/card/enums/TransactionType.java new file mode 100644 index 0000000..5ae8c94 --- /dev/null +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/card/enums/TransactionType.java @@ -0,0 +1,26 @@ +package cn.novalon.gym.manage.member.card.enums; + +/** + * 会员卡流水操作类型枚举 + * + * @author 付嘉 + * @date 2026-05-27 + */ +public enum TransactionType { + + PURCHASE("购买"), + DEDUCT("扣次/扣费"), + RENEW("续费"), + REFUND("退款"), + EXPIRE("过期"); + + private final String desc; + + TransactionType(String desc) { + this.desc = desc; + } + + public String getDesc() { + return desc; + } +} 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/src/main/java/cn/novalon/gym/manage/member/card/handler/DistributedLockService.java similarity index 93% rename from gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/handler/DistributedLockService.java rename to gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/card/handler/DistributedLockService.java index 08f57ae..c7098af 100644 --- a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/handler/DistributedLockService.java +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/card/handler/DistributedLockService.java @@ -1,4 +1,4 @@ -package cn.novalon.gym.manage.gymmembercard.handler; +package cn.novalon.gym.manage.member.card.handler; import org.springframework.stereotype.Component; import reactor.core.publisher.Mono; @@ -8,9 +8,9 @@ import java.util.concurrent.locks.ReentrantLock; /** * 分布式锁服务(简化版,使用本地锁) - * - * @author shizhounian - * @date 2026-05-23 + * + * @author 付嘉 + * @date 2026-05-27 */ @Component public class DistributedLockService { 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/src/main/java/cn/novalon/gym/manage/member/card/handler/ExpirationReminderService.java similarity index 96% rename from gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/handler/ExpirationReminderService.java rename to gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/card/handler/ExpirationReminderService.java index 3050327..c063de9 100644 --- a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/handler/ExpirationReminderService.java +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/card/handler/ExpirationReminderService.java @@ -1,6 +1,6 @@ -package cn.novalon.gym.manage.gymmembercard.handler; +package cn.novalon.gym.manage.member.card.handler; -import cn.novalon.gym.manage.gymmembercard.domain.MemberCardRecord; +import cn.novalon.gym.manage.member.card.entity.MemberCardRecord; import com.fasterxml.jackson.databind.ObjectMapper; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -16,6 +16,12 @@ import java.time.Duration; import java.time.LocalDateTime; import java.util.UUID; +/** + * 会员卡到期提醒服务 + * + * @author 付嘉 + * @date 2026-05-27 + */ @Slf4j @Component @RequiredArgsConstructor @@ -84,7 +90,7 @@ public class ExpirationReminderService { return reminderFlux.then(); } - /** + /** * 定时任务:每分钟扫描到期的提醒并发送 */ @Scheduled(fixedRate = 60000) 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/src/main/java/cn/novalon/gym/manage/member/card/handler/MemberCardHandler.java similarity index 95% rename from gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/handler/MemberCardHandler.java rename to gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/card/handler/MemberCardHandler.java index 1744819..b697999 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/src/main/java/cn/novalon/gym/manage/member/card/handler/MemberCardHandler.java @@ -1,8 +1,8 @@ -package cn.novalon.gym.manage.gymmembercard.handler; +package cn.novalon.gym.manage.member.card.handler; -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 cn.novalon.gym.manage.member.card.entity.MemberCard; +import cn.novalon.gym.manage.member.card.entity.MemberCardRecord; +import cn.novalon.gym.manage.member.card.service.IMemberCardService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.extern.slf4j.Slf4j; @@ -15,9 +15,9 @@ import reactor.core.publisher.Mono; /** * 会员卡管理处理器 - * - * @author shizhounian - * @date 2026-05-23 + * + * @author 付嘉 + * @date 2026-05-27 */ @Slf4j @Component 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/src/main/java/cn/novalon/gym/manage/member/card/handler/MemberCardRecordHandler.java similarity index 93% rename from gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/handler/MemberCardRecordHandler.java rename to gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/card/handler/MemberCardRecordHandler.java index 77ee607..7928a7c 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/src/main/java/cn/novalon/gym/manage/member/card/handler/MemberCardRecordHandler.java @@ -1,8 +1,8 @@ -package cn.novalon.gym.manage.gymmembercard.handler; +package cn.novalon.gym.manage.member.card.handler; -import cn.novalon.gym.manage.gymmembercard.domain.MemberCardRecord; -import cn.novalon.gym.manage.gymmembercard.sevice.IMemberCardRecordService; -import cn.novalon.gym.manage.gymmembercard.sevice.IMemberCardService; +import cn.novalon.gym.manage.member.card.entity.MemberCardRecord; +import cn.novalon.gym.manage.member.card.service.IMemberCardRecordService; +import cn.novalon.gym.manage.member.card.service.IMemberCardService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.Data; @@ -11,6 +11,12 @@ import org.springframework.web.reactive.function.server.ServerRequest; import org.springframework.web.reactive.function.server.ServerResponse; import reactor.core.publisher.Mono; +/** + * 会员卡记录管理处理器 + * + * @author 付嘉 + * @date 2026-05-27 + */ @Component @Tag(name = "会员卡记录管理", description = "会员卡购买、续费、使用、退款等核心业务") public class MemberCardRecordHandler { @@ -109,4 +115,4 @@ public class MemberCardRecordHandler { 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/src/main/java/cn/novalon/gym/manage/member/card/handler/MemberCardScheduledHandler.java similarity index 86% rename from gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/handler/MemberCardScheduledHandler.java rename to gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/card/handler/MemberCardScheduledHandler.java index de6108b..0c4181f 100644 --- a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/handler/MemberCardScheduledHandler.java +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/card/handler/MemberCardScheduledHandler.java @@ -1,9 +1,9 @@ -package cn.novalon.gym.manage.gymmembercard.handler; +package cn.novalon.gym.manage.member.card.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 cn.novalon.gym.manage.member.card.entity.MemberCardRecord; +import cn.novalon.gym.manage.member.card.enums.CardEvent; +import cn.novalon.gym.manage.member.card.enums.MemberCardRecordStatus; +import cn.novalon.gym.manage.member.card.repository.MemberCardRecordRepository; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.scheduling.annotation.Scheduled; @@ -12,17 +12,23 @@ import reactor.core.publisher.Mono; import java.time.LocalDateTime; +/** + * 会员卡定时任务处理器 + * + * @author 付嘉 + * @date 2026-05-27 + */ @Slf4j @Component @RequiredArgsConstructor public class MemberCardScheduledHandler { - private final IMemberCardRecordRepository recordRepository; + private final MemberCardRecordRepository recordRepository; private final ExpirationReminderService expirationReminderService; private final MemberCardStateMachine stateMachine; private final DistributedLockService distributedLockService; - /** + /** * 每日凌晨2点检查过期会员卡 */ @Scheduled(cron = "0 0 2 * * ?") @@ -38,7 +44,7 @@ public class MemberCardScheduledHandler { recordRepository.findActiveRecords() .filter(record -> record.getExpireTime() != null && record.getExpireTime().isBefore(now)) .flatMap(record -> - stateMachine.transition(record.getStatus(), MemberCardEvent.EXPIRE) + stateMachine.transition(record.getStatus(), CardEvent.EXPIRE) .flatMap(newState -> { record.setStatus(newState); return recordRepository.save(record); @@ -55,9 +61,8 @@ public class MemberCardScheduledHandler { ).subscribe(); } - /** + /** * 每日凌晨3点检查是否有遗漏的到期提醒(兜底机制) - * 主要依赖购卡/续费时的主动调用和每分钟扫描任务,此任务仅用于异常恢复 */ @Scheduled(cron = "0 0 3 * * ?") public void checkAndSendExpirationReminders() { 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/src/main/java/cn/novalon/gym/manage/member/card/handler/MemberCardStateMachine.java similarity index 56% rename from gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/handler/MemberCardStateMachine.java rename to gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/card/handler/MemberCardStateMachine.java index e48386e..94c8738 100644 --- a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/handler/MemberCardStateMachine.java +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/card/handler/MemberCardStateMachine.java @@ -1,8 +1,8 @@ -package cn.novalon.gym.manage.gymmembercard.handler; +package cn.novalon.gym.manage.member.card.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.member.card.entity.MemberCardRecord; +import cn.novalon.gym.manage.member.card.enums.CardEvent; +import cn.novalon.gym.manage.member.card.enums.MemberCardRecordStatus; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; import reactor.core.publisher.Mono; @@ -10,36 +10,42 @@ import reactor.core.publisher.Mono; import java.util.HashMap; import java.util.Map; +/** + * 会员卡状态机处理器 + * + * @author 付嘉 + * @date 2026-05-27 + */ @Slf4j @Component public class MemberCardStateMachine { - private final Map> stateTransitionMap; + private final Map> stateTransitionMap; public MemberCardStateMachine() { this.stateTransitionMap = buildStateTransitionMap(); } - private Map> buildStateTransitionMap() { - Map> map = new HashMap<>(); + 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 activeTransitions = new HashMap<>(); + activeTransitions.put(CardEvent.USE, MemberCardRecordStatus.ACTIVE); + activeTransitions.put(CardEvent.RENEW, MemberCardRecordStatus.ACTIVE); + activeTransitions.put(CardEvent.EXPIRE, MemberCardRecordStatus.EXPIRED); + activeTransitions.put(CardEvent.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 usedUpTransitions = new HashMap<>(); + usedUpTransitions.put(CardEvent.RENEW, MemberCardRecordStatus.ACTIVE); + usedUpTransitions.put(CardEvent.REFUND, MemberCardRecordStatus.REFUNDED); map.put(MemberCardRecordStatus.USED_UP, usedUpTransitions); // EXPIRED 状态可以转换的事件 - Map expiredTransitions = new HashMap<>(); - expiredTransitions.put(MemberCardEvent.RENEW, MemberCardRecordStatus.ACTIVE); + Map expiredTransitions = new HashMap<>(); + expiredTransitions.put(CardEvent.RENEW, MemberCardRecordStatus.ACTIVE); map.put(MemberCardRecordStatus.EXPIRED, expiredTransitions); // REFUNDED 状态是终态,不允许任何转换 @@ -47,9 +53,9 @@ public class MemberCardStateMachine { return map; } - public Mono canTransition(MemberCardRecordStatus currentState, MemberCardEvent event) { + public Mono canTransition(MemberCardRecordStatus currentState, CardEvent event) { return Mono.fromSupplier(() -> { - Map transitions = stateTransitionMap.get(currentState); + Map transitions = stateTransitionMap.get(currentState); if (transitions == null) { return false; } @@ -57,9 +63,9 @@ public class MemberCardStateMachine { }); } - public Mono transition(MemberCardRecordStatus currentState, MemberCardEvent event) { + public Mono transition(MemberCardRecordStatus currentState, CardEvent event) { return Mono.fromSupplier(() -> { - Map transitions = stateTransitionMap.get(currentState); + Map transitions = stateTransitionMap.get(currentState); if (transitions == null || !transitions.containsKey(event)) { log.error("Invalid state transition: currentState={}, event={}", currentState, event); throw new IllegalStateException( @@ -71,7 +77,7 @@ public class MemberCardStateMachine { }); } - public Mono validateTransition(MemberCardRecord card, MemberCardEvent event) { + public Mono validateTransition(MemberCardRecord card, CardEvent event) { return canTransition(card.getStatus(), event) .flatMap(canTransition -> { if (!canTransition) { diff --git a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/handler/MemberCardTransactionHandler.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/card/handler/MemberCardTransactionHandler.java similarity index 76% rename from gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/handler/MemberCardTransactionHandler.java rename to gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/card/handler/MemberCardTransactionHandler.java index ead4283..771248a 100644 --- a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/handler/MemberCardTransactionHandler.java +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/card/handler/MemberCardTransactionHandler.java @@ -1,9 +1,9 @@ -package cn.novalon.gym.manage.gymmembercard.handler; +package cn.novalon.gym.manage.member.card.handler; import cn.hutool.db.PageResult; -import cn.novalon.gym.manage.gymmembercard.domain.MemberCardTransactions; -import cn.novalon.gym.manage.gymmembercard.enums.MemberCardTransactionsAction; -import cn.novalon.gym.manage.gymmembercard.sevice.IMemberCardTransactionsService; +import cn.novalon.gym.manage.member.card.entity.MemberCardTransaction; +import cn.novalon.gym.manage.member.card.enums.TransactionType; +import cn.novalon.gym.manage.member.card.service.IMemberCardTransactionService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Validator; @@ -19,20 +19,22 @@ import reactor.core.publisher.Mono; import java.time.LocalDateTime; import java.util.List; -/* - *@Author:shizhounian - *@Date:2026/5/17-05 20:10:22 +/** + * 会员卡流水管理处理器 + * + * @author 付嘉 + * @date 2026-05-27 */ @Component @Tag(name = "会员卡流水管理", description = "会员卡流水相关操作") public class MemberCardTransactionHandler { - private final IMemberCardTransactionsService memberCardTransactionsService; + private final IMemberCardTransactionService memberCardTransactionService; private final Validator validator; - public MemberCardTransactionHandler(IMemberCardTransactionsService memberCardTransactionsService, + public MemberCardTransactionHandler(IMemberCardTransactionService memberCardTransactionService, Validator validator) { - this.memberCardTransactionsService = memberCardTransactionsService; + this.memberCardTransactionService = memberCardTransactionService; this.validator = validator; } @@ -41,8 +43,8 @@ public class MemberCardTransactionHandler { */ @Operation(summary = "插入流水记录", description = "购卡、扣次、续费、退款、过期时插入流水") public Mono insertTransaction(ServerRequest request) { - return request.bodyToMono(MemberCardTransactions.class) - .flatMap(memberCardTransactionsService::insertTransaction) + return request.bodyToMono(MemberCardTransaction.class) + .flatMap(memberCardTransactionService::insertTransaction) .flatMap(record -> ServerResponse.ok().bodyValue(record)); } @@ -61,8 +63,8 @@ public class MemberCardTransactionHandler { Pageable pageable = PageRequest.of(page, size, Sort.by("created_at").descending()); return ServerResponse.ok() - .body(memberCardTransactionsService.findByMemberIdAndTimeRange( - memberId, startTime, endTime, pageable), MemberCardTransactions.class); + .body(memberCardTransactionService.findByMemberIdAndTimeRange( + memberId, startTime, endTime, pageable), MemberCardTransaction.class); } /** @@ -72,25 +74,25 @@ public class MemberCardTransactionHandler { public Mono getTransactionsWithConditions(ServerRequest request) { Long memberId = request.queryParam("memberId").map(Long::parseLong).orElse(null); Long memberCardId = request.queryParam("memberCardId").map(Long::parseLong).orElse(null); - MemberCardTransactionsAction operationType = request.queryParam("operationType") - .map(s -> MemberCardTransactionsAction.valueOf(s.toUpperCase())).orElse(null); + TransactionType operationType = request.queryParam("operationType") + .map(s -> TransactionType.valueOf(s.toUpperCase())).orElse(null); LocalDateTime startTime = request.queryParam("startTime").map(LocalDateTime::parse).orElse(null); LocalDateTime endTime = request.queryParam("endTime").map(LocalDateTime::parse).orElse(null); 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("created_at").descending()); - Mono countMono = memberCardTransactionsService.countWithConditions( + Mono countMono = memberCardTransactionService.countWithConditions( memberId, memberCardId, operationType, startTime, endTime); - Flux flux = memberCardTransactionsService.findWithConditions( + Flux flux = memberCardTransactionService.findWithConditions( memberId, memberCardId, operationType, startTime, endTime, pageable); return Mono.zip(countMono, flux.collectList()) .flatMap(tuple -> { Long total = tuple.getT1(); - List list = tuple.getT2(); + List list = tuple.getT2(); // 构造 PageResult,内部自动计算总页数 - PageResult result = new PageResult<>(page, size, total.intValue()); + PageResult result = new PageResult<>(page, size, total.intValue()); result.addAll(list); return ServerResponse.ok().bodyValue(result); }); @@ -103,8 +105,8 @@ public class MemberCardTransactionHandler { public Mono getTransactionsByCardId(ServerRequest request) { Long memberCardId = Long.parseLong(request.pathVariable("cardId")); return ServerResponse.ok() - .body(memberCardTransactionsService.findByMemberCardId(memberCardId), - MemberCardTransactions.class); + .body(memberCardTransactionService.findByMemberCardId(memberCardId), + MemberCardTransaction.class); } /** @@ -117,7 +119,7 @@ public class MemberCardTransactionHandler { .map(LocalDateTime::parse).orElse(LocalDateTime.now().minusMonths(1)); LocalDateTime endTime = request.queryParam("endTime") .map(LocalDateTime::parse).orElse(LocalDateTime.now()); - return memberCardTransactionsService.sumDeductCountByCardId(memberCardId, startTime, endTime) + return memberCardTransactionService.sumDeductCountByCardId(memberCardId, startTime, endTime) .flatMap(count -> ServerResponse.ok().bodyValue(count)); } @@ -130,7 +132,7 @@ public class MemberCardTransactionHandler { .map(LocalDateTime::parse).orElse(LocalDateTime.now().minusMonths(1)); LocalDateTime endTime = request.queryParam("endTime") .map(LocalDateTime::parse).orElse(LocalDateTime.now()); - return memberCardTransactionsService.sumRenewAmountByTimeRange(startTime, endTime) + return memberCardTransactionService.sumRenewAmountByTimeRange(startTime, endTime) .flatMap(amount -> ServerResponse.ok().bodyValue(amount)); } @@ -144,7 +146,7 @@ public class MemberCardTransactionHandler { .map(LocalDateTime::parse).orElse(LocalDateTime.now().minusMonths(1)); LocalDateTime endTime = request.queryParam("endTime") .map(LocalDateTime::parse).orElse(LocalDateTime.now()); - return memberCardTransactionsService.sumPurchaseAmountByMemberId(memberId, startTime, endTime) + return memberCardTransactionService.sumPurchaseAmountByMemberId(memberId, startTime, endTime) .flatMap(amount -> ServerResponse.ok().bodyValue(amount)); } -} \ No newline at end of file +} 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/src/main/java/cn/novalon/gym/manage/member/card/handler/RefundSagaHandler.java similarity index 64% rename from gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/handler/RefundSagaHandler.java rename to gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/card/handler/RefundSagaHandler.java index 32c0ca1..fd42204 100644 --- a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/handler/RefundSagaHandler.java +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/card/handler/RefundSagaHandler.java @@ -1,12 +1,12 @@ -package cn.novalon.gym.manage.gymmembercard.handler; +package cn.novalon.gym.manage.member.card.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 cn.novalon.gym.manage.member.card.entity.MemberCardRecord; +import cn.novalon.gym.manage.member.card.entity.MemberCardTransaction; +import cn.novalon.gym.manage.member.card.enums.CardEvent; +import cn.novalon.gym.manage.member.card.enums.MemberCardRecordStatus; +import cn.novalon.gym.manage.member.card.enums.TransactionType; +import cn.novalon.gym.manage.member.card.repository.MemberCardRecordRepository; +import cn.novalon.gym.manage.member.card.service.IMemberCardTransactionService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; @@ -15,19 +15,25 @@ import reactor.core.publisher.Mono; import java.util.ArrayList; import java.util.List; +/** + * 退款 Saga 处理器 + * + * @author 付嘉 + * @date 2026-05-27 + */ @Slf4j @Component @RequiredArgsConstructor public class RefundSagaHandler { - private final IMemberCardRecordRepository recordRepository; - private final IMemberCardTransactionsService transactionsService; + private final MemberCardRecordRepository recordRepository; + private final IMemberCardTransactionService transactionService; 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) + .flatMap(record -> stateMachine.validateTransition(record, CardEvent.REFUND) .then(Mono.defer(() -> doExecuteRefund(recordId, record)))); } @@ -55,7 +61,7 @@ public class RefundSagaHandler { } private Mono updateCardStatus(Long recordId, MemberCardRecordStatus status) { - return recordRepository.updateStatus(recordId, status) + return recordRepository.updateStatus(recordId, status.name()) .flatMap(rows -> { if (rows == 0) { return Mono.error(new RuntimeException("更新会员卡状态失败")); @@ -65,29 +71,31 @@ public class RefundSagaHandler { } 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("会员卡退款"); + MemberCardTransaction transaction = MemberCardTransaction.builder() + .memberId(record.getMemberId()) + .memberCardId(record.getMemberCardId()) + .operationType(TransactionType.REFUND.name()) + .changeAmount(-record.getRemainingTimes()) + .changeBalance(-record.getRemainingAmount()) + .afterRemainingCount(0) + .afterRemainingBalance(0.0) + .remark("会员卡退款") + .build(); - return transactionsService.createTransaction(transaction); + return transactionService.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("退款冲正"); + MemberCardTransaction reversal = MemberCardTransaction.builder() + .memberId(record.getMemberId()) + .memberCardId(record.getMemberCardId()) + .operationType(TransactionType.REFUND.name()) + .changeAmount(record.getRemainingTimes()) + .changeBalance(record.getRemainingAmount()) + .remark("退款冲正") + .build(); - return transactionsService.createTransaction(reversal); + return transactionService.createTransaction(reversal); } private Mono executeSaga(List steps, List rollbackSteps) { @@ -123,4 +131,4 @@ public class RefundSagaHandler { } 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/dao/MemberCardRecordDao.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/card/repository/MemberCardRecordRepository.java similarity index 54% rename from gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/dao/MemberCardRecordDao.java rename to gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/card/repository/MemberCardRecordRepository.java index 728d82c..b18f53c 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/src/main/java/cn/novalon/gym/manage/member/card/repository/MemberCardRecordRepository.java @@ -1,32 +1,39 @@ -package cn.novalon.gym.manage.gymmembercard.dao; +package cn.novalon.gym.manage.member.card.repository; -import cn.novalon.gym.manage.gymmembercard.entity.MemberCardRecordEntity; -import cn.novalon.gym.manage.gymmembercard.enums.MemberCardRecordStatus; +import cn.novalon.gym.manage.member.card.entity.MemberCardRecord; import org.springframework.data.domain.Pageable; import org.springframework.data.r2dbc.repository.Modifying; import org.springframework.data.r2dbc.repository.Query; import org.springframework.data.r2dbc.repository.R2dbcRepository; -import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import java.time.LocalDateTime; +/** + * 会员卡记录 Repository(会员持有的卡) + * + * @author 付嘉 + * @date 2026-05-27 + */ @Repository -public interface MemberCardRecordDao extends R2dbcRepository { +public interface MemberCardRecordRepository extends R2dbcRepository { + /** + * 插入新的激活卡记录 + */ @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()) " + "RETURNING *") - Mono insertActiveRecord(@Param("memberId") Long memberId, - @Param("memberCardId") Long memberCardId, - @Param("expireTime") LocalDateTime expireTime, - @Param("remainingTimes") Integer remainingTimes, - @Param("remainingAmount") Double remainingAmount, - @Param("sourceOrderId") Long sourceOrderId); + Mono insertActiveRecord(Long memberId, Long memberCardId, + LocalDateTime expireTime, Integer remainingTimes, + Double remainingAmount, Long sourceOrderId); + /** + * 扣减使用次数/金额 + */ @Modifying @Query("UPDATE member_card_record SET " + "remaining_times = remaining_times - :deductTimes, " + @@ -36,52 +43,67 @@ public interface MemberCardRecordDao extends R2dbcRepository= :deductTimes " + "AND remaining_amount >= :deductAmount") - Mono deductUsage(@Param("recordId") Long recordId, - @Param("deductTimes") Integer deductTimes, - @Param("deductAmount") Double deductAmount); + Mono deductUsage(Long recordId, Integer deductTimes, Double deductAmount); + /** + * 续费卡片 + */ @Modifying @Query("UPDATE member_card_record SET remaining_times = remaining_times + :addTimes, " + "remaining_amount = remaining_amount + :addAmount, expire_time = :newExpireTime, updated_at = NOW() " + "WHERE member_card_record_id = :recordId AND deleted_at IS NULL") - Mono renewCard(@Param("recordId") Long recordId, - @Param("addTimes") Integer addTimes, - @Param("addAmount") Double addAmount, - @Param("newExpireTime") LocalDateTime newExpireTime); + Mono renewCard(Long recordId, Integer addTimes, Double addAmount, LocalDateTime newExpireTime); + /** + * 更新卡片状态 + */ @Modifying @Query("UPDATE member_card_record SET status = :status, updated_at = NOW() " + "WHERE member_card_record_id = :recordId AND deleted_at IS NULL") - Mono updateStatus(@Param("recordId") Long recordId, - @Param("status") MemberCardRecordStatus status); + Mono updateStatus(Long recordId, String status); + /** + * 查询会员的有效卡片 + */ @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); + Flux findActiveCardsByMemberId(Long memberId); + /** + * 查询会员的所有卡片(分页) + */ @Query("SELECT mcr.* FROM member_card_record mcr " + - "INNER JOIN member m ON mcr.member_id = m.member_id " + + "INNER JOIN member_user m ON mcr.member_id = m.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); + Flux findByMemberId(Long memberId, Pageable pageable); + /** + * 验证次卡是否有足够次数 + */ @Query("SELECT * FROM member_card_record WHERE member_card_record_id = :recordId " + "AND status = 'ACTIVE' AND deleted_at IS NULL " + "AND expire_time > NOW() " + "AND remaining_times >= :requiredTimes") - Mono validateCountCard(@Param("recordId") Long recordId, - @Param("requiredTimes") Integer requiredTimes); + Mono validateCountCard(Long recordId, Integer requiredTimes); + /** + * 验证储值卡是否有足够余额 + */ @Query("SELECT * FROM member_card_record WHERE member_card_record_id = :recordId " + "AND status = 'ACTIVE' AND deleted_at IS NULL " + "AND expire_time > NOW() " + "AND remaining_amount >= :requiredAmount") - Mono validateStoredCard(@Param("recordId") Long recordId, - @Param("requiredAmount") Double requiredAmount); + Mono validateStoredCard(Long recordId, Double requiredAmount); + /** + * 查询已过期的卡片 + */ @Query("SELECT * FROM member_card_record WHERE status = 'ACTIVE' AND expire_time < NOW() AND deleted_at IS NULL LIMIT 500") - Flux findExpiredCards(); + Flux findExpiredCards(); + /** + * 查询所有有效记录 + */ @Query("SELECT * FROM member_card_record WHERE status = 'ACTIVE' AND deleted_at IS NULL") - Flux findActiveRecords(); - -} \ No newline at end of file + Flux findActiveRecords(); +} diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/card/repository/MemberCardRepository.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/card/repository/MemberCardRepository.java new file mode 100644 index 0000000..c45dd9e --- /dev/null +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/card/repository/MemberCardRepository.java @@ -0,0 +1,91 @@ +package cn.novalon.gym.manage.member.card.repository; + +import cn.novalon.gym.manage.member.card.entity.MemberCard; +import org.springframework.data.domain.Pageable; +import org.springframework.data.r2dbc.repository.Modifying; +import org.springframework.data.r2dbc.repository.Query; +import org.springframework.data.r2dbc.repository.R2dbcRepository; +import org.springframework.stereotype.Repository; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +/** + * 会员卡类型 Repository + * + * @author 付嘉 + * @date 2026-05-27 + */ +@Repository +public interface MemberCardRepository extends R2dbcRepository { + + /** + * 根据会员卡ID查询(未删除的) + */ + Mono findByMemberCardIdAndDeletedAtIsNull(Long memberCardId); + + /** + * 条件查询会员卡列表 + */ + @Query("SELECT * FROM member_card WHERE deleted_at IS NULL " + + "AND (:status IS NULL OR member_card_status = :status) " + + "AND (:name IS NULL OR member_card_name LIKE CONCAT('%', :name, '%')) " + + "AND (:type IS NULL OR member_card_type = :type) " + + "AND (:minPrice IS NULL OR member_card_price >= :minPrice) " + + "AND (:maxPrice IS NULL OR member_card_price <= :maxPrice) " + + "ORDER BY created_at DESC LIMIT :#{#pageable.pageSize} OFFSET :#{#pageable.offset}") + Flux findWithConditions(Integer status, String name, String type, + Double minPrice, Double maxPrice, Pageable pageable); + + /** + * 统计符合条件的会员卡总数 + */ + @Query("SELECT COUNT(*) FROM member_card WHERE deleted_at IS NULL " + + "AND (:status IS NULL OR member_card_status = :status) " + + "AND (:name IS NULL OR member_card_name LIKE CONCAT('%', :name, '%')) " + + "AND (:type IS NULL OR member_card_type = :type) " + + "AND (:minPrice IS NULL OR member_card_price >= :minPrice) " + + "AND (:maxPrice IS NULL OR member_card_price <= :maxPrice)") + Mono countWithConditions(Integer status, String name, String type, + Double minPrice, Double maxPrice); + + /** + * 按状态查询会员卡列表 + */ + Flux findByMemberCardStatusAndDeletedAtIsNull(Integer status, Pageable pageable); + + /** + * 检查会员卡是否已被购买 + */ + @Query("SELECT EXISTS(SELECT 1 FROM member_card_record WHERE member_card_id = :memberCardId AND deleted_at IS NULL LIMIT 1)") + Mono existsPurchasedRecord(Long memberCardId); + + /** + * 逻辑删除会员卡 + */ + @Modifying + @Query("UPDATE member_card SET deleted_at = NOW() WHERE member_card_id = :memberCardId AND deleted_at IS NULL") + Mono logicalDelete(Long memberCardId); + + /** + * 安全更新会员卡信息 + */ + @Modifying + @Query("UPDATE member_card SET " + + "member_card_name = COALESCE(:name, member_card_name), " + + "member_card_price = COALESCE(:price, member_card_price), " + + "member_card_validity_days = COALESCE(:durationDays, member_card_validity_days), " + + "member_card_total_times = COALESCE(:totalCount, member_card_total_times), " + + "member_card_amount = COALESCE(:denomination, member_card_amount), " + + "member_card_status = COALESCE(:status, member_card_status), " + + "updated_at = NOW() " + + "WHERE member_card_id = :memberCardId AND deleted_at IS NULL") + Mono updateSafe(Long memberCardId, String name, Double price, + Integer durationDays, Integer totalCount, + Double denomination, Integer status); + + /** + * 批量查询上架的会员卡 + */ + @Query("SELECT * FROM member_card WHERE deleted_at IS NULL AND member_card_status = :status ORDER BY member_card_price ASC") + Flux findActiveCards(Integer status); +} diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/card/repository/MemberCardTransactionRepository.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/card/repository/MemberCardTransactionRepository.java new file mode 100644 index 0000000..7f33e53 --- /dev/null +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/card/repository/MemberCardTransactionRepository.java @@ -0,0 +1,121 @@ +package cn.novalon.gym.manage.member.card.repository; + +import cn.novalon.gym.manage.member.card.entity.MemberCardTransaction; +import org.springframework.data.domain.Pageable; +import org.springframework.data.r2dbc.repository.Modifying; +import org.springframework.data.r2dbc.repository.Query; +import org.springframework.data.r2dbc.repository.R2dbcRepository; +import org.springframework.stereotype.Repository; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.time.LocalDateTime; + +/** + * 会员卡交易流水 Repository + * + * @author 付嘉 + * @date 2026-05-27 + */ +@Repository +public interface MemberCardTransactionRepository extends R2dbcRepository { + + /** + * 插入交易流水记录 + */ + @Modifying + @Query("INSERT INTO member_card_transactions (member_card_record_id, member_card_id, member_id, operation_type, change_amount, " + + "change_balance, after_remaining_count, after_remaining_balance, related_biz_type, source_order_id, remark, created_at) " + + "VALUES (:memberCardRecordId, :memberCardId, :memberId, :operationType, :changeAmount, :changeBalance, " + + ":afterRemainingCount, :afterRemainingBalance, :relatedBizType, :sourceOrderId, :remark, NOW()) " + + "RETURNING *") + Mono insertTransaction(Long memberCardRecordId, Long memberCardId, Long memberId, + String operationType, Integer changeAmount, + Double changeBalance, Integer afterRemainingCount, + Double afterRemainingBalance, String relatedBizType, + Long sourceOrderId, String remark); + + /** + * 查询会员的使用记录(按时间范围) + */ + @Query("SELECT * FROM member_card_transactions WHERE member_id = :memberId " + + "AND created_at BETWEEN :startTime AND :endTime " + + "ORDER BY created_at DESC LIMIT :#{#pageable.pageSize} OFFSET :#{#pageable.offset}") + Flux findByMemberIdAndTimeRange(Long memberId, + LocalDateTime startTime, + LocalDateTime endTime, + Pageable pageable); + + /** + * 条件查询流水记录 + */ + @Query("SELECT * FROM member_card_transactions " + + "AND (:memberId IS NULL OR member_id = :memberId) " + + "AND (:memberCardId IS NULL OR member_card_id = :memberCardId) " + + "AND (:operationType IS NULL OR operation_type = :operationType) " + + "AND (:startTime IS NULL OR created_at >= :startTime) " + + "AND (:endTime IS NULL OR created_at <= :endTime) " + + "ORDER BY created_at DESC LIMIT :#{#pageable.pageSize} OFFSET :#{#pageable.offset}") + Flux findWithConditions(Long memberId, Long memberCardId, + String operationType, + LocalDateTime startTime, + LocalDateTime endTime, + Pageable pageable); + + /** + * 统计符合条件的流水总数 + */ + @Query("SELECT COUNT(*) FROM member_card_transactions " + + "AND (:memberId IS NULL OR member_id = :memberId) " + + "AND (:memberCardId IS NULL OR member_card_id = :memberCardId) " + + "AND (:operationType IS NULL OR operation_type = :operationType) " + + "AND (:startTime IS NULL OR created_at >= :startTime) " + + "AND (:endTime IS NULL OR created_at <= :endTime)") + Mono countWithConditions(Long memberId, Long memberCardId, + String operationType, + LocalDateTime startTime, + LocalDateTime endTime); + + /** + * 按会员卡ID查询所有流水 + */ + @Query("SELECT * FROM member_card_transactions WHERE member_card_id = :memberCardId ORDER BY created_at DESC") + Flux findByMemberCardId(Long memberCardId); + + /** + * 按会员ID查询所有流水 + */ + @Query("SELECT * FROM member_card_transactions WHERE member_id = :memberId ORDER BY created_at DESC") + Flux findByMemberId(Long memberId); + + /** + * 按会员卡记录ID查询所有流水 + */ + @Query("SELECT * FROM member_card_transactions WHERE member_card_record_id = :recordId ORDER BY created_at DESC") + Flux findByRecordId(Long recordId); + + /** + * 统计某卡种的总扣次数 + */ + @Query("SELECT COALESCE(SUM(change_amount), 0) FROM member_card_transactions " + + "WHERE member_card_id = :memberCardId AND operation_type = 'DEDUCT' " + + "AND created_at BETWEEN :startTime AND :endTime") + Mono sumDeductCountByCardId(Long memberCardId, + LocalDateTime startTime, + LocalDateTime endTime); + + /** + * 统计某时间段的续费总金额 + */ + @Query("SELECT COALESCE(SUM(change_balance), 0) FROM member_card_transactions " + + "WHERE operation_type = 'RENEW' AND created_at BETWEEN :startTime AND :endTime") + Mono sumRenewAmountByTimeRange(LocalDateTime startTime, LocalDateTime endTime); + + /** + * 统计某会员的购卡总金额 + */ + @Query("SELECT COALESCE(SUM(change_balance), 0) FROM member_card_transactions " + + "WHERE member_id = :memberId AND operation_type = 'PURCHASE' " + + "AND created_at BETWEEN :startTime AND :endTime") + Mono sumPurchaseAmountByMemberId(Long memberId, LocalDateTime startTime, LocalDateTime endTime); +} 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/src/main/java/cn/novalon/gym/manage/member/card/repository/RefundApplicationRepository.java similarity index 53% rename from gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/dao/RefundApplicationDao.java rename to gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/card/repository/RefundApplicationRepository.java index 3dca6d4..ae9f288 100644 --- a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/dao/RefundApplicationDao.java +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/card/repository/RefundApplicationRepository.java @@ -1,63 +1,47 @@ -package cn.novalon.gym.manage.gymmembercard.dao; +package cn.novalon.gym.manage.member.card.repository; -import cn.novalon.gym.manage.gymmembercard.entity.RefundApplicationEntity; +import cn.novalon.gym.manage.member.card.entity.RefundApplication; import org.springframework.data.r2dbc.repository.Query; import org.springframework.data.r2dbc.repository.R2dbcRepository; +import org.springframework.stereotype.Repository; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; /** - * 退款申请数据访问对象 - * - * @author shizhounian - * @date 2026-05-23 + * 退款申请 Repository + * + * @author 付嘉 + * @date 2026-05-27 */ -public interface RefundApplicationDao extends R2dbcRepository { +@Repository +public interface RefundApplicationRepository 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); + 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); + 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); + 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/sevice/IMemberCardRecordService.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/card/service/IMemberCardRecordService.java similarity index 77% rename from gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/sevice/IMemberCardRecordService.java rename to gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/card/service/IMemberCardRecordService.java index 06e1cdd..32348f5 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/src/main/java/cn/novalon/gym/manage/member/card/service/IMemberCardRecordService.java @@ -1,10 +1,16 @@ -package cn.novalon.gym.manage.gymmembercard.sevice; +package cn.novalon.gym.manage.member.card.service; -import cn.novalon.gym.manage.gymmembercard.domain.MemberCardRecord; +import cn.novalon.gym.manage.member.card.entity.MemberCardRecord; import org.springframework.data.domain.Pageable; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +/** + * 会员卡记录服务接口 + * + * @author 付嘉 + * @date 2026-05-27 + */ public interface IMemberCardRecordService { Mono findById(Long id); @@ -19,11 +25,11 @@ public interface IMemberCardRecordService { 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 updateStatus(Long recordId, String status); Mono validateCountCard(Long recordId, Integer requiredTimes); Mono validateStoredCard(Long recordId, Double requiredAmount); 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/src/main/java/cn/novalon/gym/manage/member/card/service/IMemberCardService.java similarity index 84% rename from gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/sevice/IMemberCardService.java rename to gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/card/service/IMemberCardService.java index 255dfe7..ec5a3c3 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/src/main/java/cn/novalon/gym/manage/member/card/service/IMemberCardService.java @@ -1,11 +1,17 @@ -package cn.novalon.gym.manage.gymmembercard.sevice; +package cn.novalon.gym.manage.member.card.service; -import cn.novalon.gym.manage.gymmembercard.domain.MemberCard; -import cn.novalon.gym.manage.gymmembercard.domain.MemberCardRecord; +import cn.novalon.gym.manage.member.card.entity.MemberCard; +import cn.novalon.gym.manage.member.card.entity.MemberCardRecord; import org.springframework.data.domain.Pageable; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +/** + * 会员卡服务接口 + * + * @author 付嘉 + * @date 2026-05-27 + */ public interface IMemberCardService { Mono findByMemberCardIdAndDeletedAtIsNull(Long memberCardId); @@ -35,4 +41,4 @@ public interface IMemberCardService { Mono refundCard(Long recordId); Mono processExpiredCards(); -} \ No newline at end of file +} diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/card/service/IMemberCardTransactionService.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/card/service/IMemberCardTransactionService.java new file mode 100644 index 0000000..516b393 --- /dev/null +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/card/service/IMemberCardTransactionService.java @@ -0,0 +1,93 @@ +package cn.novalon.gym.manage.member.card.service; + +import cn.novalon.gym.manage.member.card.entity.MemberCardTransaction; +import cn.novalon.gym.manage.member.card.enums.TransactionType; +import org.springframework.data.domain.Pageable; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.time.LocalDateTime; + +/** + * 会员卡交易流水服务接口 + * + * @author 付嘉 + * @date 2026-05-27 + */ +public interface IMemberCardTransactionService { + + /** + * 记录每一次变动 + * @param transaction 流水记录 + * @return 插入的流水记录 + */ + Mono insertTransaction(MemberCardTransaction transaction); + + /** + * 会员端"使用记录" + * @param memberId 会员ID + * @param startTime 开始时间 + * @param endTime 结束时间 + * @param pageable 分页参数 + * @return 流水记录列表 + */ + Flux findByMemberIdAndTimeRange(Long memberId, LocalDateTime startTime, + LocalDateTime endTime, Pageable pageable); + + /** + * 后台"使用记录查询" + * @param memberId 会员ID + * @param memberCardId 会员卡ID + * @param operationType 操作类型 + * @param startTime 开始时间 + * @param endTime 结束时间 + * @param pageable 分页参数 + * @return 流水记录列表 + */ + Flux findWithConditions(Long memberId, Long memberCardId, + TransactionType operationType, + LocalDateTime startTime, LocalDateTime endTime, + Pageable pageable); + + /** + * 统计符合条件的流水总数 + */ + Mono countWithConditions(Long memberId, Long memberCardId, + TransactionType operationType, + LocalDateTime startTime, LocalDateTime endTime); + + /** + * 按会员卡ID查询所有流水记录 + */ + Flux findByMemberCardId(Long memberCardId); + + /** + * 数据统计 - 统计某卡种的总扣次数 + */ + Mono sumDeductCountByCardId(Long memberCardId, LocalDateTime startTime, LocalDateTime endTime); + + /** + * 数据统计 - 统计某时间段的续费总金额 + */ + Mono sumRenewAmountByTimeRange(LocalDateTime startTime, LocalDateTime endTime); + + /** + * 数据统计 - 统计某会员的购卡总金额 + */ + Mono sumPurchaseAmountByMemberId(Long memberId, LocalDateTime startTime, LocalDateTime endTime); + + /** + * 创建交易记录 + */ + Mono createTransaction(MemberCardTransaction transaction); + + /** + * 查询会员的交易记录 + */ + Flux findByMemberId(Long memberId); + + /** + * 查询会员卡记录的交易历史 + */ + Flux findByRecordId(Long recordId); +} 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/src/main/java/cn/novalon/gym/manage/member/card/service/IRefundApplicationService.java similarity index 75% rename from gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/sevice/IRefundApplicationService.java rename to gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/card/service/IRefundApplicationService.java index 56fa066..669a42b 100644 --- a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/sevice/IRefundApplicationService.java +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/card/service/IRefundApplicationService.java @@ -1,13 +1,13 @@ -package cn.novalon.gym.manage.gymmembercard.sevice; +package cn.novalon.gym.manage.member.card.service; -import cn.novalon.gym.manage.gymmembercard.domain.RefundApplication; +import cn.novalon.gym.manage.member.card.entity.RefundApplication; import reactor.core.publisher.Mono; /** - * 退款申请服务 - * - * @author shizhounian - * @date 2026-05-23 + * 退款申请服务接口 + * + * @author 付嘉 + * @date 2026-05-27 */ public interface IRefundApplicationService { @@ -30,4 +30,4 @@ public interface IRefundApplicationService { * 根据记录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/src/main/java/cn/novalon/gym/manage/member/card/service/impl/MemberCardRecordServiceImpl.java similarity index 69% rename from gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/sevice/impl/MemberCardRecordServiceImpl.java rename to gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/card/service/impl/MemberCardRecordServiceImpl.java index 598c435..874a342 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/src/main/java/cn/novalon/gym/manage/member/card/service/impl/MemberCardRecordServiceImpl.java @@ -1,10 +1,8 @@ -package cn.novalon.gym.manage.gymmembercard.sevice.impl; +package cn.novalon.gym.manage.member.card.service.impl; -import cn.novalon.gym.manage.gymmembercard.domain.MemberCardRecord; -import cn.novalon.gym.manage.gymmembercard.enums.MemberCardRecordStatus; -import cn.novalon.gym.manage.gymmembercard.repository.IMemberCardRecordRepository; -import cn.novalon.gym.manage.gymmembercard.sevice.IMemberCardRecordService; -import org.springframework.beans.factory.annotation.Qualifier; +import cn.novalon.gym.manage.member.card.entity.MemberCardRecord; +import cn.novalon.gym.manage.member.card.repository.MemberCardRecordRepository; +import cn.novalon.gym.manage.member.card.service.IMemberCardRecordService; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import reactor.core.publisher.Flux; @@ -12,17 +10,45 @@ import reactor.core.publisher.Mono; import java.time.LocalDateTime; +/** + * 会员卡记录服务实现 + * + * @author 付嘉 + * @date 2026-05-27 + */ @Service public class MemberCardRecordServiceImpl implements IMemberCardRecordService { - private final IMemberCardRecordRepository memberCardRecordRepository; + private final MemberCardRecordRepository memberCardRecordRepository; - public MemberCardRecordServiceImpl(IMemberCardRecordRepository memberCardRecordRepository) { + public MemberCardRecordServiceImpl(MemberCardRecordRepository memberCardRecordRepository) { this.memberCardRecordRepository = memberCardRecordRepository; } + @Override + public Mono findById(Long recordId) { + return memberCardRecordRepository.findById(recordId); + } + + @Override + public Flux findByMemberId(Long memberId, Pageable pageable) { + return memberCardRecordRepository.findByMemberId(memberId, pageable); + } + + @Override + public Flux findActiveCardsByMemberId(Long memberId) { + return memberCardRecordRepository.findActiveCardsByMemberId(memberId); + } + @Override public Mono insertActiveRecord(MemberCardRecord record) { - return memberCardRecordRepository.insertActiveRecord(record); + return memberCardRecordRepository.insertActiveRecord( + record.getMemberId(), + record.getMemberCardId(), + record.getExpireTime(), + record.getRemainingTimes(), + record.getRemainingAmount(), + record.getSourceOrderId() + ); } @Override @@ -36,20 +62,10 @@ public class MemberCardRecordServiceImpl implements IMemberCardRecordService { } @Override - public Mono updateStatus(Long recordId, MemberCardRecordStatus status) { + public Mono updateStatus(Long recordId, String status) { return memberCardRecordRepository.updateStatus(recordId, status); } - @Override - public Flux findActiveCardsByMemberId(Long memberId) { - return memberCardRecordRepository.findActiveCardsByMemberId(memberId); - } - - @Override - public Flux findByMemberId(Long memberId, Pageable pageable) { - return memberCardRecordRepository.findByMemberId(memberId, pageable); - } - @Override public Mono validateCountCard(Long recordId, Integer requiredTimes) { return memberCardRecordRepository.validateCountCard(recordId, requiredTimes); @@ -64,9 +80,4 @@ 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/src/main/java/cn/novalon/gym/manage/member/card/service/impl/MemberCardServiceImpl.java similarity index 78% rename from gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/sevice/impl/MemberCardServiceImpl.java rename to gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/card/service/impl/MemberCardServiceImpl.java index 379dded..939704d 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/src/main/java/cn/novalon/gym/manage/member/card/service/impl/MemberCardServiceImpl.java @@ -1,20 +1,20 @@ -package cn.novalon.gym.manage.gymmembercard.sevice.impl; +package cn.novalon.gym.manage.member.card.service.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.sevice.IMemberCardService; -import cn.novalon.gym.manage.gymmembercard.sevice.IMemberCardTransactionsService; +import cn.novalon.gym.manage.member.card.entity.MemberCard; +import cn.novalon.gym.manage.member.card.entity.MemberCardRecord; +import cn.novalon.gym.manage.member.card.entity.MemberCardTransaction; +import cn.novalon.gym.manage.member.card.enums.CardEvent; +import cn.novalon.gym.manage.member.card.enums.MemberCardRecordStatus; +import cn.novalon.gym.manage.member.card.enums.MemberCardType; +import cn.novalon.gym.manage.member.card.enums.TransactionType; +import cn.novalon.gym.manage.member.card.handler.DistributedLockService; +import cn.novalon.gym.manage.member.card.handler.ExpirationReminderService; +import cn.novalon.gym.manage.member.card.handler.MemberCardStateMachine; +import cn.novalon.gym.manage.member.card.handler.RefundSagaHandler; +import cn.novalon.gym.manage.member.card.repository.MemberCardRecordRepository; +import cn.novalon.gym.manage.member.card.repository.MemberCardRepository; +import cn.novalon.gym.manage.member.card.service.IMemberCardService; +import cn.novalon.gym.manage.member.card.service.IMemberCardTransactionService; import lombok.extern.slf4j.Slf4j; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; @@ -23,27 +23,33 @@ import reactor.core.publisher.Mono; import java.time.LocalDateTime; +/** + * 会员卡服务实现 + * + * @author 付嘉 + * @date 2026-05-27 + */ @Slf4j @Service public class MemberCardServiceImpl implements IMemberCardService { - private final IMemberCardRepository memberCardRepository; - private final IMemberCardRecordRepository recordRepository; - private final IMemberCardTransactionsService transactionsService; + private final MemberCardRepository memberCardRepository; + private final MemberCardRecordRepository recordRepository; + private final IMemberCardTransactionService transactionService; private final MemberCardStateMachine stateMachine; private final DistributedLockService distributedLockService; private final ExpirationReminderService expirationReminderService; private final RefundSagaHandler refundSagaHandler; - public MemberCardServiceImpl(IMemberCardRepository memberCardRepository, - IMemberCardRecordRepository recordRepository, - IMemberCardTransactionsService transactionsService, + public MemberCardServiceImpl(MemberCardRepository memberCardRepository, + MemberCardRecordRepository recordRepository, + IMemberCardTransactionService transactionService, MemberCardStateMachine stateMachine, DistributedLockService distributedLockService, ExpirationReminderService expirationReminderService, RefundSagaHandler refundSagaHandler) { this.memberCardRepository = memberCardRepository; this.recordRepository = recordRepository; - this.transactionsService = transactionsService; + this.transactionService = transactionService; this.stateMachine = stateMachine; this.distributedLockService = distributedLockService; this.expirationReminderService = expirationReminderService; @@ -109,7 +115,7 @@ public class MemberCardServiceImpl implements IMemberCardService { Mono.defer(() -> createCardRecord(memberId, memberCardId, sourceOrderId, card)) ); }) - .flatMap(record -> createTransaction(record, MemberCardTransactionsAction.PURCHASE, "购买会员卡") + .flatMap(record -> createTransaction(record, TransactionType.PURCHASE, "购买会员卡") .thenReturn(record)) .flatMap(record -> expirationReminderService.scheduleExpirationReminder(record) .then(Mono.just(record))); @@ -118,12 +124,13 @@ public class MemberCardServiceImpl implements IMemberCardService { 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); + MemberCardRecord record = MemberCardRecord.builder() + .memberId(memberId) + .memberCardId(memberCardId) + .sourceOrderId(sourceOrderId) + .purchaseTime(LocalDateTime.now()) + .status(MemberCardRecordStatus.ACTIVE) + .build(); MemberCardType cardType = MemberCardType.valueOf(card.getMemberCardType()); LocalDateTime now = LocalDateTime.now(); @@ -148,7 +155,14 @@ public class MemberCardServiceImpl implements IMemberCardService { return Mono.error(new RuntimeException("不支持的会员卡类型")); } - return recordRepository.insertActiveRecord(record); + return recordRepository.insertActiveRecord( + record.getMemberId(), + record.getMemberCardId(), + record.getExpireTime(), + record.getRemainingTimes(), + record.getRemainingAmount(), + record.getSourceOrderId() + ); }); } @@ -157,7 +171,7 @@ public class MemberCardServiceImpl implements IMemberCardService { Integer addDays, Long sourceOrderId) { return recordRepository.findById(recordId) .switchIfEmpty(Mono.error(new RuntimeException("会员卡记录不存在"))) - .flatMap(originalRecord -> stateMachine.validateTransition(originalRecord, MemberCardEvent.RENEW) + .flatMap(originalRecord -> stateMachine.validateTransition(originalRecord, CardEvent.RENEW) .then(Mono.just(originalRecord))) .flatMap(originalRecord -> memberCardRepository.findByMemberCardIdAndDeletedAtIsNull(originalRecord.getMemberCardId()) .switchIfEmpty(Mono.error(new RuntimeException("会员卡类型不存在"))) @@ -188,7 +202,7 @@ public class MemberCardServiceImpl implements IMemberCardService { 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) { + if (MemberCardRecordStatus.USED_UP.equals(record.getStatus())) { record.setStatus(MemberCardRecordStatus.ACTIVE); } break; @@ -196,7 +210,7 @@ public class MemberCardServiceImpl implements IMemberCardService { 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) { + if (MemberCardRecordStatus.USED_UP.equals(record.getStatus())) { record.setStatus(MemberCardRecordStatus.ACTIVE); } break; @@ -205,7 +219,7 @@ public class MemberCardServiceImpl implements IMemberCardService { } return recordRepository.save(record) - .flatMap(updatedRecord -> createTransaction(updatedRecord, MemberCardTransactionsAction.RENEW, "续费会员卡") + .flatMap(updatedRecord -> createTransaction(updatedRecord, TransactionType.RENEW, "续费会员卡") .then(expirationReminderService.scheduleExpirationReminder(updatedRecord)) .thenReturn(updatedRecord)); } @@ -214,7 +228,7 @@ public class MemberCardServiceImpl implements IMemberCardService { 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) + .flatMap(record -> stateMachine.validateTransition(record, CardEvent.USE) .then(Mono.just(record))) .flatMap(record -> memberCardRepository.findByMemberCardIdAndDeletedAtIsNull(record.getMemberCardId()) .switchIfEmpty(Mono.error(new RuntimeException("会员卡类型不存在"))) @@ -231,7 +245,7 @@ public class MemberCardServiceImpl implements IMemberCardService { private Mono doUseCard(MemberCardRecord record, MemberCard card, Integer deductTimes, Double deductAmount) { - if (record.getStatus() != MemberCardRecordStatus.ACTIVE) { + if (!MemberCardRecordStatus.ACTIVE.name().equals(record.getStatus())) { return Mono.error(new RuntimeException("会员卡状态不正确")); } @@ -271,7 +285,7 @@ public class MemberCardServiceImpl implements IMemberCardService { } return recordRepository.save(record) - .flatMap(updatedRecord -> createTransaction(updatedRecord, MemberCardTransactionsAction.DEDUCT, "使用会员卡") + .flatMap(updatedRecord -> createTransaction(updatedRecord, TransactionType.DEDUCT, "使用会员卡") .thenReturn(updatedRecord)); } @@ -294,24 +308,25 @@ public class MemberCardServiceImpl implements IMemberCardService { @Override public Mono processExpiredCards() { return recordRepository.findExpiredCards() - .flatMap(record -> stateMachine.transition(record.getStatus(), MemberCardEvent.EXPIRE) + .flatMap(record -> stateMachine.transition(record.getStatus(), CardEvent.EXPIRE) .flatMap(newState -> recordRepository.updateStatus( - record.getMemberCardRecordId(), newState))) - .reduce(0, Integer::sum) + record.getMemberCardRecordId(), newState.name()))) + .reduce(0, (count, result) -> count + 1) .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); + private Mono createTransaction(MemberCardRecord record, TransactionType action, String remark) { + MemberCardTransaction transaction = MemberCardTransaction.builder() + .memberId(record.getMemberId()) + .memberCardId(record.getMemberCardId()) + .operationType(action.name()) + .changeAmount(record.getRemainingTimes()) + .changeBalance(record.getRemainingAmount()) + .afterRemainingCount(record.getRemainingTimes()) + .afterRemainingBalance(record.getRemainingAmount()) + .remark(remark) + .build(); - return transactionsService.createTransaction(transaction); + return transactionService.createTransaction(transaction); } -} \ No newline at end of file +} diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/card/service/impl/MemberCardTransactionServiceImpl.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/card/service/impl/MemberCardTransactionServiceImpl.java new file mode 100644 index 0000000..6b5bc21 --- /dev/null +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/card/service/impl/MemberCardTransactionServiceImpl.java @@ -0,0 +1,109 @@ +package cn.novalon.gym.manage.member.card.service.impl; + +import cn.novalon.gym.manage.member.card.entity.MemberCardTransaction; +import cn.novalon.gym.manage.member.card.enums.TransactionType; +import cn.novalon.gym.manage.member.card.repository.MemberCardTransactionRepository; +import cn.novalon.gym.manage.member.card.service.IMemberCardTransactionService; +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; + +/** + * 会员卡交易流水服务实现 + * + * @author 付嘉 + * @date 2026-05-27 + */ +@Slf4j +@Service +public class MemberCardTransactionServiceImpl implements IMemberCardTransactionService { + private final MemberCardTransactionRepository transactionRepository; + + public MemberCardTransactionServiceImpl(MemberCardTransactionRepository transactionRepository) { + this.transactionRepository = transactionRepository; + } + + @Override + public Mono insertTransaction(MemberCardTransaction transaction) { + return transactionRepository.insertTransaction( + transaction.getMemberCardRecordId(), + transaction.getMemberCardId(), + transaction.getMemberId(), + transaction.getOperationType(), + transaction.getChangeAmount(), + transaction.getChangeBalance(), + transaction.getAfterRemainingCount(), + transaction.getAfterRemainingBalance(), + transaction.getRelatedBizType(), + transaction.getSourceOrderId(), + transaction.getRemark() + ); + } + + @Override + public Flux findByMemberIdAndTimeRange(Long memberId, LocalDateTime startTime, + LocalDateTime endTime, Pageable pageable) { + return transactionRepository.findByMemberIdAndTimeRange(memberId, startTime, endTime, pageable); + } + + @Override + public Flux findWithConditions(Long memberId, Long memberCardId, + TransactionType operationType, + LocalDateTime startTime, LocalDateTime endTime, + Pageable pageable) { + return transactionRepository.findWithConditions(memberId, memberCardId, + operationType != null ? operationType.name() : null, + startTime, endTime, pageable); + } + + @Override + public Mono countWithConditions(Long memberId, Long memberCardId, + TransactionType operationType, + LocalDateTime startTime, LocalDateTime endTime) { + return transactionRepository.countWithConditions(memberId, memberCardId, + operationType != null ? operationType.name() : null, + startTime, endTime); + } + + @Override + public Flux findByMemberCardId(Long memberCardId) { + return transactionRepository.findByMemberCardId(memberCardId); + } + + @Override + public Mono sumDeductCountByCardId(Long memberCardId, LocalDateTime startTime, LocalDateTime endTime) { + return transactionRepository.sumDeductCountByCardId(memberCardId, startTime, endTime); + } + + @Override + public Mono sumRenewAmountByTimeRange(LocalDateTime startTime, LocalDateTime endTime) { + return transactionRepository.sumRenewAmountByTimeRange(startTime, endTime); + } + + @Override + public Mono sumPurchaseAmountByMemberId(Long memberId, LocalDateTime startTime, LocalDateTime endTime) { + return transactionRepository.sumPurchaseAmountByMemberId(memberId, startTime, endTime); + } + + @Override + public Mono createTransaction(MemberCardTransaction transaction) { + return transactionRepository.save(transaction) + .then() + .doOnSuccess(v -> log.info("创建会员卡交易记录: memberId={}, cardId={}, type={}", + transaction.getMemberId(), transaction.getMemberCardId(), transaction.getOperationType())); + } + + @Override + public Flux findByMemberId(Long memberId) { + return transactionRepository.findByMemberId(memberId); + } + + @Override + public Flux findByRecordId(Long recordId) { + return transactionRepository.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/src/main/java/cn/novalon/gym/manage/member/card/service/impl/RefundApplicationServiceImpl.java similarity index 72% rename from gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/sevice/impl/RefundApplicationServiceImpl.java rename to gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/card/service/impl/RefundApplicationServiceImpl.java index 4c29ac7..48fb6ae 100644 --- 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/src/main/java/cn/novalon/gym/manage/member/card/service/impl/RefundApplicationServiceImpl.java @@ -1,28 +1,28 @@ -package cn.novalon.gym.manage.gymmembercard.sevice.impl; +package cn.novalon.gym.manage.member.card.service.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 cn.novalon.gym.manage.member.card.entity.RefundApplication; +import cn.novalon.gym.manage.member.card.enums.RefundStatus; +import cn.novalon.gym.manage.member.card.repository.RefundApplicationRepository; +import cn.novalon.gym.manage.member.card.service.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 + * 退款申请服务实现 + * + * @author 付嘉 + * @date 2026-05-27 */ @Slf4j @Service public class RefundApplicationServiceImpl implements IRefundApplicationService { - private final IRefundApplicationRepository refundApplicationRepository; + private final RefundApplicationRepository refundApplicationRepository; - public RefundApplicationServiceImpl(IRefundApplicationRepository refundApplicationRepository) { + public RefundApplicationServiceImpl(RefundApplicationRepository refundApplicationRepository) { this.refundApplicationRepository = refundApplicationRepository; } @@ -36,13 +36,14 @@ public class RefundApplicationServiceImpl implements IRefundApplicationService { return Mono.empty(); }) .then(Mono.defer(() -> { - RefundApplication application = new RefundApplication(); - application.setRecordId(recordId); - application.setReason(reason); - application.setStatus("PENDING"); - application.setApplyTime(LocalDateTime.now()); + RefundApplication application = RefundApplication.builder() + .recordId(recordId) + .reason(reason) + .status(RefundStatus.PENDING) + .applyTime(LocalDateTime.now()) + .build(); - return refundApplicationRepository.create(application) + return refundApplicationRepository.save(application) .doOnSuccess(app -> log.info("创建退款申请成功: applicationId={}, recordId={}", app.getId(), recordId)); })); @@ -58,6 +59,7 @@ public class RefundApplicationServiceImpl implements IRefundApplicationService { } return refundApplicationRepository.approve(applicationId, "APPROVED", auditorId, remark) + .thenReturn(application) .doOnSuccess(app -> log.info("批准退款申请成功: applicationId={}, auditorId={}", applicationId, auditorId)); }); @@ -73,6 +75,7 @@ public class RefundApplicationServiceImpl implements IRefundApplicationService { } return refundApplicationRepository.approve(applicationId, "REJECTED", auditorId, remark) + .thenReturn(application) .doOnSuccess(app -> log.info("拒绝退款申请成功: applicationId={}, auditorId={}", applicationId, auditorId)); }); diff --git a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/util/BeanConvertUtil.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/card/util/BeanConvertUtil.java similarity index 91% rename from gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/util/BeanConvertUtil.java rename to gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/card/util/BeanConvertUtil.java index f9f7e00..493fa85 100644 --- a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/util/BeanConvertUtil.java +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/card/util/BeanConvertUtil.java @@ -1,8 +1,4 @@ -package cn.novalon.gym.manage.gymmembercard.util; -/* - *@Author:shizhounian - *@Date:2026/5/11-05 21:19:04 - */ +package cn.novalon.gym.manage.member.card.util; import cn.hutool.core.bean.BeanUtil; import org.springframework.stereotype.Component; @@ -12,6 +8,9 @@ import java.util.List; /** * Entity、Domain、VO、DTO转换工具类 + * + * @author 付嘉 + * @date 2026-05-27 */ @Component public class BeanConvertUtil { @@ -46,4 +45,4 @@ public class BeanConvertUtil { } return targetList; } -} \ No newline at end of file +} diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/config/HttpClientConfig.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/config/HttpClientConfig.java new file mode 100644 index 0000000..a3be578 --- /dev/null +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/config/HttpClientConfig.java @@ -0,0 +1,22 @@ +package cn.novalon.gym.manage.member.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.reactive.function.client.WebClient; + +/** + * HTTP 客户端配置 + * + * @author 付嘉 + * @date 2026-05-01 + */ + +@Configuration +public class HttpClientConfig { + + // WebClient Bean,用于调用微信 API + @Bean + public WebClient webClient() { + return WebClient.builder().build(); + } +} diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/config/WechatProperties.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/config/WechatProperties.java new file mode 100644 index 0000000..ec32292 --- /dev/null +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/config/WechatProperties.java @@ -0,0 +1,63 @@ +package cn.novalon.gym.manage.member.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +/** + * 微信配置 + * + * @author 付嘉 + * @date 2026-05-01 + */ + +@Data +@Component +@ConfigurationProperties(prefix = "wechat") +public class WechatProperties { + + // 小程序配置 + private MiniApp miniapp = new MiniApp(); + + // 服务号配置 + private Mp mp = new Mp(); + + // 手机号加密配置 + private PhoneEncryption phoneEncryption = new PhoneEncryption(); + + @Data + public static class MiniApp { + // 小程序 AppID + private String appId; + + // 小程序 AppSecret + private String appSecret; + } + + @Data + public static class Mp { + // 服务号 AppID + private String appId; + + // 服务号 AppSecret + private String appSecret; + + // Token 验证信息 + private String token; + + // EncodingAESKey + private String aesKey; + + // 回调地址(微信服务号事件推送 URL) + private String callbackUrl; + } + + @Data + public static class PhoneEncryption { + // 手机号加密密钥 + private String secretKey; + + // 初始化向量 IV + private String iv; + } +} diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/dto/AdminUpdatePhoneDto.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/dto/AdminUpdatePhoneDto.java new file mode 100644 index 0000000..0854b49 --- /dev/null +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/dto/AdminUpdatePhoneDto.java @@ -0,0 +1,17 @@ +package cn.novalon.gym.manage.member.dto; + +import lombok.Data; + +/** + * 更新手机号Dto + * + * @author 付嘉 + * @date 2026-05-01 + */ + +@Data +public class AdminUpdatePhoneDto { + + // 手机号 + private String phone; +} diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/dto/SearchMemberDto.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/dto/SearchMemberDto.java new file mode 100644 index 0000000..bcd528d --- /dev/null +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/dto/SearchMemberDto.java @@ -0,0 +1,23 @@ +package cn.novalon.gym.manage.member.dto; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class SearchMemberDto { + + // 搜索字段 - 包括 会员号、昵称、手机号 + private String searchValue; + + // 排序 + private String filter; + + // 页码 + private Integer pageNum = 1; + + // 页大小 + private Integer pageSize = 10; +} diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/dto/UpdateMemberInfoDto.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/dto/UpdateMemberInfoDto.java new file mode 100644 index 0000000..835347c --- /dev/null +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/dto/UpdateMemberInfoDto.java @@ -0,0 +1,30 @@ +package cn.novalon.gym.manage.member.dto; + +import lombok.Data; + +import java.util.Date; + +/** + * 更新会员信息Dto + * + * @author 付嘉 + * @date 2026-05-10 + */ +@Data +public class UpdateMemberInfoDto { + + // 昵称 + private String nickname; + + // 性别 + private Integer gender; + + // 生日 + private Date birthday; + + // 头像 + private String avatar; + + // 地址 + private String address; +} diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/dto/WechatLoginDto.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/dto/WechatLoginDto.java new file mode 100644 index 0000000..b8f706d --- /dev/null +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/dto/WechatLoginDto.java @@ -0,0 +1,29 @@ +package cn.novalon.gym.manage.member.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import jakarta.validation.constraints.NotBlank; + +/** + * 微信小程序登录 DTO + * + * @author 付嘉 + * @date 2026-05-01 + */ + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class WechatLoginDto { + + // 微信小程序登录 code + @NotBlank(message = "登录code不能为空") + private String code; + + // 手机号code + private String phoneCode; +} diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/dto/WechatOfficialEventDto.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/dto/WechatOfficialEventDto.java new file mode 100644 index 0000000..602f3d3 --- /dev/null +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/dto/WechatOfficialEventDto.java @@ -0,0 +1,35 @@ +package cn.novalon.gym.manage.member.dto; + +import lombok.Data; + +/** + * 微信服务号事件 DTO + * + * @author 付嘉 + * @date 2026-05-01 + */ + +@Data +public class WechatOfficialEventDto { + + // 微信号 + private String toUserName; + + // 发送方帐号(一个 OpenID) + private String fromUserName; + + // 消息创建时间(整型) + private Long createTime; + + // 消息类型,event + private String msgType; + + // 事件类型:subscribe(关注)/ unsubscribe(取消关注) + private String event; + + // 事件 KEY + private String eventKey; + + // 二维码 ticket(获取二维码图片) + private String ticket; +} diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/entity/BaseEntity.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/entity/BaseEntity.java new file mode 100644 index 0000000..1ad7597 --- /dev/null +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/entity/BaseEntity.java @@ -0,0 +1,41 @@ +package cn.novalon.gym.manage.member.entity; + +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.Id; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.domain.Persistable; +import org.springframework.data.relational.core.mapping.Column; + +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * 会员模块实体基类 + * + * @author 付嘉 + * @date 2026-05-08 + */ +@Data +public abstract class BaseEntity implements Persistable { + + // ID + @Id + private Long id; + + // 创建时间 + @CreatedDate + @Column("created_at") + private LocalDateTime createdAt; + + // 更新时间 + @LastModifiedDate + @Column("updated_at") + private LocalDateTime updatedAt; + + // 判断当前实体是否是新建的 + @Override + public boolean isNew() { + return createdAt == null; + } +} diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/entity/Member.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/entity/Member.java new file mode 100644 index 0000000..36c7ba4 --- /dev/null +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/entity/Member.java @@ -0,0 +1,81 @@ +package cn.novalon.gym.manage.member.entity; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import org.springframework.data.relational.core.mapping.Column; +import org.springframework.data.relational.core.mapping.Table; + +import java.time.LocalDateTime; +import java.util.Date; + +/** + * 会员实体类 - 对应 member_user 表 + * + * @author 付嘉 + * @date 2026-05-01 + */ + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode(callSuper = true) +@Table("member_user") +public class Member extends BaseEntity { + + //会员号 + @Column("member_no") + private String memberNo; + + //昵称 + @Column("nickname") + private String nickname; + + //手机号(AES 加密存储 + @Column("phone") + private String phone; + + //性别 + @Column("gender") + private Integer gender; + + //生日 + @Column("birthday") + private Date birthday; + + //地址 + @Column("address") + private String address; + + //是否关注服务号 + @Column("subscribed") + private Boolean subscribed; + + // 最后登录时间 + @Column("last_login_at") + private LocalDateTime lastLoginAt; + + // 头像 + @Column("avatar") + private String avatar; + + // 微信UnionID + @Column("union_id") + private String unionId; + + // 微信OpenID小程序 + @Column("miniapp_open_id") + private String miniappOpenId; + + // 服务号openid + @Column("official_open_id") + private String officialOpenId; + + // 软删除 + @Column("is_deleted") + private Boolean isDeleted; + +} diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/entity/SignInRecord.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/entity/SignInRecord.java new file mode 100644 index 0000000..83dae3a --- /dev/null +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/entity/SignInRecord.java @@ -0,0 +1,38 @@ +package cn.novalon.gym.manage.member.entity; + +import lombok.*; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.Id; +import org.springframework.data.relational.core.mapping.Column; +import org.springframework.data.relational.core.mapping.Table; + +import java.time.LocalDate; +import java.time.LocalDateTime; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Table("sign_in_record") +public class SignInRecord { + + @Id + private Long id; + + // 会员ID + @Column("member_id") + private Long memberId; + + // 签到日期 + @Column("sign_in_date") + private LocalDate signInDate; + + // 签到时间 + @Column("sign_in_time") + private LocalDateTime signInTime; + + // 创建时间 + @CreatedDate + @Column("created_at") + private LocalDateTime createdAt; +} diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/entity/WechatUser.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/entity/WechatUser.java new file mode 100644 index 0000000..2b7a8e0 --- /dev/null +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/entity/WechatUser.java @@ -0,0 +1,55 @@ +package cn.novalon.gym.manage.member.entity; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import org.springframework.data.relational.core.mapping.Column; +import org.springframework.data.relational.core.mapping.Table; + +import java.time.LocalDateTime; + +/** + * 微信用户信息 + * + * @author 付嘉 + * @date 2026-05-01 + */ + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode(callSuper = true) +@Table("wechat_user") +public class WechatUser extends BaseEntity { + + // 会员 ID + @Column("member_id") + private Long memberId; + + // 微信 UnionID + @Column("union_id") + private String unionId; + + // 小程序 OpenID + @Column("miniapp_openid") + private String miniappOpenid; + + // 服务号 OpenID + @Column("mp_openid") + private String mpOpenid; + + // 是否关注服务号 + @Column("is_subscribed") + private Boolean isSubscribed; + + // 首次关注时间公众号的时间 + @Column("subscribe_time") + private LocalDateTime subscribeTime; + + // 最后一次取消关注的时间 + @Column("unsubscribe_time") + private LocalDateTime unsubscribeTime; +} diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/es/entity/MemberES.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/es/entity/MemberES.java new file mode 100644 index 0000000..b479ce5 --- /dev/null +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/es/entity/MemberES.java @@ -0,0 +1,35 @@ +package cn.novalon.gym.manage.member.es.entity; + +import lombok.Data; +import org.springframework.data.annotation.Id; +import org.springframework.data.elasticsearch.annotations.Document; +import org.springframework.data.elasticsearch.annotations.Field; +import org.springframework.data.elasticsearch.annotations.FieldType; + +@Data +@Document(indexName = "gym_members") +public class MemberES { + + @Id + private String id; + + // 会员号 - 需要搜索(精确匹配) + @Field(type = FieldType.Keyword) + private String memberNo; + + // 昵称 - 需要搜索(模糊搜索) + @Field(type = FieldType.Text) + private String nickname; + + // 手机号 - 需要搜索(精确匹配) + @Field(type = FieldType.Keyword) + private String phone; + + // 性别 - 用于筛选 + @Field(type = FieldType.Integer) + private Integer gender; + + // 头像 - 列表展示 + @Field(type = FieldType.Keyword) + private String avatar; +} \ No newline at end of file diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/es/repository/MemberESRepository.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/es/repository/MemberESRepository.java new file mode 100644 index 0000000..a233097 --- /dev/null +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/es/repository/MemberESRepository.java @@ -0,0 +1,20 @@ +package cn.novalon.gym.manage.member.es.repository; + +import cn.novalon.gym.manage.member.es.entity.MemberES; +import org.springframework.data.domain.Pageable; +import org.springframework.data.elasticsearch.repository.ReactiveElasticsearchRepository; +import org.springframework.stereotype.Repository; +import reactor.core.publisher.Flux; + +/** + * ES 会员数据访问层 + */ +@Repository +public interface MemberESRepository extends ReactiveElasticsearchRepository { + + /** + * 前台通用搜索:会员号(精确匹配) 或 昵称(模糊匹配) 或 手机号(精确匹配)并且 性别筛选(精确匹配) + */ + Flux findByMemberNoOrPhoneOrNicknameContainingAndGender( + String memberNo, String phone, String nickname,String gender, Pageable pageable); +} \ No newline at end of file diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/handler/MemberHandler.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/handler/MemberHandler.java new file mode 100644 index 0000000..7571053 --- /dev/null +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/handler/MemberHandler.java @@ -0,0 +1,287 @@ +package cn.novalon.gym.manage.member.handler; + +import cn.novalon.gym.manage.member.config.WechatProperties; +import cn.novalon.gym.manage.member.dto.AdminUpdatePhoneDto; +import cn.novalon.gym.manage.member.dto.SearchMemberDto; +import cn.novalon.gym.manage.member.dto.UpdateMemberInfoDto; +import cn.novalon.gym.manage.member.service.MemberService; +import cn.novalon.gym.manage.member.service.WechatAuthService; +import cn.novalon.gym.manage.member.service.WechatOfficialService; +import cn.novalon.gym.manage.member.util.AesUtil; +import cn.novalon.gym.manage.sys.util.AuthUtil; +import cn.novalon.gym.manage.sys.security.JwtTokenProvider; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.math.NumberUtils; +import org.springframework.http.MediaType; +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; + +/** + * 会员信息处理器 + * + * @author 付嘉 + * @date 2026-05-01 + */ + +@Slf4j +@Component +@RequiredArgsConstructor +public class MemberHandler { + + private final MemberService memberService; + private final WechatAuthService wechatAuthService; + private final WechatOfficialService wechatOfficialService; + private final JwtTokenProvider jwtTokenProvider; + private final WechatProperties wechatProperties; + private final AuthUtil authUtil; + + /** + * 获取会员信息 + * + * GET /api/member/info + * header: { "Authorization": "Bearer xxx" } + */ + public Mono getMemberInfo(ServerRequest request) { + + Long memberId = authUtil.getMemberIdOrThrow(request); + + log.info("获取会员信息, memberId: {}", memberId); + + return memberService.getMemberInfo(memberId) + .flatMap(info -> ServerResponse.ok() + .contentType(MediaType.APPLICATION_JSON) + .bodyValue(info)); + } + + /** + * 更新会员信息 + * + * PUT /api/member/info + * header: { "Authorization": "Bearer xxx" } + * Body: { + * "nickname": "新昵称", + * "gender": 1, + * "birthday": "2000-01-01", + * "avatar": "https://example.com/avatar.jpg", + * "address": "北京市朝阳区" + * } + */ + public Mono updateMemberInfo(ServerRequest request) { + + Long memberId = authUtil.getMemberIdOrThrow(request); + + log.info("更新会员信息, memberId: {}", memberId); + + return request.bodyToMono(UpdateMemberInfoDto.class) + .flatMap(updateDto -> memberService.updateMemberInfo(memberId, updateDto)) + .flatMap(info -> ServerResponse.ok() + .contentType(MediaType.APPLICATION_JSON) + .bodyValue(info)); + } + + /** + * 绑定手机号(微信小程序) + * header: { "Authorization": "Bearer xxx" } + * POST /api/member/phone/bind?code=PHONE_CODE + */ + public Mono bindPhone(ServerRequest request) { + + Long memberId = authUtil.getMemberIdOrThrow(request); + + String phoneCode = request.queryParam("phoneCode").orElse(""); + + if (phoneCode.trim().isEmpty()) throw new IllegalArgumentException("手机号code不能为空"); + + log.info("收到绑定手机号请求, memberId: {}, phoneCode: {}", memberId, phoneCode); + + return wechatAuthService.bindPhone(memberId, phoneCode) + .flatMap(success -> ServerResponse.ok() + .contentType(MediaType.APPLICATION_JSON) + .bodyValue(success)); + } + + /** + * 查询服务号关注状态 + * + * GET /api/member/subscribe/status + * + */ + public Mono checkSubscribeStatus(ServerRequest request) { + + Long memberId = authUtil.getMemberIdOrThrow(request); + + log.info("查询服务号关注状态, memberId: {}", memberId); + + return wechatOfficialService.checkSubscribeStatus(memberId) + .flatMap(subscribed -> { + return ServerResponse.ok() + .contentType(MediaType.APPLICATION_JSON) + .bodyValue(subscribed); + }); + } + + /** + * 管理员更新手机号 + * + * POST /api/admin/member/123/phone + * header: { "Authorization": "Bearer xxx" } + * Body: { "phone": "13800138000" } + * + */ + public Mono adminUpdatePhone(ServerRequest request) { + + Long adminId = authUtil.getMemberIdOrThrow(request); + + String memberIdStr = request.pathVariable("id"); + long memberId = NumberUtils.toLong(memberIdStr, 0L); + + if (memberId <= 0) throw new IllegalArgumentException("更新手机号失败: memberId 无效"); + + log.info("收到更新手机号请求, memberId: {}", memberId); + + return request.bodyToMono(AdminUpdatePhoneDto.class) + .flatMap(body -> { + String phone = body.getPhone(); + + if (phone == null || phone.isEmpty()) return Mono.error(new IllegalArgumentException("手机号不能为空")); + + if (!phone.matches("^1[3-9]\\d{9}$")) return Mono.error(new IllegalArgumentException("手机号格式不正确")); + + log.info("开始更新手机号, memberId: {}, phone: {}", memberId, phone); + + return memberService.adminUpdatePhone(memberId, phone); + }) + .flatMap(success -> { + log.info("手机号更新成功, memberId: {}", memberId); + return ServerResponse.ok() + .contentType(MediaType.APPLICATION_JSON) + .bodyValue(success); + }); + } + + /** + * 前台查看会员信息 + * + * GET /api/admin/member/{id} + * header: { "Authorization": "xxx" } + * + */ + public Mono adminGetMemberInfo(ServerRequest request) { + + Long adminId = authUtil.getMemberIdOrThrow(request); + + String memberIdStr = request.pathVariable("id"); + long memberId = NumberUtils.toLong(memberIdStr, 0L); + if(memberId <= 0) throw new IllegalArgumentException("会员ID格式错误"); + + log.info("前台查看会员信息, adminId: {}, memberId: {}", adminId, memberId); + + // TODO 多表查询:会员信息、团课信息、会员卡信息 + + return ServerResponse.ok() + .contentType(MediaType.APPLICATION_JSON) + .bodyValue("成功"); + } + + /** + * 前台编辑会员信息 + * + * PUT /api/admin/member/{id} + * header: { "Authorization": "xxx" } + * Body:{"字段","值"} + */ + public Mono adminUpdateMemberInfo(ServerRequest request) { + + Long adminId = authUtil.getMemberIdOrThrow(request); + + String memberIdStr = request.pathVariable("id"); + long memberId = NumberUtils.toLong(memberIdStr, 0L); + if(memberId <= 0L) throw new IllegalArgumentException("会员ID格式错误"); + + log.info("前台编辑会员信息, adminId: {}, memberId: {}", adminId, memberId); + + // TODO 多表查询:会员信息、团课信息、会员卡信息 + + return ServerResponse.ok() + .contentType(MediaType.APPLICATION_JSON) + .bodyValue("成功"); + } + + /** + * 前台搜索会员列表 + * + * GET /api/admin/members?searchValue=手机号/姓名/会员号&filter=男/女&pageNum=1&pageSize=10 + * header: { "Authorization": "Bearer xxx" } + */ + public Mono searchMembers(ServerRequest request) { + + Long adminId = authUtil.getMemberIdOrThrow(request); + + String keyword = request.queryParam("searchValue").orElse(null); + String filter = request.queryParam("filter").orElse(null); + int pageNum = NumberUtils.toInt(request.queryParam("pageNum").orElse("1"), 1); + int pageSize = NumberUtils.toInt(request.queryParam("pageSize").orElse("10"), 10); + + log.info("前台搜索会员列表, adminId: {}, keyword: {}, filter: {}, pageNum: {}, pageSize: {}", + adminId, keyword, filter, pageNum, pageSize); + + return memberService.searchMember(new SearchMemberDto(keyword, filter, pageNum, pageSize)) + .map(member -> { + // 解密手机号 + if (member.getPhone() != null && !member.getPhone().isEmpty()) { + try { + String secretKey = wechatProperties.getPhoneEncryption().getSecretKey(); + String iv = wechatProperties.getPhoneEncryption().getIv(); + String decryptedPhone = AesUtil.decrypt(member.getPhone(), secretKey, iv); + member.setPhone(decryptedPhone); + } catch (Exception e) { + log.error("手机号解密失败, memberId: {}", member.getId(), e); + member.setPhone(null); + } + } + return member; + }) + .collectList() + .flatMap(list -> ServerResponse.ok().bodyValue(list)); + } + + + /** + * 前台查看会员列表 + * + * GET /api/admin/members/all?pageNum=1&pageSize=10 + * header: { "Authorization": "Bearer xxx" } + */ + public Mono getAllMembers(ServerRequest request) { + + Long adminId = authUtil.getMemberIdOrThrow(request); + + int pageNum = NumberUtils.toInt(request.queryParam("pageNum").orElse("1"), 1); + int pageSize = NumberUtils.toInt(request.queryParam("pageSize").orElse("10"), 10); + + log.info("前台查看会员列表, adminId: {}, pageNum: {}, pageSize: {}", adminId, pageNum, pageSize); + + return memberService.findAll(pageNum, pageSize) + .map(member -> { + // 解密手机号 + if (member.getPhone() != null && !member.getPhone().isEmpty()) { + try { + String secretKey = wechatProperties.getPhoneEncryption().getSecretKey(); + String iv = wechatProperties.getPhoneEncryption().getIv(); + String decryptedPhone = AesUtil.decrypt(member.getPhone(), secretKey, iv); + member.setPhone(decryptedPhone); + } catch (Exception e) { + log.error("手机号解密失败, memberId: {}", member.getId(), e); + member.setPhone(null); + } + } + return member; + }) + .collectList() + .flatMap(list -> ServerResponse.ok().bodyValue(list)); + } + +} diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/handler/WechatAuthHandler.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/handler/WechatAuthHandler.java new file mode 100644 index 0000000..13cfd53 --- /dev/null +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/handler/WechatAuthHandler.java @@ -0,0 +1,68 @@ +package cn.novalon.gym.manage.member.handler; + +import cn.novalon.gym.manage.member.dto.WechatLoginDto; +import cn.novalon.gym.manage.member.service.WechatAuthService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.MediaType; +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; + +/** + * 微信认证 + * + * @author 付嘉 + * @date 2026-05-01 + */ + +@Slf4j +@Component +@RequiredArgsConstructor +public class WechatAuthHandler { + + private final WechatAuthService wechatAuthService; + private final WechatOfficialEventHandler wechatOfficialEventHandler; + + /** + * 小程序更新 + * + * POST /api/member/auth/miniapp/login + * Body: {"code": "wx_login_code"} + * + * @param request ServerRequest + * @return Mono 登录响应 + */ + public Mono miniappLogin(ServerRequest request) { + log.info("收到小程序登录请求"); + + return request.bodyToMono(WechatLoginDto.class) + .flatMap(loginRequest -> { + log.info("开始微信AuthService, code: {}", loginRequest.getCode()); + return wechatAuthService.miniappLogin(loginRequest); + }) + .flatMap(response -> { + log.info("更新成功, memberId: {}", response.getMemberId()); + return ServerResponse.ok() + .contentType(MediaType.APPLICATION_JSON) + .bodyValue(response); + }); + } + + /** + * 公众号回调 + * + * POST /api/member/auth/mp/callback + * Body: subscribeopenid + * + */ + public Mono mpCallback(ServerRequest request) { + return wechatOfficialEventHandler.handleEvent(request); + } + + // 验证微信公众号签名 + public Mono verifyMpSignature(ServerRequest request) { + return wechatOfficialEventHandler.verifySignature(request); + } +} diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/handler/WechatOfficialEventHandler.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/handler/WechatOfficialEventHandler.java new file mode 100644 index 0000000..d01e989 --- /dev/null +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/handler/WechatOfficialEventHandler.java @@ -0,0 +1,218 @@ +package cn.novalon.gym.manage.member.handler; + +import cn.novalon.gym.manage.member.config.WechatProperties; +import cn.novalon.gym.manage.member.service.WechatOfficialService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.MediaType; +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.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.util.Arrays; + +/** + * 微信公众号事件处理器 + * + * @author 付嘉 + * @date 2026-05-01 + */ + +@Slf4j +@Component +@RequiredArgsConstructor +public class WechatOfficialEventHandler { + + private final WechatOfficialService wechatOfficialService; + private final WechatProperties wechatProperties; + + /** + * 处理微信公众号事件 + * + * 请求格式:XML + * 响应格式:success 或 回复消息内容 + */ + public Mono handleEvent(ServerRequest request) { + return request.bodyToMono(String.class) + .flatMap(xmlBody -> { + log.info("收到微信公众号事件 {}", xmlBody); + + // TODO: 将XML解析为WechatOfficialEventDto + // 目前简化处理直接获取openId和event + + String openId = extractOpenId(xmlBody); + String event = extractEvent(xmlBody); + + if (openId == null || event == null) { + log.error("无法解析微信公众号事件"); + return ServerResponse.badRequest().bodyValue("error"); + } + + log.info("处理事件 openId={}, event={}", openId, event); + + // 根据事件类型处理 + if ("subscribe".equals(event)) { + return wechatOfficialService.handleSubscribeEvent(openId) + .then(ServerResponse.ok() + .contentType(MediaType.TEXT_PLAIN) + .bodyValue("success")); + } else if ("unsubscribe".equals(event)) { + return wechatOfficialService.handleUnsubscribeEvent(openId) + .then(ServerResponse.ok() + .contentType(MediaType.TEXT_PLAIN) + .bodyValue("success")); + } else { + log.warn("未知事件类型: {}", event); + return ServerResponse.ok() + .contentType(MediaType.TEXT_PLAIN) + .bodyValue("success"); + } + }) + .onErrorResume(e -> { + log.error("处理微信公众号事件失败", e); + return ServerResponse.ok() + .contentType(MediaType.TEXT_PLAIN) + .bodyValue("success"); // 即使处理失败也返回success避免微信重试 + }); + } + + /** + * 验证微信公众号签名 + * + * GET请求用于验证服务器地址 + */ + public Mono verifySignature(ServerRequest request) { + String signature = request.queryParam("signature").orElse(""); + String timestamp = request.queryParam("timestamp").orElse(""); + String nonce = request.queryParam("nonce").orElse(""); + String echostr = request.queryParam("echostr").orElse(""); + + log.info("========== 微信公众号签名验证 =========="); + log.info("收到的参数:"); + log.info(" signature: {}", signature); + log.info(" timestamp: {}", timestamp); + log.info(" nonce: {}", nonce); + log.info(" echostr: {}", echostr); + + // 获取配置的Token + String token = wechatProperties.getMp().getToken(); + log.info("配置的Token: {}", token); + + // 验证签名 + if (checkSignature(signature, timestamp, nonce, token)) { + log.info("签名验证成功,返回echostr: {}", echostr); + log.info("========== 微信公众号签名验证结束 =========="); + return ServerResponse.ok() + .contentType(MediaType.TEXT_PLAIN) + .bodyValue(echostr); + } else { + log.warn("签名验证失败"); + log.info("========== 微信公众号签名验证结束 =========="); + return ServerResponse.badRequest() + .contentType(MediaType.TEXT_PLAIN) + .bodyValue("error"); + } + } + + /** + * 验证签名 + * + * @param signature 微信加密签名 + * @param timestamp 时间戳 + * @param nonce 随机数 + * @param token Token + * @return 是否验证通过 + */ + private boolean checkSignature(String signature, String timestamp, String nonce, String token) { + // 1. 将token、timestamp、nonce三个参数进行字典序排序 + String[] arr = new String[]{token, timestamp, nonce}; + Arrays.sort(arr); + + // 2. 将三个参数字符串拼接成一个字符串 + StringBuilder sb = new StringBuilder(); + for (String str : arr) { + sb.append(str); + } + + // 3. 将拼接后的字符串进行sha1加密 + String encrypted = sha1(sb.toString()); + log.debug("计算的签名 {}", encrypted); + + // 4. 将加密后的字符串与signature对比 + return encrypted != null && encrypted.equalsIgnoreCase(signature); + } + + /** + * SHA1加密 + * + * @param str 待加密字符串 + * @return 加密后字符串 + */ + private String sha1(String str) { + try { + MessageDigest md = MessageDigest.getInstance("SHA-1"); + byte[] digest = md.digest(str.getBytes(StandardCharsets.UTF_8)); + StringBuilder hexString = new StringBuilder(); + for (byte b : digest) { + String hex = Integer.toHexString(0xff & b); + if (hex.length() == 1) { + hexString.append('0'); + } + hexString.append(hex); + } + return hexString.toString(); + } catch (Exception e) { + log.error("SHA1加密失败", e); + return null; + } + } + + /** + * 从XML中提取OpenID + */ + private String extractOpenId(String xml) { + int start = xml.indexOf(""); + int end = xml.indexOf(""); + if (start != -1 && end != -1) { + String value = xml.substring(start + 14, end); + // 去除 CDATA 标记 + return cleanCdata(value); + } + return null; + } + + /** + * 从XML中获取事件类型 + */ + private String extractEvent(String xml) { + int start = xml.indexOf(""); + int end = xml.indexOf(""); + if (start != -1 && end != -1) { + String value = xml.substring(start + 7, end); + // 去除 CDATA 标记 + return cleanCdata(value); + } + return null; + } + + /** + * 清理 CDATA 标记 + * 例如: -> subscribe + */ + private String cleanCdata(String value) { + if (value == null) { + return null; + } + // 去除前后空白 + value = value.trim(); + // 提取 CDATA 中间内容 + // 格式: + if (value.startsWith("")) { + return value.substring(9, value.length() - 3); + } + return value; + } +} diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/repository/IMemberRepository.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/repository/IMemberRepository.java new file mode 100644 index 0000000..e746aea --- /dev/null +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/repository/IMemberRepository.java @@ -0,0 +1,36 @@ +package cn.novalon.gym.manage.member.repository; + +import cn.novalon.gym.manage.member.entity.Member; +import org.springframework.data.domain.Pageable; +import org.springframework.data.r2dbc.repository.R2dbcRepository; +import org.springframework.stereotype.Repository; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +/** + * 会员Repository + * @author 付嘉 + * @date 2026-05-01 + */ + +@Repository +public interface IMemberRepository extends R2dbcRepository { + + // UnionID查询会员 + Mono findByUnionId(String unionId); + + // 小程序OpenID查询会员 + Mono findByMiniappOpenId(String miniappOpenId); + + // 服务号OpenID查询会员 + Mono findByOfficialOpenId(String officialOpenId); + + // 手机号查询 + Mono findByPhone(String phone); + + /** + * 分页查询所有会员 + * 方法名 findAllBy 是 Spring Data 的约定,表示按条件查询所有 + */ + Flux findAllBy(Pageable pageable); +} diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/repository/WechatUserRepository.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/repository/WechatUserRepository.java new file mode 100644 index 0000000..a0afe0b --- /dev/null +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/repository/WechatUserRepository.java @@ -0,0 +1,29 @@ +package cn.novalon.gym.manage.member.repository; + +import cn.novalon.gym.manage.member.entity.WechatUser; +import org.springframework.data.r2dbc.repository.R2dbcRepository; +import org.springframework.stereotype.Repository; +import reactor.core.publisher.Mono; + +/** + * 微信用户Repository + * + * @author 付嘉 + * @date 2026-05-01 + */ + +@Repository +public interface WechatUserRepository extends R2dbcRepository { + + // 通过UnionID查询微信用户 + Mono findByUnionId(String unionId); + + // 通过小程序OpenID查询微信用户 + Mono findByMiniappOpenid(String miniappOpenid); + + // 通过服务号OpenID查询微信用户 + Mono findByMpOpenid(String mpOpenid); + + // 通过会员ID查询微信用户 + Mono findByMemberId(Long memberId); +} diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/MemberService.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/MemberService.java new file mode 100644 index 0000000..c659f1b --- /dev/null +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/MemberService.java @@ -0,0 +1,61 @@ +package cn.novalon.gym.manage.member.service; + +import cn.novalon.gym.manage.member.dto.SearchMemberDto; +import cn.novalon.gym.manage.member.dto.UpdateMemberInfoDto; +import cn.novalon.gym.manage.member.entity.Member; +import cn.novalon.gym.manage.member.es.entity.MemberES; +import cn.novalon.gym.manage.member.vo.MemberInfoVO; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +/** + * 会员服务接口 + * + * @author 付嘉 + * @date 2026-05-01 + */ +public interface MemberService { + + /** + * 获取会员信息 + * + * @param memberId 会员ID + * @return 会员信息 + */ + Mono getMemberInfo(Long memberId); + + /** + * 会员更新个人信息 + * + * @param memberId 会员ID + * @param updateDto 更新信息DTO + * @return 更新后的会员信息 + */ + Mono updateMemberInfo(Long memberId, UpdateMemberInfoDto updateDto); + + /** + * 管理端更新会员手机号 + * + * @param memberId 会员ID + * @param phone 明文手机号 + * @return 是否成功 + */ + Mono adminUpdatePhone(Long memberId, String phone); + + /** + * 管理端查询会员(es查询) + * + * @param searchMemberDto 会员信息dto + * @return 会员信息 + */ + Flux searchMember(SearchMemberDto searchMemberDto); + + /** + * 管理端查询所有会员 + * + * @param pageNum 页码 + * @param pageSize 页大小 + * @return 所有会员信息 + */ + Flux findAll(Integer pageNum, Integer pageSize); +} diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/WechatApiService.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/WechatApiService.java new file mode 100644 index 0000000..3258cb9 --- /dev/null +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/WechatApiService.java @@ -0,0 +1,48 @@ +package cn.novalon.gym.manage.member.service; + +import reactor.core.publisher.Mono; + +import java.util.Map; + +/** + * 微信API服务接口 + * + * @author 付嘉 + * @date 2026-05-01 + */ +public interface WechatApiService { + + /** + * 小程序- 通过code获取session_key/openid + * + * @param code 小程序登录的code + * @return Mono> session_keyopenidunionid + */ + Mono> jsCode2Session(String code); + + /** + * 获取手机号- 通过code获取手机号 + * + * @param code 小程序登录的code + * @return Mono ܺ手机号 + */ + Mono getPhoneNumber(String code); + + /** + * 获取Access Token + * + * @param appType 应用类型miniapp-小程序mp + * @return Mono access_token + */ + Mono getAccessToken(String appType); + + /** + * 验证签名 + * + * @param signature 微信签名 + * @param timestamp 创建时间 + * @param nonce 随机字符串 + * @return boolean 签名是否有效 + */ + boolean checkSignature(String signature, String timestamp, String nonce); +} diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/WechatAuthService.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/WechatAuthService.java new file mode 100644 index 0000000..fc52de9 --- /dev/null +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/WechatAuthService.java @@ -0,0 +1,31 @@ +package cn.novalon.gym.manage.member.service; + +import cn.novalon.gym.manage.member.dto.WechatLoginDto; +import cn.novalon.gym.manage.member.vo.WechatLoginVO; +import reactor.core.publisher.Mono; + +/** + * 微信授权服务接口 + * + * @author 付嘉 + * @date 2026-05-01 + */ +public interface WechatAuthService { + + /** + * 小程序 + * + * @param request 小程序登录请求 + * @return 小程序登录响应 + */ + Mono miniappLogin(WechatLoginDto request); + + /** + * 手机号 + * + * @param memberId 会员ID + * @param code 微信手机号code + * @return 是否绑定成功 + */ + Mono bindPhone(Long memberId, String code); +} diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/WechatOfficialService.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/WechatOfficialService.java new file mode 100644 index 0000000..0a78d6f --- /dev/null +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/WechatOfficialService.java @@ -0,0 +1,54 @@ +package cn.novalon.gym.manage.member.service; + +import cn.novalon.gym.manage.member.vo.WechatUserInfoVO; +import reactor.core.publisher.Mono; + +/** + * 微信公众号服务接口 + * + * @author 付嘉 + * @date 2026-05-01 + */ +public interface WechatOfficialService { + + /** + * 处理订阅事件 + * + * @param openId OpenID + * @return Mono + */ + Mono handleSubscribeEvent(String openId); + + /** + * 处理取消订阅事件 + * + * @param openId OpenID + * @return Mono + */ + Mono handleUnsubscribeEvent(String openId); + + /** + * 获取微信用户信息 + * + * @param openId OpenID + * @return Mono 用户信息 + */ + Mono getUserInfo(String openId); + + /** + * 通过 UnionID 关联小程序用户 + * + * @param unionId 微信 UnionID + * @param officialOpenId OpenID + * @return Mono 是否关联成功 + */ + Mono linkByUnionId(String unionId, String officialOpenId); + + /** + * 查询会员订阅状态 + * + * @param memberId 会员ID + * @return Mono true=已订阅false=未订阅 + */ + Mono checkSubscribeStatus(Long memberId); +} diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/MemberServiceImpl.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/MemberServiceImpl.java new file mode 100644 index 0000000..d1b6c44 --- /dev/null +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/MemberServiceImpl.java @@ -0,0 +1,208 @@ +package cn.novalon.gym.manage.member.service.impl; + +import cn.novalon.gym.manage.common.exception.ConflictException; +import cn.novalon.gym.manage.common.exception.ErrorCode; +import cn.novalon.gym.manage.common.exception.NotFoundException; +import cn.novalon.gym.manage.common.exception.SystemException; +import cn.novalon.gym.manage.member.config.WechatProperties; +import cn.novalon.gym.manage.member.dto.SearchMemberDto; +import cn.novalon.gym.manage.member.dto.UpdateMemberInfoDto; +import cn.novalon.gym.manage.member.entity.Member; +import cn.novalon.gym.manage.member.es.entity.MemberES; +import cn.novalon.gym.manage.member.es.repository.MemberESRepository; +import cn.novalon.gym.manage.member.repository.IMemberRepository; +import cn.novalon.gym.manage.member.service.MemberService; +import cn.novalon.gym.manage.member.util.AesUtil; +import cn.novalon.gym.manage.member.util.EsSyncUtils; +import cn.novalon.gym.manage.member.vo.MemberInfoVO; +import jakarta.annotation.PostConstruct; +import lombok.RequiredArgsConstructor; +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.stereotype.Service; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.time.LocalDateTime; + +/** + * 会员服务实现 + * + * @author 付嘉 + * @date 2026-05-01 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class MemberServiceImpl implements MemberService { + + private final IMemberRepository memberRepository; + private final MemberESRepository memberESRepository; + private final EsSyncUtils esSyncUtils; + private final WechatProperties wechatProperties; + + private EsSyncUtils.EntitySyncer memberSyncer; + + @PostConstruct + public void init() { + this.memberSyncer = esSyncUtils.bind(Member.class, MemberES.class, memberESRepository); + } + + @Override + public Mono getMemberInfo(Long memberId) { + return memberRepository.findById(memberId) + .map(this::buildMemberInfoResponse) + .switchIfEmpty(Mono.error(() -> { + log.error("会员不存在: memberId={}", memberId); + throw new NotFoundException(ErrorCode.NOT_FOUND_USER, "会员不存在"); + })); + } + + @Override + public Mono updateMemberInfo(Long memberId, UpdateMemberInfoDto updateDto) { + log.info("会员更新个人信息, memberId: {}", memberId); + + return memberRepository.findById(memberId) + .flatMap(member -> { + if (updateDto.getNickname() != null) { + member.setNickname(updateDto.getNickname()); + } + if (updateDto.getGender() != null) { + member.setGender(updateDto.getGender()); + } + if (updateDto.getBirthday() != null) { + member.setBirthday(updateDto.getBirthday()); + } + if (updateDto.getAvatar() != null) { + member.setAvatar(updateDto.getAvatar()); + } + if (updateDto.getAddress() != null) { + member.setAddress(updateDto.getAddress()); + } + + return memberRepository.save(member); + }) + .doOnSuccess(memberSyncer::sync) + .map(savedMember -> { + log.info("会员信息更新成功, memberId: {}", savedMember.getId()); + return buildMemberInfoResponse(savedMember); + }) + .switchIfEmpty(Mono.error(() -> { + log.error("会员不存在: memberId={}", memberId); + throw new NotFoundException(ErrorCode.NOT_FOUND_USER, "会员不存在"); + })); + } + + private MemberInfoVO buildMemberInfoResponse(Member member) { + String phone = member.getPhone(); + String maskedPhone = phone != null ? phone.replace(phone.substring(3, 7), "****") : null; + + return MemberInfoVO.builder() + .id(member.getId()) + .nickname(member.getNickname()) + .phone(maskedPhone) + .gender(member.getGender()) + .birthday(member.getBirthday()) + .avatar(member.getAvatar()) + .hasPhone(phone != null) + .isSubscribed(member.getSubscribed() != null && member.getSubscribed()) + .build(); + } + + @Override + public Mono adminUpdatePhone(Long memberId, String phone) { + log.info("管理端录入手机号, memberId: {}, phone: {}", memberId, phone); + + String encryptedPhone; + try { + String secretKey = wechatProperties.getPhoneEncryption().getSecretKey(); + String iv = wechatProperties.getPhoneEncryption().getIv(); + encryptedPhone = AesUtil.encrypt(phone, secretKey, iv); + log.info("手机号加密成功"); + } catch (Exception e) { + log.error("手机号加密失败", e); + throw new SystemException(ErrorCode.SYSTEM_INTERNAL_ERROR, "手机号加密失败: " + e.getMessage()); + } + + return memberRepository.findByPhone(encryptedPhone) + .flatMap(existingMember -> { + if (existingMember.getId().equals(memberId)) { + log.warn("手机号已是当前用户的: memberId={}", memberId); + throw new ConflictException(ErrorCode.CONFLICT_DUPLICATE_USER, "重复绑定"); + } else { + log.warn("手机号已被其他用户绑定: memberId={}, existingMemberId={}", + memberId, existingMember.getId()); + throw new ConflictException(ErrorCode.CONFLICT_DUPLICATE_USER, "该手机号已被其他会员绑定"); + } + }) + .switchIfEmpty(Mono.defer(() -> { + log.info("手机号未被占用,可以绑定"); + return updateMemberPhone(memberId, encryptedPhone); + })); + } + + @Override + public Flux searchMember(SearchMemberDto searchMemberDto) { + log.info("搜索会员, searchValue: {}, filter: {}, pageNum: {}, pageSize: {}", + searchMemberDto.getSearchValue(), + searchMemberDto.getFilter(), + searchMemberDto.getPageNum(), + searchMemberDto.getPageSize()); + + String searchValue = searchMemberDto.getSearchValue(); + + if(searchValue != null && searchValue.matches("^1[3-9]\\d{9}$")){ + log.debug("搜索值为手机号格式,进行加密处理"); + String secretKey = wechatProperties.getPhoneEncryption().getSecretKey(); + String iv = wechatProperties.getPhoneEncryption().getIv(); + searchValue = AesUtil.encrypt(searchValue,secretKey,iv); + } + + Pageable pageable = PageRequest.of( + searchMemberDto.getPageNum() - 1, + searchMemberDto.getPageSize(), + Sort.by(Sort.Direction.DESC, "update_at") + ); + + return memberESRepository.findByMemberNoOrPhoneOrNicknameContainingAndGender( + searchValue, + searchValue, + searchValue, + searchMemberDto.getFilter() , + pageable + ); + } + + @Override + public Flux findAll(Integer pageNum, Integer pageSize) { + log.info("查询所有会员列表, pageNum: {}, pageSize: {}", pageNum, pageSize); + + Pageable pageable = PageRequest.of( + pageNum - 1, + pageSize + ); + + return memberRepository.findAllBy(pageable); + } + + private Mono updateMemberPhone(Long memberId, String encryptedPhone) { + return memberRepository.findById(memberId) + .flatMap(member -> { + member.setPhone(encryptedPhone); + member.setLastLoginAt(LocalDateTime.now()); + + return memberRepository.save(member) + .doOnSuccess(memberSyncer::sync) + .map(savedMember -> { + log.info("手机号录入成功, memberId: {}", savedMember.getId()); + return true; + }); + }) + .switchIfEmpty(Mono.error(() -> { + log.error("会员不存在: memberId={}", memberId); + throw new NotFoundException(ErrorCode.NOT_FOUND_USER, "会员不存在"); + })); + } +} diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/WechatApiServiceImpl.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/WechatApiServiceImpl.java new file mode 100644 index 0000000..f760ae4 --- /dev/null +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/WechatApiServiceImpl.java @@ -0,0 +1,221 @@ +package cn.novalon.gym.manage.member.service.impl; + +import cn.novalon.gym.manage.common.exception.ErrorCode; +import cn.novalon.gym.manage.common.exception.SystemException; +import cn.novalon.gym.manage.member.config.WechatProperties; +import cn.novalon.gym.manage.member.service.WechatApiService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Service; +import org.springframework.web.reactive.function.client.WebClient; +import reactor.core.publisher.Mono; + +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +/** + * 微信API服务实现 + * + * @author 付嘉 + * @date 2026-05-01 + */ + +@Slf4j +@Service +@RequiredArgsConstructor +public class WechatApiServiceImpl implements WechatApiService { + + private final WechatProperties wechatProperties; + + private final WebClient webClient = WebClient.builder() + .baseUrl("https://api.weixin.qq.com") + .codecs(configurer -> configurer.defaultCodecs().maxInMemorySize(10 * 1024 * 1024)) + .build(); + + @Override + public Mono> jsCode2Session(String code) { + log.info("微信jsCode2Session API"); + log.info("信息 - AppID: {}, AppSecret {}", + wechatProperties.getMiniapp().getAppId(), + wechatProperties.getMiniapp().getAppSecret() != null ? + wechatProperties.getMiniapp().getAppSecret().substring(0, Math.min(4, wechatProperties.getMiniapp().getAppSecret().length())) + "***" : "null"); + log.info(" - code: {}", code); + + return webClient.get() + .uri(uriBuilder -> uriBuilder + .path("/sns/jscode2session") + .queryParam("appid", wechatProperties.getMiniapp().getAppId()) + .queryParam("secret", wechatProperties.getMiniapp().getAppSecret()) + .queryParam("js_code", code) + .queryParam("grant_type", "authorization_code") + .build()) + .retrieve() + .bodyToMono(String.class) + .map(responseBody -> { + log.info("微信API响应: {}", responseBody); + + Map response; + try { + com.fasterxml.jackson.databind.ObjectMapper mapper = new com.fasterxml.jackson.databind.ObjectMapper(); + response = mapper.readValue(responseBody, Map.class); + } catch (Exception e) { + log.error("微信API响应失败", e); + throw new SystemException(ErrorCode.SYSTEM_INTERNAL_ERROR, "微信API响应失败: " + e.getMessage()); + } + + Map result = new HashMap<>(); + + if (response.containsKey("errcode")) { + Integer errcode = (Integer) response.get("errcode"); + String errmsg = (String) response.get("errmsg"); + log.error("微信API失败, errcode: {}, errmsg: {}", errcode, errmsg); + throw new SystemException(ErrorCode.SYSTEM_INTERNAL_ERROR, "微信API失败 [" + errcode + "]: " + errmsg); + } + + result.put("session_key", (String) response.get("session_key")); + result.put("openid", (String) response.get("openid")); + result.put("unionid", (String) response.get("unionid")); + + log.info("微信API响应成功, openid: {}, unionid: {}", + result.get("openid"), result.get("unionid")); + return result; + }) + .onErrorResume(e -> { + log.error("微信API响应异常 - URL: https://api.weixin.qq.com/sns/jscode2session"); + log.error("异常: {}", e.getClass().getName()); + log.error("异常信息: {}", e.getMessage()); + if (e.getCause() != null) { + log.error("异常原因: {}", e.getCause().getMessage()); + } + if (e instanceof SystemException) { + return Mono.error(e); + } + throw new SystemException(ErrorCode.SYSTEM_INTERNAL_ERROR, "微信API响应异常 " + e.getMessage()); + }); + } + + @Override + public Mono getPhoneNumber(String code) { + log.debug("微信getPhoneNumber API, code: {}", code); + + return getAccessToken("miniapp") + .flatMap(accessToken -> { + + Map requestBody = new HashMap<>(); + requestBody.put("code", code); + + return webClient.post() + .uri(uriBuilder -> uriBuilder + .path("/wxa/business/getuserphonenumber") + .queryParam("access_token", accessToken) + .build()) + .contentType(MediaType.APPLICATION_JSON) + .bodyValue(requestBody) + .retrieve() + .bodyToMono(Map.class) + .map(response -> { + if (response.containsKey("errcode") && + (Integer) response.get("errcode") == 0) { + + Map phoneInfo = + (Map) response.get("phone_info"); + String phoneNumber = (String) phoneInfo.get("purePhoneNumber"); + + log.info("获取手机号成功{}", phoneNumber); + return phoneNumber; + } else { + String errmsg = (String) response.get("errmsg"); + log.error("获取手机号失败 {}", errmsg); + throw new SystemException(ErrorCode.SYSTEM_INTERNAL_ERROR, "获取手机号失败 " + errmsg); + } + }); + }) + .onErrorResume(e -> { + log.error("获取手机号失败", e); + if (e instanceof SystemException) { + return Mono.error(e); + } + throw new SystemException(ErrorCode.SYSTEM_INTERNAL_ERROR, "获取手机号失败 " + e.getMessage()); + }); + } + + @Override + public Mono getAccessToken(String appType) { + log.debug("获取access_token, appType: {}", appType); + + String appId, appSecret; + if ("miniapp".equals(appType)) { + appId = wechatProperties.getMiniapp().getAppId(); + appSecret = wechatProperties.getMiniapp().getAppSecret(); + } else { + appId = wechatProperties.getMp().getAppId(); + appSecret = wechatProperties.getMp().getAppSecret(); + } + + return webClient.get() + .uri(uriBuilder -> uriBuilder + .path("/cgi-bin/token") + .queryParam("grant_type", "client_credential") + .queryParam("appid", appId) + .queryParam("secret", appSecret) + .build()) + .retrieve() + .bodyToMono(Map.class) + .map(response -> { + if (response.containsKey("access_token")) { + String accessToken = (String) response.get("access_token"); + Integer expiresIn = (Integer) response.get("expires_in"); + log.info("获取access_token成功, expires_in: {}s", expiresIn); + return accessToken; + } else { + String errmsg = (String) response.get("errmsg"); + log.error("获取access_token失败: {}", errmsg); + throw new SystemException(ErrorCode.SYSTEM_INTERNAL_ERROR, "获取access_token失败: " + errmsg); + } + }); + } + + @Override + public boolean checkSignature(String signature, String timestamp, String nonce) { + log.debug("验证微信消息签名, signature: {}, timestamp: {}, nonce: {}", + signature, timestamp, nonce); + + try { + String token = wechatProperties.getMp().getToken(); + + String[] arr = new String[]{token, timestamp, nonce}; + Arrays.sort(arr); + + StringBuilder content = new StringBuilder(); + for (String s : arr) { + content.append(s); + } + + MessageDigest md = MessageDigest.getInstance("SHA-1"); + byte[] digest = md.digest(content.toString().getBytes(StandardCharsets.UTF_8)); + + StringBuilder hexString = new StringBuilder(); + for (byte b : digest) { + String hex = Integer.toHexString(0xff & b); + if (hex.length() == 1) { + hexString.append('0'); + } + hexString.append(hex); + } + + String calculatedSignature = hexString.toString(); + + boolean isValid = calculatedSignature.equals(signature); + log.debug("验证微信消息签名结果: {}", isValid ? "通过" : "失败"); + + return isValid; + } catch (Exception e) { + log.error("验证微信消息签名异常", e); + return false; + } + } +} diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/WechatAuthServiceImpl.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/WechatAuthServiceImpl.java new file mode 100644 index 0000000..5e7f299 --- /dev/null +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/WechatAuthServiceImpl.java @@ -0,0 +1,300 @@ +package cn.novalon.gym.manage.member.service.impl; + +import cn.novalon.gym.manage.common.exception.ConflictException; +import cn.novalon.gym.manage.common.exception.ErrorCode; +import cn.novalon.gym.manage.common.exception.NotFoundException; +import cn.novalon.gym.manage.common.exception.SystemException; +import cn.novalon.gym.manage.member.config.WechatProperties; +import cn.novalon.gym.manage.member.dto.WechatLoginDto; +import cn.novalon.gym.manage.member.entity.Member; +import cn.novalon.gym.manage.member.es.entity.MemberES; +import cn.novalon.gym.manage.member.es.repository.MemberESRepository; +import cn.novalon.gym.manage.member.repository.IMemberRepository; +import cn.novalon.gym.manage.member.service.WechatApiService; +import cn.novalon.gym.manage.member.service.WechatAuthService; +import cn.novalon.gym.manage.member.util.AesUtil; +import cn.novalon.gym.manage.member.util.EsSyncUtils; +import cn.novalon.gym.manage.member.util.MemberNoGenerator; +import cn.novalon.gym.manage.member.util.WechatPhoneUtil; +import cn.novalon.gym.manage.member.vo.WechatLoginVO; +import cn.novalon.gym.manage.sys.security.JwtTokenProvider; +import jakarta.annotation.PostConstruct; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import reactor.core.publisher.Mono; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +/** + * 微信认证服务实现 + * + * @author 付嘉 + * @date 2026-05-01 + */ + +@Slf4j +@Service +@RequiredArgsConstructor +public class WechatAuthServiceImpl implements WechatAuthService { + + private final WechatApiService wechatApiService; + private final IMemberRepository memberRepository; + private final WechatProperties wechatProperties; + private final WechatPhoneUtil wechatPhoneUtil; + private final MemberESRepository memberESRepository; + private final EsSyncUtils esSyncUtils; + private final JwtTokenProvider jwtTokenProvider; + + private EsSyncUtils.EntitySyncer memberSyncer; + + @PostConstruct + public void init() { + this.memberSyncer = esSyncUtils.bind(Member.class, MemberES.class, memberESRepository); + } + + + @Override + public Mono miniappLogin(WechatLoginDto request) { + log.info("开始小程序登录"); + + return wechatApiService.jsCode2Session(request.getCode()) + .flatMap(sessionData -> { + String openid = sessionData.get("openid"); + String unionId = sessionData.get("unionid"); + String sessionKey = sessionData.get("session_key"); + + log.info("微信 API 返回: openid={}, unionid={}", openid, unionId); + + if (unionId != null && !unionId.isEmpty()) { + return memberRepository.findByUnionId(unionId) + .flatMap(member -> { + log.info("找到会员, memberId: {}", member.getId()); + + if (member.getMiniappOpenId() == null || member.getMiniappOpenId().isEmpty()) { + log.info("用户已有 UnionID,补充小程序 OpenID, memberId: {}", member.getId()); + member.setMiniappOpenId(openid); + member.setLastLoginAt(LocalDateTime.now()); + + return memberRepository.save(member) + .doOnSuccess(memberSyncer::sync) + .flatMap(savedMember -> { + WechatLoginVO response = buildLoginResponse(savedMember, false, sessionKey); + return Mono.just(response); + }); + } else { + log.info("老用户登录,更新最后登录时间, memberId: {}", member.getId()); + member.setLastLoginAt(LocalDateTime.now()); + + return memberRepository.save(member) + .doOnSuccess(memberSyncer::sync) + .flatMap(savedMember -> { + WechatLoginVO response = buildLoginResponse(savedMember, false, sessionKey); + return Mono.just(response); + }); + } + }) + .switchIfEmpty(Mono.defer(() -> { + log.info("UnionID 未找到,尝试通过小程序 OpenID 查询, openid: {}", openid); + return memberRepository.findByMiniappOpenId(openid) + .flatMap(member -> { + log.info("找到会员, memberId: {}", member.getId()); + member.setUnionId(unionId); + member.setLastLoginAt(LocalDateTime.now()); + + return memberRepository.save(member) + .doOnSuccess(memberSyncer::sync) + .flatMap(savedMember -> { + WechatLoginVO response = buildLoginResponse(savedMember, false, sessionKey); + return Mono.just(response); + }); + }) + .switchIfEmpty(Mono.defer(() -> { + log.info("OpenID 也未找到,创建新会员(无 UnionID), openid: {}", openid); + return createNewMember(unionId, openid, sessionKey, request.getPhoneCode()); + })); + })); + } else { + log.warn("微信 API 未返回 UnionID,尝试通过小程序 OpenID 查询, openid: {}", openid); + return memberRepository.findByMiniappOpenId(openid) + .flatMap(member -> { + log.info("找到会员, memberId: {}", member.getId()); + member.setLastLoginAt(LocalDateTime.now()); + + return memberRepository.save(member) + .doOnSuccess(memberSyncer::sync) + .flatMap(savedMember -> { + WechatLoginVO response = buildLoginResponse(savedMember, false, sessionKey); + return Mono.just(response); + }); + }) + .switchIfEmpty(Mono.defer(() -> { + log.info("OpenID也未找到,创建新会员(无UnionID标识信息)"); + return createNewMember(unionId, openid, sessionKey, request.getPhoneCode()); + })); + } + }) + .onErrorResume(e -> { + log.error("小程序登录失败", e); + if (e instanceof SystemException) { + return Mono.error(e); + } + return Mono.error(new SystemException(ErrorCode.SYSTEM_INTERNAL_ERROR, "登录失败: " + e.getMessage())); + }); + } + + @Override + public Mono bindPhone(Long memberId, String code) { + log.info("开始绑定手机号, memberId: {}", memberId); + + return wechatApiService.getPhoneNumber(code) + .flatMap(phoneNumber -> { + log.info("获取手机号: {}", phoneNumber); + + String encryptedPhone = encryptPhone(phoneNumber); + + return memberRepository.findByPhone(encryptedPhone) + .flatMap(existingMember -> { + if (!existingMember.getId().equals(memberId)) { + log.warn("手机号已被其他会员绑定, currentMemberId={}, existingMemberId={}", memberId, existingMember.getId()); + throw new ConflictException(ErrorCode.CONFLICT_DUPLICATE_USER, "手机号已被其他会员绑定"); + } else { + log.info("更新会员手机号, memberId: {}", memberId); + return updateMemberPhone(memberId, encryptedPhone); + } + }) + .switchIfEmpty(Mono.defer(() -> { + log.info("该手机号未被使用,直接绑定到当前会员, memberId: {}", memberId); + return updateMemberPhone(memberId, encryptedPhone); + })); + }) + .onErrorResume(e -> { + log.error("绑定手机号失败", e); + if (e instanceof SystemException) { + return Mono.error(e); + } + return Mono.error(new SystemException(ErrorCode.SYSTEM_INTERNAL_ERROR, "绑定失败: " + e.getMessage())); + }); + } + + private Mono updateMemberPhone(Long memberId, String encryptedPhone) { + return memberRepository.findById(memberId) + .flatMap(member -> { + member.setPhone(encryptedPhone); + member.setLastLoginAt(LocalDateTime.now()); + return memberRepository.save(member) + .doOnSuccess(memberSyncer::sync) + .map(savedMember -> { + log.info("更新会员手机号成功, memberId: {}", savedMember.getId()); + return true; + }); + }) + .switchIfEmpty(Mono.error(() -> { + log.error("会员不存在, memberId={}", memberId); + throw new NotFoundException(ErrorCode.NOT_FOUND_USER, "会员不存在"); + })); + } + + private String encryptPhone(String phoneNumber) { + try { + String secretKey = wechatProperties.getPhoneEncryption().getSecretKey(); + String iv = wechatProperties.getPhoneEncryption().getIv(); + + String encryptedPhone = AesUtil.encrypt(phoneNumber, secretKey, iv); + + log.debug("手机号加密成功"); + return encryptedPhone; + } catch (Exception e) { + log.error("手机号加密失败", e); + throw new SystemException(ErrorCode.SYSTEM_INTERNAL_ERROR, "手机号加密失败 " + e.getMessage()); + } + } + + public String decryptPhone(String encryptedPhone) { + try { + String secretKey = wechatProperties.getPhoneEncryption().getSecretKey(); + String iv = wechatProperties.getPhoneEncryption().getIv(); + + String phoneNumber = AesUtil.decrypt(encryptedPhone, secretKey, iv); + + log.debug("手机号解密成功"); + return phoneNumber; + } catch (Exception e) { + log.error("手机号解密失败", e); + throw new SystemException(ErrorCode.SYSTEM_INTERNAL_ERROR, "手机号解密失败 " + e.getMessage()); + } + } + + private Mono createNewMember(String unionId, String openid, String sessionKey, String phoneCode) { + log.info("开始创建新会员, unionId: {}, openid: {}", unionId, openid); + + String memberNo = MemberNoGenerator.generate(); + log.info("生成会员号: {}", memberNo); + + Member member = Member.builder() + .memberNo(memberNo) + .unionId(unionId) + .miniappOpenId(openid) + .lastLoginAt(LocalDateTime.now()) + .build(); + + log.info("用户未注册,创建新会员(仅保存标识信息)"); + + return memberRepository.save(member) + .doOnSuccess(memberSyncer::sync) + .flatMap(savedMember -> { + log.info("保存 Member 成功, id: {}, memberNo: {}", savedMember.getId(), savedMember.getMemberNo()); + if (phoneCode != null && !phoneCode.isEmpty()) { + log.info("检测到 phoneCode,尝试获取手机号"); + return wechatPhoneUtil.getPhoneNumber(phoneCode) + .flatMap(phoneNumber -> { + if (phoneNumber != null && !phoneNumber.isEmpty()) { + log.info("获取到手机号: {}", phoneNumber); + String encryptedPhone = encryptPhone(phoneNumber); + savedMember.setPhone(encryptedPhone); + return memberRepository.save(savedMember) + .doOnSuccess(memberSyncer::sync) + .doOnSuccess(m -> { + log.info("新用户手机号绑定成功"); + }) + .thenReturn(buildLoginResponse(savedMember, true, sessionKey)); + } else { + log.warn("未获取到手机号"); + return Mono.just(buildLoginResponse(savedMember, true, sessionKey)); + } + }); + } else { + return Mono.just(buildLoginResponse(savedMember, true, sessionKey)); + } + }); + } + + private WechatLoginVO buildLoginResponse(Member member, boolean isNewUser, String sessionKey) { + log.debug("构建登录响应, memberId: {}, isNewUser: {}", member.getId(), isNewUser); + + boolean needCompleteInfo = member.getNickname() == null || member.getPhone() == null; + if (needCompleteInfo) { + log.info("用户需要补全信息: nickname={}, phone={}", + member.getNickname() != null ? "已有" : "未设置", + member.getPhone() != null ? "已绑定" : "未绑定"); + } + + List roles = new ArrayList<>(); + String accessToken = jwtTokenProvider.generateToken(String.valueOf(member.getId()), member.getId(), roles); + + log.info("JWT Token 生成成功, memberId: {}", member.getId()); + + int expiresIn = 86400; + + return WechatLoginVO.builder() + .memberId(member.getId()) + .accessToken(accessToken) + .refreshToken(accessToken) + .expiresIn(expiresIn) + .isNewUser(isNewUser) + .needCompleteInfo(needCompleteInfo) + .build(); + } +} diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/WechatOfficialServiceImpl.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/WechatOfficialServiceImpl.java new file mode 100644 index 0000000..b5b863a --- /dev/null +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/WechatOfficialServiceImpl.java @@ -0,0 +1,338 @@ +package cn.novalon.gym.manage.member.service.impl; + +import cn.novalon.gym.manage.member.config.WechatProperties; +import cn.novalon.gym.manage.member.entity.Member; +import cn.novalon.gym.manage.member.es.entity.MemberES; +import cn.novalon.gym.manage.member.es.repository.MemberESRepository; +import cn.novalon.gym.manage.member.repository.IMemberRepository; +import cn.novalon.gym.manage.member.service.WechatOfficialService; +import cn.novalon.gym.manage.member.util.EsSyncUtils; +import cn.novalon.gym.manage.member.vo.WechatUserInfoVO; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.annotation.PostConstruct; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Service; +import org.springframework.web.reactive.function.client.WebClient; +import reactor.core.publisher.Mono; + +import java.time.LocalDateTime; +import java.util.HashMap; +import java.util.Map; + +/** + * 微信服务号服务实现类 + * + * @author 付嘉 + * @date 2026-05-01 + */ + +@Slf4j +@Service +@RequiredArgsConstructor +public class WechatOfficialServiceImpl implements WechatOfficialService { + + private final IMemberRepository memberRepository; + private final WechatProperties wechatProperties; + private final WebClient webClient; + private final MemberESRepository memberESRepository; + private final ObjectMapper objectMapper = new ObjectMapper() + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + + private final EsSyncUtils esSyncUtils; + + private EsSyncUtils.EntitySyncer memberSyncer; + + @PostConstruct + public void init() { + this.memberSyncer = esSyncUtils.bind(Member.class, MemberES.class, memberESRepository); + } + + /** + * 处理关注事件 + */ + @Override + public Mono handleSubscribeEvent(String openId) { + log.info("处理关注事件, openId: {}", openId); + + return getUserInfo(openId) + .flatMap(userInfo -> { + String unionId = userInfo.getUnionid(); + log.info("获取到用户信息 unionId: {}, nickname: {}", unionId, userInfo.getNickname()); + + if (unionId != null && !unionId.isEmpty()) { + return memberRepository.findByUnionId(unionId) + .flatMap(existingMember -> { + log.info("通过UnionID找到已有会员, memberId: {}", existingMember.getId()); + + if (existingMember.getOfficialOpenId() == null || existingMember.getOfficialOpenId().isEmpty()) { + log.info("用户先使用小程序,更新服务号OpenID: {}", openId); + existingMember.setSubscribed(true); + existingMember.setLastLoginAt(LocalDateTime.now()); + existingMember.setOfficialOpenId(openId); + + if (existingMember.getNickname() == null || existingMember.getNickname().isEmpty()) { + existingMember.setNickname(userInfo.getNickname()); + } + + if (existingMember.getAvatar() == null || existingMember.getAvatar().isEmpty()) { + existingMember.setAvatar(userInfo.getHeadimgurl()); + } + + return memberRepository.save(existingMember) + .doOnSuccess(memberSyncer::sync) + .then(sendWelcomeMessage(openId)); + } else { + log.info("老用户关注服务号: memberId={}", existingMember.getId()); + existingMember.setSubscribed(true); + existingMember.setLastLoginAt(LocalDateTime.now()); + + return memberRepository.save(existingMember) + .doOnSuccess(memberSyncer::sync) + .then(sendWelcomeMessage(openId)); + } + }) + .switchIfEmpty(Mono.defer(() -> { + log.info("UnionID未找到,降级到服务号OpenID查询: {}", openId); + return memberRepository.findByOfficialOpenId(openId) + .flatMap(existingMember -> { + log.info("通过服务号OpenID找到已有会员,更新UnionID, memberId: {}", existingMember.getId()); + existingMember.setUnionId(unionId); + existingMember.setSubscribed(true); + existingMember.setLastLoginAt(LocalDateTime.now()); + + return memberRepository.save(existingMember) + .doOnSuccess(memberSyncer::sync) + .then(sendWelcomeMessage(openId)); + }) + .switchIfEmpty(Mono.defer(() -> { + log.info("OpenID也未找到,创建新用户"); + return createNewMemberFromOfficial(unionId, openId) + .then(sendWelcomeMessage(openId)); + })); + })); + } else { + log.warn("用户没有UnionID,尝试通过服务号OpenID查询"); + return memberRepository.findByOfficialOpenId(openId) + .flatMap(existingMember -> { + log.info("通过服务号OpenID找到已有会员, memberId: {}", existingMember.getId()); + existingMember.setSubscribed(true); + existingMember.setLastLoginAt(LocalDateTime.now()); + + return memberRepository.save(existingMember) + .doOnSuccess(memberSyncer::sync) + .then(sendWelcomeMessage(openId)); + }) + .switchIfEmpty(Mono.defer(() -> { + log.info("OpenID也未找到,创建新会员(无UnionID)"); + return createNewMemberFromOfficial(unionId, openId) + .then(sendWelcomeMessage(openId)); + })); + } + }) + .then(); + } + + /** + * 处理取消关注事件 + */ + @Override + public Mono handleUnsubscribeEvent(String openId) { + log.info("处理取消关注事件, openId: {}", openId); + + return memberRepository.findByOfficialOpenId(openId) + .flatMap(member -> { + log.info("找到会员,更新为未关注状态, memberId: {}", member.getId()); + member.setSubscribed(false); + member.setLastLoginAt(LocalDateTime.now()); + return memberRepository.save(member) + .doOnSuccess(memberSyncer::sync) + .then(); + }) + .then() + .switchIfEmpty(Mono.defer(() -> { + log.warn("未找到对应的会员记录, officialOpenId: {}", openId); + return Mono.empty(); + })) + .onErrorResume(e -> { + log.error("处理取消关注事件失败", e); + return Mono.empty(); + }); + } + + /** + * 获取微信用户信息 + */ + @Override + public Mono getUserInfo(String openId) { + log.debug("获取微信用户信息, openId: {}", openId); + + // 获取AccessToken + return getAccessToken() + .flatMap(accessToken -> { + String url = "https://api.weixin.qq.com/cgi-bin/user/info" + + "?access_token=" + accessToken + + "&openid=" + openId + + "&lang=zh_CN"; + + return webClient.get() + .uri(url) + .accept(MediaType.APPLICATION_JSON) + .retrieve() + .bodyToMono(String.class) + .map(responseJson -> { + log.info("微信API原始响应: {}", responseJson); + try { + WechatUserInfoVO userInfo = objectMapper.readValue(responseJson, WechatUserInfoVO.class); + log.info("解析后的用户信息 - unionId: {}, nickname: {}, openid: {}", + userInfo.getUnionid(), userInfo.getNickname(), userInfo.getOpenid()); + return userInfo; + } catch (Exception e) { + log.error("解析微信用户信息失败", e); + throw new RuntimeException("解析微信用户信息失败", e); + } + }); + }); + } + + /** + * 通过 UnionID 关联小程序用户和服务号用户 + */ + @Override + public Mono linkByUnionId(String unionId, String officialOpenId) { + log.debug("关联小程序用户和服务号用户 unionId: {}, officialOpenId: {}", unionId, officialOpenId); + + return memberRepository.findByUnionId(unionId) + .flatMap(member -> { + member.setSubscribed(true); + member.setLastLoginAt(LocalDateTime.now()); + + if (officialOpenId != null && !officialOpenId.isEmpty()) { + member.setOfficialOpenId(officialOpenId); + } + return memberRepository.save(member) + .doOnSuccess(memberSyncer::sync) + .map(savedMember -> { + log.info("关联成功, memberId: {}", savedMember.getId()); + return true; + }); + }) + .switchIfEmpty(Mono.defer(() -> { + log.warn("未找到对应的会员记录, unionId: {}", unionId); + return Mono.just(false); + })) + .onErrorResume(e -> { + log.error("关联失败", e); + return Mono.just(false); + }); + } + + /** + * 查询用户关注状态 + */ + @Override + public Mono checkSubscribeStatus(Long memberId) { + log.info("查询用户关注状态, memberId: {}", memberId); + + return memberRepository.findById(memberId) + .map(member -> { + Boolean subscribed = member.getSubscribed(); + boolean result = subscribed != null && subscribed; + log.info("查询用户关注状态结果, memberId: {}, subscribed: {}", memberId, result); + return result; + }) + .defaultIfEmpty(false); + } + + /** + * 从服务号用户信息创建新会员 + */ + private Mono createNewMemberFromOfficial(String unionId, String openId) { + Member member = Member.builder() + .unionId(unionId) + .officialOpenId(openId) + .subscribed(true) + .lastLoginAt(LocalDateTime.now()) + .build(); + + log.info("新用户关注服务号,仅保存标识信息(UnionID和OpenID)"); + return memberRepository.save(member) + .doOnSuccess(memberSyncer::sync) + .doOnSuccess(savedMember -> { + log.info("从服务号创建新会员成功, memberId: {}", savedMember.getId()); + }) + .then(); + } + + /** + * 获取微信AccessToken + * + * TODO: 应该使用缓存,避免频繁请求 + */ + private Mono getAccessToken() { + String appId = wechatProperties.getMp().getAppId(); + String appSecret = wechatProperties.getMp().getAppSecret(); + + String url = "https://api.weixin.qq.com/cgi-bin/token" + + "?grant_type=client_credential" + + "&appid=" + appId + + "&secret=" + appSecret; + + return webClient.get() + .uri(url) + .accept(MediaType.APPLICATION_JSON) + .retrieve() + .bodyToMono(Map.class) + .map(response -> { + if (response.containsKey("errcode")) { + throw new RuntimeException("获取AccessToken失败: " + response.get("errmsg")); + } + return (String) response.get("access_token"); + }); + } + + /** + * 发送欢迎消息给用户 + */ + private Mono sendWelcomeMessage(String openId) { + log.info("发送欢迎消息给 openId: {}", openId); + + // 真正调用微信 API 发送消息(测试模式和生产模式都执行) + return getAccessToken() + .flatMap(accessToken -> { + String url = "https://api.weixin.qq.com/cgi-bin/message/custom/send" + + "?access_token=" + accessToken; + + // 构建客服消息�? + Map messageBody = new HashMap<>(); + messageBody.put("touser", openId); + messageBody.put("msgtype", "text"); + + Map text = new HashMap<>(); + text.put("content", "欢迎使用"); + messageBody.put("text", text); + + return webClient.post() + .uri(url) + .contentType(MediaType.APPLICATION_JSON) + .bodyValue(messageBody) + .retrieve() + .bodyToMono(Map.class) + .doOnSuccess(response -> { + if (response.containsKey("errcode") && !"0".equals(String.valueOf(response.get("errcode")))) { + log.error("发送欢迎消息失败: {}", response.get("errmsg")); + } else { + log.info("欢迎消息发送成功, openId: {}", openId); + } + }) + .doOnError(error -> log.error("发送欢迎消息异常", error)) + .then(); + }) + .onErrorResume(e -> { + log.error("发送欢迎消息失败, openId: {}", openId, e); + return Mono.empty(); // 即使发送失败也不影响主流程 + }); + } +} diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/util/AesUtil.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/util/AesUtil.java new file mode 100644 index 0000000..f54a003 --- /dev/null +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/util/AesUtil.java @@ -0,0 +1,77 @@ +package cn.novalon.gym.manage.member.util; + +import lombok.extern.slf4j.Slf4j; + +import javax.crypto.Cipher; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.SecretKeySpec; +import java.nio.charset.StandardCharsets; +import java.util.Base64; + +/** + * AES加密工具类 + * + * @author 付嘉 + * @date 2026-05-01 + */ + +@Slf4j +public class AesUtil { + + private static final String ALGORITHM = "AES"; + private static final String TRANSFORMATION = "AES/CBC/PKCS5Padding"; + + /** + * AES 解密 + * + * @param encryptedData 加密数据,Base64编码 + * @param key AES密钥,Base64编码(32字节) + * @param iv 初始化向量IV,Base64编码(16字节) + * @return 解密后的字符串 + */ + public static String decrypt(String encryptedData, String key, String iv) { + try { + byte[] dataByte = Base64.getDecoder().decode(encryptedData); + byte[] keyByte = Base64.getDecoder().decode(key); + byte[] ivByte = Base64.getDecoder().decode(iv); + + Cipher cipher = Cipher.getInstance(TRANSFORMATION); + SecretKeySpec secretKeySpec = new SecretKeySpec(keyByte, ALGORITHM); + IvParameterSpec ivParameterSpec = new IvParameterSpec(ivByte); + cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, ivParameterSpec); + + byte[] resultByte = cipher.doFinal(dataByte); + return new String(resultByte, StandardCharsets.UTF_8); + } catch (Exception e) { + log.error("AES解密失败", e); + throw new RuntimeException("解密失败", e); + } + } + + /** + * AES 加密 + * + * @param data 原始数据 + * @param key AES密钥,Base64编码(32字节) + * @param iv 初始化向量IV,Base64编码(16字节) + * @return Base64编码的加密数据 + */ + public static String encrypt(String data, String key, String iv) { + try { + byte[] dataByte = data.getBytes(StandardCharsets.UTF_8); + byte[] keyByte = Base64.getDecoder().decode(key); + byte[] ivByte = Base64.getDecoder().decode(iv); + + Cipher cipher = Cipher.getInstance(TRANSFORMATION); + SecretKeySpec secretKeySpec = new SecretKeySpec(keyByte, ALGORITHM); + IvParameterSpec ivParameterSpec = new IvParameterSpec(ivByte); + cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, ivParameterSpec); + + byte[] resultByte = cipher.doFinal(dataByte); + return Base64.getEncoder().encodeToString(resultByte); + } catch (Exception e) { + log.error("AES加密失败", e); + throw new RuntimeException("加密失败", e); + } + } +} diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/util/EsSyncUtils.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/util/EsSyncUtils.java new file mode 100644 index 0000000..e4db37d --- /dev/null +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/util/EsSyncUtils.java @@ -0,0 +1,159 @@ +package cn.novalon.gym.manage.member.util; + +import cn.hutool.core.bean.BeanUtil; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.elasticsearch.repository.ReactiveElasticsearchRepository; +import org.springframework.stereotype.Component; +import reactor.core.publisher.Mono; + +/** + * 通用 ES 同步工具类 + * + * 使用方式: + * + * 1. 注入工具类 + * @Autowired private EsSyncUtils esSyncUtils; + * + * 2. 同步数据到 ES(不返回结果,适合 doOnSuccess) + * esSyncUtils.sync(Member.class, MemberES.class, member, memberESRepository); + * + * 3. 同步数据到 ES(返回 Mono,适合链式调用) + * esSyncUtils.syncToES(Member.class, MemberES.class, member, memberESRepository).subscribe(); + * + * 4. 如果 Repository 是单例,可以先绑定 + * var syncer = esSyncUtils.bind(Member.class, MemberES.class, memberESRepository); + * syncer.sync(member); + */ +@Slf4j +@Component +@RequiredArgsConstructor +public class EsSyncUtils { + + /** + * 同步实体到 ES(不返回结果,适合 doOnSuccess) + * + * @param sourceClass 源实体类(如 Member.class) + * @param targetClass 目标ES实体类(如 MemberES.class) + * @param source 源实体对象 + * @param repository ES Repository + * @param 源实体类型 + * @param ES实体类型 + * @param ID类型 + */ + public void sync(Class sourceClass, Class targetClass, + S source, ReactiveElasticsearchRepository repository) { + if (source == null) { + log.warn("同步 ES 失败:源实体为空"); + return; + } + + try { + T target = BeanUtil.toBean(source, targetClass); + repository.save(target).subscribe( + success -> log.debug("同步到 ES 成功, 类型: {}", targetClass.getSimpleName()), + error -> log.error("同步到 ES 失败, 类型: {}", targetClass.getSimpleName(), error) + ); + } catch (Exception e) { + log.error("转换 ES 实体失败, 源类型: {}, 目标类型: {}", + sourceClass.getSimpleName(), targetClass.getSimpleName(), e); + } + } + + /** + * 同步实体到 ES(返回 Mono,适合链式调用) + * + * @param sourceClass 源实体类 + * @param targetClass 目标ES实体类 + * @param source 源实体对象 + * @param repository ES Repository + * @return Mono + */ + public Mono syncToES(Class sourceClass, Class targetClass, + S source, ReactiveElasticsearchRepository repository) { + if (source == null) { + log.warn("同步 ES 失败:源实体为空"); + return Mono.empty(); + } + + try { + T target = BeanUtil.toBean(source, targetClass); + return repository.save(target) + .doOnSuccess(t -> log.debug("同步到 ES 成功, 类型: {}", targetClass.getSimpleName())) + .doOnError(e -> log.error("同步到 ES 失败, 类型: {}", targetClass.getSimpleName(), e)) + .then(); + } catch (Exception e) { + log.error("转换 ES 实体失败, 源类型: {}, 目标类型: {}", + sourceClass.getSimpleName(), targetClass.getSimpleName(), e); + return Mono.empty(); + } + } + + /** + * 绑定 Repository,返回一个针对特定实体类型的同步器 + * + * @param sourceClass 源实体类 + * @param targetClass 目标ES实体类 + * @param repository ES Repository + * @return 实体同步器 + */ + public EntitySyncer bind(Class sourceClass, Class targetClass, + ReactiveElasticsearchRepository repository) { + return new EntitySyncer<>(sourceClass, targetClass, repository); + } + + /** + * 实体同步器(绑定特定类型的同步器,避免重复传 Class) + * + * @param 源实体类型 + * @param ES实体类型 + * @param ID类型 + */ + public static class EntitySyncer { + private final Class sourceClass; + private final Class targetClass; + private final ReactiveElasticsearchRepository repository; + + public EntitySyncer(Class sourceClass, Class targetClass, + ReactiveElasticsearchRepository repository) { + this.sourceClass = sourceClass; + this.targetClass = targetClass; + this.repository = repository; + } + + /** + * 同步(不返回结果) + */ + public void sync(S source) { + if (source == null) return; + try { + T target = BeanUtil.toBean(source, targetClass); + repository.save(target).subscribe( + success -> log.debug("同步到 ES 成功, 类型: {}", targetClass.getSimpleName()), + error -> log.error("同步到 ES 失败, 类型: {}", targetClass.getSimpleName(), error) + ); + } catch (Exception e) { + log.error("转换 ES 实体失败, 源类型: {}, 目标类型: {}", + sourceClass.getSimpleName(), targetClass.getSimpleName(), e); + } + } + + /** + * 同步(返回 Mono) + */ + public Mono syncMono(S source) { + if (source == null) return Mono.empty(); + try { + T target = BeanUtil.toBean(source, targetClass); + return repository.save(target) + .doOnSuccess(t -> log.debug("同步到 ES 成功, 类型: {}", targetClass.getSimpleName())) + .doOnError(e -> log.error("同步到 ES 失败, 类型: {}", targetClass.getSimpleName(), e)) + .then(); + } catch (Exception e) { + log.error("转换 ES 实体失败, 源类型: {}, 目标类型: {}", + sourceClass.getSimpleName(), targetClass.getSimpleName(), e); + return Mono.empty(); + } + } + } +} \ No newline at end of file diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/util/MemberNoGenerator.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/util/MemberNoGenerator.java new file mode 100644 index 0000000..2163923 --- /dev/null +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/util/MemberNoGenerator.java @@ -0,0 +1,53 @@ +package cn.novalon.gym.manage.member.util; + +import lombok.extern.slf4j.Slf4j; + +import java.security.SecureRandom; + +/** + * 会员号生成器 + * + * @author 付嘉 + * @date 2026-05-01 + */ + +@Slf4j +public class MemberNoGenerator { + + // 会员号前缀 + private static final String PREFIX = "GYM"; + + // 随机数长度 + private static final int RANDOM_LENGTH = 8; + + // 字符集(排除易混淆字符 0/O, 1/I/l + private static final String CHARACTERS = "23456789ABCDEFGHJKLMNPQRSTUVWXYZ"; + + // SecureRandom 实例(线程安全) + private static final SecureRandom SECURE_RANDOM = new SecureRandom(); + + // 生成会员号 + public static String generate() { + StringBuilder sb = new StringBuilder(PREFIX); + + for (int i = 0; i < RANDOM_LENGTH; i++) { + int index = SECURE_RANDOM.nextInt(CHARACTERS.length()); + sb.append(CHARACTERS.charAt(index)); + } + + String memberNo = sb.toString(); + log.debug("生成会员号: {}", memberNo); + + return memberNo; + } + + // 批量生成会员号 + // count 生成数量 + public static String[] generateBatch(int count) { + String[] memberNos = new String[count]; + for (int i = 0; i < count; i++) { + memberNos[i] = generate(); + } + return memberNos; + } +} diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/util/WechatPhoneUtil.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/util/WechatPhoneUtil.java new file mode 100644 index 0000000..e4f3c4e --- /dev/null +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/util/WechatPhoneUtil.java @@ -0,0 +1,65 @@ +package cn.novalon.gym.manage.member.util; + +import cn.novalon.gym.manage.member.service.WechatApiService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import reactor.core.publisher.Mono; + +/** + * 微信手机号获取工具类 + * + * @author 付嘉 + * @date 2026-05-01 + */ + +@Slf4j +@Component +@RequiredArgsConstructor +public class WechatPhoneUtil { + + private final WechatApiService wechatApiService; + + /** + * 通过phoneCode获取手机号 + * + * @param phoneCode 手机号 code + * @return Mono 手机号,获取失败则返回 null + */ + public Mono getPhoneNumber(String phoneCode) { + if (phoneCode == null || phoneCode.isEmpty()) { + log.debug("未提供phoneCode获取手机号"); + return Mono.empty(); + } + + log.info("开始获取手机号, phoneCode: {}", phoneCode); + + return wechatApiService.getPhoneNumber(phoneCode) + .doOnSuccess(phoneNumber -> { + if (phoneNumber != null && !phoneNumber.isEmpty()) { + log.info("获取手机号成功: {}", maskPhone(phoneNumber)); + } else { + log.warn("获取手机号失败"); + } + }) + .doOnError(e -> { + log.warn("获取手机号失败 {}", e.getMessage()); + }) + .onErrorResume(e -> { + return Mono.empty(); + }); + } + + /** + * 手机号脱敏处理 + * + * @param phone 手机号 + * @return 脱敏后的手机号,如:138****8000 + */ + private String maskPhone(String phone) { + if (phone == null || phone.length() < 7) { + return "***"; + } + return phone.substring(0, 3) + "****" + phone.substring(7); + } +} diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/vo/MemberInfoVO.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/vo/MemberInfoVO.java new file mode 100644 index 0000000..d8b60e4 --- /dev/null +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/vo/MemberInfoVO.java @@ -0,0 +1,46 @@ +package cn.novalon.gym.manage.member.vo; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.Date; + +/** + * 会员信息 VO + * + * @author 付嘉 + * @date 2026-05-01 + */ + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class MemberInfoVO { + + // 会员 ID + private Long id; + + // 昵称 + private String nickname; + + // 手机号(脱敏) + private String phone; + + // 性别 + private Integer gender; + + // 生日 + private Date birthday; + + // 头像 + private String avatar; + + // 是否已绑定手机号 + private Boolean hasPhone; + + // 是否已关注公众号 + private Boolean isSubscribed; +} diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/vo/WechatLoginVO.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/vo/WechatLoginVO.java new file mode 100644 index 0000000..b47370b --- /dev/null +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/vo/WechatLoginVO.java @@ -0,0 +1,38 @@ +package cn.novalon.gym.manage.member.vo; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 微信登录VO + * + * @author 付嘉 + * @date 2026-05-01 + */ + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class WechatLoginVO { + + // 会员 ID + private Long memberId; + + // Access Token(访问令牌) + private String accessToken; + + // Refresh Token(刷新令牌) + private String refreshToken; + + // Token 过期时间(秒) + private Integer expiresIn; + + // 是否为新用户 + private Boolean isNewUser; + + // 是否需要补全信息(昵称、手机号等) + private Boolean needCompleteInfo; +} diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/vo/WechatUserInfoVO.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/vo/WechatUserInfoVO.java new file mode 100644 index 0000000..8fc4735 --- /dev/null +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/vo/WechatUserInfoVO.java @@ -0,0 +1,72 @@ +package cn.novalon.gym.manage.member.vo; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import java.util.List; + +/** + * 微信用户信息 VO + * + * @author 付嘉 + * @date 2026-05-01 + */ + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class WechatUserInfoVO { + + // 用户是否关注该公众号(true-已关注,false-未关注) + private Boolean subscribe; + + // 服务号 OpenID + private String openid; + + // 用户昵称 + private String nickname; + + // 性别(1-男,2-女,0-未知) + private Integer sex; + + // 国家 + private String country; + + // 省份 + private String province; + + // 城市 + private String city; + + // 语言 + private String language; + + // 头像 URL + private String headimgurl; + + // 关注时间 + private Long subscribeTime; + + // UnionID + private String unionid; + + // 公众号运营者对粉丝的备注 + private String remark; + + // 用户所在的分组 ID + private Integer groupid; + + // 用户被打上的标签 ID 列表 + private List tagidList; + + // 关注来源 + private Integer subscribeScene; + + // 二维码场景值 + private String qrScene; + + // 二维码场景值字符串 + private String qrSceneStr; +} diff --git a/gym-manage-api/gym-member/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/gym-manage-api/gym-member/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000..18cc11f --- /dev/null +++ b/gym-manage-api/gym-member/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +cn.novalon.gym.manage.member.config.WechatProperties diff --git a/gym-manage-api/gym-member/src/main/resources/db/schema.sql b/gym-manage-api/gym-member/src/main/resources/db/schema.sql new file mode 100644 index 0000000..2eea8da --- /dev/null +++ b/gym-manage-api/gym-member/src/main/resources/db/schema.sql @@ -0,0 +1,281 @@ +-- ============================================ +-- 1. member_user 表(会员表) +-- ============================================ + +-- Step 1: 删除已存在的表(如果需要重建) +-- DROP TABLE IF EXISTS member_user CASCADE; + +-- Step 2: 创建 member_user 表 +CREATE TABLE IF NOT EXISTS member_user ( + -- ========== 主键和基础字段(来自BaseEntity)========== + id BIGSERIAL PRIMARY KEY, -- 主键ID,自增 + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, -- 创建时间 + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, -- 更新时间 + + -- ========== 会员核心字段 ========== + member_no VARCHAR(50) NOT NULL UNIQUE, -- 会员编号(唯一) + nickname VARCHAR(100), -- 昵称 + phone VARCHAR(255), -- 手机号(AES加密存储) + gender INTEGER DEFAULT 0, -- 性别:0-未知,1-男,2-女 + birthday TIMESTAMP, -- 生日 + address VARCHAR(500), -- 地址 + avatar VARCHAR(500), -- 头像URL + subscribed BOOLEAN DEFAULT FALSE, -- 是否关注服务号 + last_login_at TIMESTAMP, -- 最后登录时间 + + -- ========== 微信相关字段 ========== + union_id VARCHAR(100), -- 微信UnionID(跨应用唯一标识) + miniapp_open_id VARCHAR(100), -- 小程序OpenID + official_open_id VARCHAR(100), -- 服务号OpenID + + -- ========== 软删除字段 ========== + is_deleted BOOLEAN DEFAULT FALSE -- 是否删除(软删除标记) +); + +-- Step 3: 创建索引 +-- 会员编号索引(唯一索引,加速查询) +CREATE UNIQUE INDEX IF NOT EXISTS idx_member_user_member_no ON member_user(member_no); + +-- UnionID索引(加速跨平台用户查找) +CREATE INDEX IF NOT EXISTS idx_member_user_union_id ON member_user(union_id); + +-- 小程序OpenID索引(加速小程序登录查询) +CREATE INDEX IF NOT EXISTS idx_member_user_miniapp_openid ON member_user(miniapp_open_id); + +-- 服务号OpenID索引(加速服务号事件处理) +CREATE INDEX IF NOT EXISTS idx_member_user_official_openid ON member_user(official_open_id); + +-- 手机号索引(加速手机号查询和去重) +CREATE INDEX IF NOT EXISTS idx_member_user_phone ON member_user(phone); + +-- 软删除索引(加速查询未删除的记录) +CREATE INDEX IF NOT EXISTS idx_member_user_is_deleted ON member_user(is_deleted); + +-- Step 4: 添加注释 +COMMENT ON TABLE member_user IS '会员表'; + +COMMENT ON COLUMN member_user.id IS '主键ID'; +COMMENT ON COLUMN member_user.created_at IS '创建时间'; +COMMENT ON COLUMN member_user.updated_at IS '更新时间'; + +COMMENT ON COLUMN member_user.member_no IS '会员编号(唯一,格式:MEM + 8位随机字符)'; +COMMENT ON COLUMN member_user.nickname IS '昵称'; +COMMENT ON COLUMN member_user.phone IS '手机号(AES-128-CBC加密存储)'; +COMMENT ON COLUMN member_user.gender IS '性别:0-未知,1-男,2-女'; +COMMENT ON COLUMN member_user.birthday IS '生日'; +COMMENT ON COLUMN member_user.address IS '地址'; +COMMENT ON COLUMN member_user.avatar IS '头像URL'; +COMMENT ON COLUMN member_user.subscribed IS '是否关注服务号:true-已关注,false-未关注'; +COMMENT ON COLUMN member_user.last_login_at IS '最后登录时间'; + +COMMENT ON COLUMN member_user.union_id IS '微信UnionID(用户在开放平台的唯一标识,跨应用相同)'; +COMMENT ON COLUMN member_user.miniapp_open_id IS '小程序OpenID(用户在当前小程序的唯一标识)'; +COMMENT ON COLUMN member_user.official_open_id IS '服务号OpenID(用户在当前服务号的唯一标识)'; + +COMMENT ON COLUMN member_user.is_deleted IS '是否删除(软删除标记):false-正常,true-已删除'; + + +-- ============================================ +-- 2. wechat_user 表(微信用户表) +-- ============================================ + +-- Step 1: 删除已存在的表(如果需要重建) +-- DROP TABLE IF EXISTS wechat_user CASCADE; + +-- Step 2: 创建 wechat_user 表 +CREATE TABLE IF NOT EXISTS wechat_user ( + -- ========== 主键和基础字段(来自BaseEntity)========== + id BIGSERIAL PRIMARY KEY, -- 主键ID,自增 + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, -- 创建时间 + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, -- 更新时间 + + -- ========== 关联字段 ========== + member_id BIGINT NOT NULL, -- 会员ID(外键) + + -- ========== 微信标识字段 ========== + union_id VARCHAR(100), -- 微信UnionID + miniapp_openid VARCHAR(100), -- 小程序OpenID + mp_openid VARCHAR(100), -- 服务号OpenID + + -- ========== 关注状态字段 ========== + is_subscribed BOOLEAN DEFAULT FALSE, -- 是否关注服务号 + subscribe_time TIMESTAMP, -- 首次关注时间 + unsubscribe_time TIMESTAMP -- 最后一次取消关注时间 +); + +-- Step 3: 创建外键约束 +ALTER TABLE wechat_user + ADD CONSTRAINT fk_wechat_user_member + FOREIGN KEY (member_id) REFERENCES member_user(id) ON DELETE CASCADE; + +-- Step 4: 创建索引 +-- UnionID索引(加速跨平台用户查找) +CREATE INDEX IF NOT EXISTS idx_wechat_user_union_id ON wechat_user(union_id); + +-- 小程序OpenID索引(加速小程序登录查询) +CREATE INDEX IF NOT EXISTS idx_wechat_user_miniapp_openid ON wechat_user(miniapp_openid); + +-- 服务号OpenID索引(加速服务号事件处理) +CREATE INDEX IF NOT EXISTS idx_wechat_user_mp_openid ON wechat_user(mp_openid); + +-- 会员ID索引(加速关联查询) +CREATE INDEX IF NOT EXISTS idx_wechat_user_member_id ON wechat_user(member_id); + +-- Step 5: 添加注释 +COMMENT ON TABLE wechat_user IS '微信用户表'; + +COMMENT ON COLUMN wechat_user.id IS '主键ID'; +COMMENT ON COLUMN wechat_user.created_at IS '创建时间'; +COMMENT ON COLUMN wechat_user.updated_at IS '更新时间'; + +COMMENT ON COLUMN wechat_user.member_id IS '会员ID(关联 member_user 表的 id 字段)'; +COMMENT ON COLUMN wechat_user.union_id IS '微信UnionID(用户在开放平台的唯一标识)'; +COMMENT ON COLUMN wechat_user.miniapp_openid IS '小程序OpenID(用户在当前小程序的唯一标识)'; +COMMENT ON COLUMN wechat_user.mp_openid IS '服务号OpenID(用户在当前服务号的唯一标识)'; + +COMMENT ON COLUMN wechat_user.is_subscribed IS '是否关注服务号:true-已关注,false-未关注'; +COMMENT ON COLUMN wechat_user.subscribe_time IS '首次关注时间'; +COMMENT ON COLUMN wechat_user.unsubscribe_time IS '最后一次取消关注时间'; + + +-- ============================================ +-- 3. member_card 表(会员卡类型表) +-- ============================================ +CREATE TABLE IF NOT EXISTS member_card ( + id BIGSERIAL PRIMARY KEY, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + deleted_at TIMESTAMP, + + member_card_id BIGSERIAL, + 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 TEXT DEFAULT '{}' +); + +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格式)'; + + +-- ============================================ +-- 4. member_card_record 表(会员卡记录表) +-- ============================================ +CREATE TABLE IF NOT EXISTS member_card_record ( + id BIGSERIAL PRIMARY KEY, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + deleted_at TIMESTAMP, + + member_card_record_id BIGSERIAL, + 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 TIMESTAMP, + source_order_id BIGINT, + purchase_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + version INTEGER DEFAULT 0 NOT NULL, + card_composition TEXT +); + +-- 索引优化 +CREATE INDEX IF NOT EXISTS idx_member_card_record_member_id ON member_card_record(member_id); +CREATE INDEX IF NOT EXISTS idx_member_card_record_status ON member_card_record(status); +CREATE INDEX IF NOT EXISTS idx_member_card_record_expire_time ON member_card_record(expire_time); +CREATE INDEX IF NOT EXISTS idx_member_card_record_member_status ON member_card_record(member_id, status); +CREATE INDEX IF NOT EXISTS 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格式,用于组合卡)'; + + +-- ============================================ +-- 5. member_card_transactions 表(会员卡交易流水表) +-- ============================================ +CREATE TABLE IF NOT EXISTS member_card_transactions ( + id BIGSERIAL PRIMARY KEY, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + + 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, + archived_at TIMESTAMP +); + +-- 索引优化 +CREATE INDEX IF NOT EXISTS idx_member_card_transactions_member_id ON member_card_transactions(member_id); +CREATE INDEX IF NOT EXISTS idx_member_card_transactions_record_id ON member_card_transactions(member_card_record_id); +CREATE INDEX IF NOT EXISTS idx_member_card_transactions_created_at ON member_card_transactions(created_at); +CREATE INDEX IF NOT EXISTS 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 '归档时间'; + + +-- ============================================ +-- 6. refund_application 表(退款申请表) +-- ============================================ +CREATE TABLE IF NOT EXISTS refund_application ( + id BIGSERIAL PRIMARY KEY, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + deleted_at TIMESTAMP, + + record_id BIGINT NOT NULL, + member_id BIGINT NOT NULL, + status VARCHAR(20) NOT NULL DEFAULT 'PENDING', + reason VARCHAR(500), + apply_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + audit_time TIMESTAMP, + auditor_id BIGINT, + audit_remark VARCHAR(500), + refund_amount DECIMAL(10, 2) +); + +CREATE INDEX IF NOT EXISTS idx_refund_application_record_id ON refund_application(record_id); +CREATE INDEX IF NOT EXISTS 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-失败'; diff --git a/gym-manage-api/gym-member/src/main/resources/member-config.yml b/gym-manage-api/gym-member/src/main/resources/member-config.yml new file mode 100644 index 0000000..8c457f6 --- /dev/null +++ b/gym-manage-api/gym-member/src/main/resources/member-config.yml @@ -0,0 +1,22 @@ +# 微信配置(测试环境使用模拟数据) +wechat: + # Mock模式:true=使用模拟数据(开发测试),false=调用真实微信API(生产环境) + mock-enabled: false + miniapp: + app-id: wx4d480112b426100b + app-secret: 78548f0c0ff66c73d3e8b071897eb1e5 + mp: + app-id: wx6f138c9aacc8a0e8 + app-secret: 5df2e315e9268e96a43bb2cce1d2270b + token: test_token + aes-key: ${WECHAT_MP_AESKEY:test_aes_key} + # 服务器回调地址(微信服务器推送事件的URL) + callback-url: https://1me240209tk74.vicp.fun/api/member/auth/mp/callback + # 手机号加密配置 + phone-encryption: + secret-key: nVnA99iBfyK0IE6SkcUYdVAaVrezyn2sLRdLfkIyWnY= + iv: LMpG6Ih9mmfEAALOCeIJBw== + +spring: + elasticsearch: + uris: http://localhost:9200 # ES 服务器地址(支持多个,逗号分隔) diff --git a/gym-manage-api/gym-member-card/src/test/java/cn/novalon/gym/manage/gymmembercard/GymMemberCardApplicationTests.java b/gym-manage-api/gym-member/src/test/java/cn/novalon/gym/manage/member/MemberModuleTest.java similarity index 50% rename from gym-manage-api/gym-member-card/src/test/java/cn/novalon/gym/manage/gymmembercard/GymMemberCardApplicationTests.java rename to gym-manage-api/gym-member/src/test/java/cn/novalon/gym/manage/member/MemberModuleTest.java index 56e47d9..799f865 100644 --- a/gym-manage-api/gym-member-card/src/test/java/cn/novalon/gym/manage/gymmembercard/GymMemberCardApplicationTests.java +++ b/gym-manage-api/gym-member/src/test/java/cn/novalon/gym/manage/member/MemberModuleTest.java @@ -1,13 +1,16 @@ -package cn.novalon.gym.manage.gymmembercard; +package cn.novalon.gym.manage.member; import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; +/** + * 会员模块测试类 + */ @SpringBootTest -class GymMemberCardApplicationTests { +public class MemberModuleTest { @Test void contextLoads() { + // 测试Spring上下文是否能正常加载 } - } diff --git a/gym-manage-api/manage-app/pom.xml b/gym-manage-api/manage-app/pom.xml index bc92fbd..fb5f6f4 100644 --- a/gym-manage-api/manage-app/pom.xml +++ b/gym-manage-api/manage-app/pom.xml @@ -38,6 +38,12 @@ manage-db ${project.version} + + cn.novalon.gym.manage + gym-member + ${project.version} + + org.springframework.boot spring-boot-starter-webflux @@ -133,12 +139,6 @@ org.springdoc springdoc-openapi-starter-webflux-ui - - cn.novalon.gym.manage - gym-member-card - 0.0.1-SNAPSHOT - compile - 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 114ffd4..5df39d9 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 @@ -1,23 +1,26 @@ package cn.novalon.gym.manage.app.config; -import cn.novalon.gym.manage.gymmembercard.handler.MemberCardHandler; -import cn.novalon.gym.manage.gymmembercard.handler.MemberCardRecordHandler; -import cn.novalon.gym.manage.gymmembercard.handler.MemberCardTransactionHandler; -import cn.novalon.gym.manage.sys.handler.auth.SysAuthHandler; -import cn.novalon.gym.manage.sys.handler.auth.PasswordDiagnosticHandler; -import cn.novalon.gym.manage.sys.handler.config.SysConfigHandler; -import cn.novalon.gym.manage.sys.handler.dictionary.DictionaryHandler; -import cn.novalon.gym.manage.sys.handler.dict.SysDictHandler; -import cn.novalon.gym.manage.sys.handler.log.SysLogHandler; -import cn.novalon.gym.manage.sys.handler.log.OperationLogHandler; -import cn.novalon.gym.manage.sys.handler.menu.MenuHandler; -import cn.novalon.gym.manage.sys.handler.role.SysRoleHandler; -import cn.novalon.gym.manage.sys.handler.permission.SysPermissionHandler; -import cn.novalon.gym.manage.sys.handler.stats.StatsHandler; -import cn.novalon.gym.manage.sys.handler.user.SysUserHandler; + +import cn.novalon.gym.manage.file.handler.SysFileHandler; +import cn.novalon.gym.manage.member.card.handler.MemberCardHandler; +import cn.novalon.gym.manage.member.card.handler.MemberCardRecordHandler; +import cn.novalon.gym.manage.member.card.handler.MemberCardTransactionHandler; +import cn.novalon.gym.manage.member.handler.MemberHandler; +import cn.novalon.gym.manage.member.handler.WechatAuthHandler; import cn.novalon.gym.manage.notify.handler.SysNoticeHandler; import cn.novalon.gym.manage.notify.handler.SysUserMessageHandler; -import cn.novalon.gym.manage.file.handler.SysFileHandler; +import cn.novalon.gym.manage.sys.handler.auth.PasswordDiagnosticHandler; +import cn.novalon.gym.manage.sys.handler.auth.SysAuthHandler; +import cn.novalon.gym.manage.sys.handler.config.SysConfigHandler; +import cn.novalon.gym.manage.sys.handler.dict.SysDictHandler; +import cn.novalon.gym.manage.sys.handler.dictionary.DictionaryHandler; +import cn.novalon.gym.manage.sys.handler.log.OperationLogHandler; +import cn.novalon.gym.manage.sys.handler.log.SysLogHandler; +import cn.novalon.gym.manage.sys.handler.menu.MenuHandler; +import cn.novalon.gym.manage.sys.handler.permission.SysPermissionHandler; +import cn.novalon.gym.manage.sys.handler.role.SysRoleHandler; +import cn.novalon.gym.manage.sys.handler.stats.StatsHandler; +import cn.novalon.gym.manage.sys.handler.user.SysUserHandler; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.reactive.function.server.RouterFunction; @@ -54,6 +57,8 @@ public class SystemRouter { SysUserMessageHandler messageHandler, SysFileHandler fileHandler, SysPermissionHandler permissionHandler, + MemberHandler memberHandler, + WechatAuthHandler wechatAuthHandler, PasswordDiagnosticHandler passwordDiagnosticHandler, MemberCardHandler memberCardHandler, MemberCardRecordHandler memberCardRecordHandler, @@ -199,6 +204,25 @@ public class SystemRouter { .PUT("/api/permissions/{id}", permissionHandler::updatePermission) .DELETE("/api/permissions/{id}", permissionHandler::deletePermission) + // ========== 会员模块路由 - 微信认证 ========== + .POST("/api/member/auth/miniapp/login", wechatAuthHandler::miniappLogin) + .GET("/api/member/auth/mp/callback", wechatAuthHandler::verifyMpSignature) + .POST("/api/member/auth/mp/callback", wechatAuthHandler::mpCallback) + + // ========== 会员模块路由 - 会员信息 ========== + .GET("/api/member/info", memberHandler::getMemberInfo) + .PUT("/api/member/info", memberHandler::updateMemberInfo) + .POST("/api/member/phone/bind", memberHandler::bindPhone) + .GET("/api/member/subscribe/status", memberHandler::checkSubscribeStatus) + + // ========== 会员模块路由 - 管理端 ========== + .POST("/api/admin/member/{id}/phone", memberHandler::adminUpdatePhone) + .GET("/api/admin/member/{id}", memberHandler::adminGetMemberInfo) + .PUT("/api/admin/member/{id}", memberHandler::adminUpdateMemberInfo) + .GET("/api/admin/members", memberHandler::searchMembers) + .GET("/api/admin/members/all", memberHandler::getAllMembers) + + // ======================================== // ========== 会员卡管理路由 ============== // ======================================== diff --git a/gym-manage-api/manage-app/src/main/resources/application-dev.yml b/gym-manage-api/manage-app/src/main/resources/application-dev.yml index 8f5e8dc..443a17e 100644 --- a/gym-manage-api/manage-app/src/main/resources/application-dev.yml +++ b/gym-manage-api/manage-app/src/main/resources/application-dev.yml @@ -12,11 +12,15 @@ spring: max-life-time: 30m acquire-timeout: 3s flyway: - enabled: true + url: jdbc:postgresql://localhost:55432/manage_system + user: novalon + password: novalon123 + enabled: false locations: classpath:db/migration baseline-on-migrate: true validate-on-migrate: true + jwt: secret: novalon-gym-manage-jwt-secret-key-for-development-only-2026 expiration: 86400000 diff --git a/gym-manage-api/manage-app/src/main/resources/application-local.yml b/gym-manage-api/manage-app/src/main/resources/application-local.yml index 2e9c4f6..73b95b3 100644 --- a/gym-manage-api/manage-app/src/main/resources/application-local.yml +++ b/gym-manage-api/manage-app/src/main/resources/application-local.yml @@ -19,7 +19,7 @@ spring: password: 123456 driver-class-name: org.postgresql.Driver flyway: - enabled: true + enabled: false locations: classpath:db/migration baseline-on-migrate: true baseline-version: 0 diff --git a/gym-manage-api/manage-app/src/main/resources/application-test.yml b/gym-manage-api/manage-app/src/main/resources/application-test.yml index 5a55a80..0b1fba2 100644 --- a/gym-manage-api/manage-app/src/main/resources/application-test.yml +++ b/gym-manage-api/manage-app/src/main/resources/application-test.yml @@ -15,7 +15,7 @@ spring: max-life-time: 1h acquire-timeout: 5s flyway: - enabled: true + enabled: false locations: classpath:db/migration baseline-on-migrate: true validate-on-migrate: true diff --git a/gym-manage-api/manage-app/src/main/resources/application.yml b/gym-manage-api/manage-app/src/main/resources/application.yml index 9305b16..9ff6e52 100644 --- a/gym-manage-api/manage-app/src/main/resources/application.yml +++ b/gym-manage-api/manage-app/src/main/resources/application.yml @@ -29,7 +29,7 @@ spring: password: ${DB_PASSWORD:postgres} driver-class-name: org.postgresql.Driver flyway: - enabled: true + enabled: false locations: classpath:db/migration baseline-on-migrate: true baseline-version: 0 @@ -38,6 +38,10 @@ spring: user: name: disabled password: disabled + profiles: + active: dev + config: + import: classpath:member-config.yml management: endpoints: diff --git a/gym-manage-api/manage-gateway/src/main/java/cn/novalon/gym/manage/gateway/filter/JwtAuthenticationFilter.java b/gym-manage-api/manage-gateway/src/main/java/cn/novalon/gym/manage/gateway/filter/JwtAuthenticationFilter.java index cc54fbd..ea4b245 100644 --- a/gym-manage-api/manage-gateway/src/main/java/cn/novalon/gym/manage/gateway/filter/JwtAuthenticationFilter.java +++ b/gym-manage-api/manage-gateway/src/main/java/cn/novalon/gym/manage/gateway/filter/JwtAuthenticationFilter.java @@ -47,6 +47,7 @@ public class JwtAuthenticationFilter extends AbstractGatewayFilterFactory manage-sys manage-gateway - gym-member-card manage-app manage-common manage-db manage-audit manage-notify manage-file - + gym-member +