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 new file mode 100644 index 0000000..8877d8d --- /dev/null +++ b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/dao/MemberCardDao.java @@ -0,0 +1,130 @@ +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/MemberCardRecordDao.java b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/dao/MemberCardRecordDao.java new file mode 100644 index 0000000..e9c6bb1 --- /dev/null +++ b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/dao/MemberCardRecordDao.java @@ -0,0 +1,148 @@ +package cn.novalon.gym.manage.gymmembercard.dao; + +import cn.novalon.gym.manage.gymmembercard.entity.MemberCardRecordEntity; +import cn.novalon.gym.manage.gymmembercard.enums.MemberCardRecordStatus; +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 MemberCardRecordDao extends R2dbcRepository { + + /** + * 会员购卡/后台发卡 + * 支付成功后插入一条记录,状态为ACTIVE,设置有效期和初始剩余次数/余额 + * @param memberId 会员ID + * @param memberCardId 会员卡类型ID + * @param expireTime 到期时间 + * @param remainingTimes 剩余次数 + * @param remainingAmount 剩余余额 + * @param sourceOrderId 来源订单ID + * @return 插入的记录 + */ + @Modifying + @Query("INSERT INTO member_card_record (member_id, member_card_id, status, expire_time, remaining_times, remaining_amount, source_order_id, purchase_time, created_at, updated_at) " + + "VALUES (:memberId, :memberCardId, 'ACTIVE', :expireTime, :remainingTimes, :remainingAmount, :sourceOrderId, NOW(), NOW(), NOW()) " + + "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); + + /** + * 扣次/扣费(含防超扣校验) + * 预约团课或私教成功后,更新 remaining_times 减1 或 remaining_amount 减课程价格 + * @param recordId 会员卡记录ID + * @param deductTimes 扣除次数 + * @param deductAmount 扣除金额 + * @return 受影响的行数(0表示余额不足,扣费失败) + */ + @Modifying + @Query("UPDATE member_card_record SET " + + "remaining_times = remaining_times - :deductTimes, " + + "remaining_amount = remaining_amount - :deductAmount, " + + "updated_at = NOW() " + + "WHERE member_card_record_id = :recordId " + + "AND deleted_at IS NULL " + + "AND remaining_times >= :deductTimes " + + "AND remaining_amount >= :deductAmount") + Mono deductUsage(@Param("recordId") Long recordId, + @Param("deductTimes") Integer deductTimes, + @Param("deductAmount") Double deductAmount); + + /** + * 续费 + * 累加剩余次数/余额,顺延到期日期 + * @param recordId 会员卡记录ID + * @param addTimes 增加次数 + * @param addAmount 增加金额 + * @param newExpireTime 新的到期时间 + * @return 受影响的行数 + */ + @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); + + /** + * 状态变更 + * 过期定时任务将状态改为EXPIRED;退款后改为REFUNDED + * @param recordId 会员卡记录ID + * @param status 新状态 + * @return 受影响的行数 + */ + @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); + + /** + * 会员端"我的卡包" + * 根据会员ID查询所有卡,过滤状态为ACTIVE的,展示剩余次数/天数/余额 + * @param memberId 会员ID + * @return 有效会员卡列表 + */ + @Query("SELECT * FROM member_card_record WHERE member_id = :memberId AND status = 'ACTIVE' AND deleted_at IS NULL ORDER BY expire_time ASC") + Flux findActiveCardsByMemberId(@Param("memberId") Long memberId); + + /** + * 前台/店长查会员卡 + * 输入会员手机号或姓名,查出该会员持有的所有卡的信息 + * @param memberId 会员ID + * @param pageable 分页参数 + * @return 会员卡列表 + */ + @Query("SELECT mcr.* FROM member_card_record mcr " + + "INNER JOIN member m ON mcr.member_id = m.member_id " + + "WHERE mcr.member_id = :memberId AND mcr.deleted_at IS NULL " + + "ORDER BY mcr.purchase_time DESC LIMIT :#{#pageable.pageSize} OFFSET :#{#pageable.offset}") + Flux findByMemberId(@Param("memberId") Long memberId, Pageable pageable); + + /** + * 验证次卡是否可用(仅检验次数和过期时间) + * @param recordId 会员卡记录ID + * @param requiredTimes 需要的次数 + * @return 符合条件的记录,空表示不可用 + */ + @Query("SELECT * FROM member_card_record WHERE member_card_record_id = :recordId " + + "AND status = 'ACTIVE' AND deleted_at IS NULL " + + "AND expire_time > NOW() " + + "AND remaining_times >= :requiredTimes") + Mono validateCountCard(@Param("recordId") Long recordId, + @Param("requiredTimes") Integer requiredTimes); + + /** + * 验证储值卡是否可用(仅检验余额和过期时间) + * @param recordId 会员卡记录ID + * @param requiredAmount 需要的金额 + * @return 符合条件的记录,空表示不可用 + */ + @Query("SELECT * FROM member_card_record WHERE member_card_record_id = :recordId " + + "AND status = 'ACTIVE' AND deleted_at IS NULL " + + "AND expire_time > NOW() " + + "AND remaining_amount >= :requiredAmount") + Mono validateStoredCard(@Param("recordId") Long recordId, + @Param("requiredAmount") Double requiredAmount); + + /** + * 到期扫描(分批处理,避免内存压力) + * 定时任务:查询 status=ACTIVE 且 expire_time < 当前时间 的记录,用于批量过期处理 + * @return 已过期的会员卡记录列表(最多500条) + */ + @Query("SELECT * FROM member_card_record WHERE status = 'ACTIVE' AND expire_time < NOW() AND deleted_at IS NULL LIMIT 500") + Flux findExpiredCards(); +} \ 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 new file mode 100644 index 0000000..87836bb --- /dev/null +++ b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/dao/MemberCardTransactionsDao.java @@ -0,0 +1,159 @@ +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); +} \ 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 new file mode 100644 index 0000000..c5cf0e6 --- /dev/null +++ b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/domain/MemberCard.java @@ -0,0 +1,49 @@ +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; +} 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 new file mode 100644 index 0000000..7e04215 --- /dev/null +++ b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/domain/MemberCardRecord.java @@ -0,0 +1,50 @@ +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; + +/* + *@Author:shizhounian + *@Date:2026/5/10-05 22:47:31 + */ +@Schema(description = "会员卡记录",example = "member_card_record") +public class MemberCardRecord extends BaseDomain { + //会员持有卡id + @Schema(description = "会员持有卡Id",example = "1") + private Long memberCardRecordId; + + //会员Id + @Schema(description = "会员Id",example = "1") + private Long memberId; + + //关联会员卡Id + @Schema(description = "关联会员卡Id",example = "1") + private Long memberCardId; + + //状态:ACTIVE(有效) / USED_UP(用完) / EXPIRED(过期) / REFUNDED(已退款) + @Schema(description = "状态",example = "ACTIVE") + private MemberCardRecordStatus status; + + //剩余次数 + @Schema(description = "剩余次数",example = "1") + private Integer remainingTimes; + + //剩余余额 + @Schema(description = "剩余余额",example = "1") + private Double remainingAmount; + + //到期时间 + @Schema(description = "到期时间",example = "2026-05-10 05:22:47") + private LocalDateTime expireTime; + + //购买订单Id + @Schema(description = "购买订单Id",example = "1") + private Long sourceOrderId; + + //购买时间 + @Schema(description = "购买时间",example = "2026-05-10 05:22:47") + private LocalDateTime purchaseTime; +} 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 new file mode 100644 index 0000000..410ddac --- /dev/null +++ b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/domain/MemberCardTransactions.java @@ -0,0 +1,59 @@ +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; + + //创建时间 + @Schema(description = "创建时间",example = "2026-05-10 05:22:47") + private LocalDateTime createdAt; +} 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 new file mode 100644 index 0000000..06ff025 --- /dev/null +++ b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/entity/MemberCardEntity.java @@ -0,0 +1,52 @@ +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 + */ +@Data +@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; +} 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 new file mode 100644 index 0000000..2c1e1c6 --- /dev/null +++ b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/entity/MemberCardRecordEntity.java @@ -0,0 +1,53 @@ +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 + */ +@Data +@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; +} 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 new file mode 100644 index 0000000..b988065 --- /dev/null +++ b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/entity/MemberCardTransactionsEntity.java @@ -0,0 +1,62 @@ +package cn.novalon.gym.manage.gymmembercard.entity; + +import cn.novalon.gym.manage.db.entity.BaseEntity; +import cn.novalon.gym.manage.gymmembercard.enums.MemberCardTransactionsAction; +import cn.novalon.gym.manage.gymmembercard.enums.MemberCardTransactionsType; +import lombok.Data; +import org.springframework.data.relational.core.mapping.Column; +import org.springframework.data.relational.core.mapping.Table; + +import java.time.LocalDateTime; + +/* + *@Author:shizhounian + *@Date:2026/5/10-05 22:47:31 + */ +@Data +@Table("member_card_transactions") +public class MemberCardTransactionsEntity extends BaseEntity { + //会员卡流水Id + @Column("member_card_transactions_id") + private Long memberCardTransactionsId; + + //会员卡Id + @Column("member_card_id") + private Long memberCardId; + + //会员Id + @Column("member_id") + private Long memberId; + + //操作类型:PURCHASE(购买) / DEDUCT(扣次/扣费) / RENEW(续费) / REFUND(退款) / EXPIRE(过期) + @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("remark") + private String remark; + + //创建时间 + @Column("created_at") + private LocalDateTime createdAt; +} \ 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 new file mode 100644 index 0000000..b4fb006 --- /dev/null +++ b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/enums/MemberCardRecordStatus.java @@ -0,0 +1,36 @@ +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 new file mode 100644 index 0000000..6be4fc0 --- /dev/null +++ b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/enums/MemberCardTransactionsAction.java @@ -0,0 +1,40 @@ +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 new file mode 100644 index 0000000..ae55fe9 --- /dev/null +++ b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/enums/MemberCardTransactionsType.java @@ -0,0 +1,32 @@ +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/handler/MemberCardHandler.java b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/handler/MemberCardHandler.java new file mode 100644 index 0000000..eaff5f0 --- /dev/null +++ b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/handler/MemberCardHandler.java @@ -0,0 +1,114 @@ +package cn.novalon.gym.manage.gymmembercard.handler; + +import cn.hutool.db.PageResult; +import cn.novalon.gym.manage.gymmembercard.domain.MemberCard; +import cn.novalon.gym.manage.gymmembercard.sevice.IMemberCardService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Validator; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.stereotype.Component; +import org.springframework.web.reactive.function.server.ServerRequest; +import org.springframework.web.reactive.function.server.ServerResponse; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import java.util.List; + +/* + *@Author:shizhounian + *@Date:2026/5/17-05 20:10:22 + */ +@Component +@Tag(name = "会员卡类型管理", description = "会员卡类型相关操作") +public class MemberCardHandler { + private final IMemberCardService memberCardService; + private final Validator validator; + + public MemberCardHandler(IMemberCardService memberCardService, Validator validator) { + this.memberCardService = memberCardService; + this.validator = validator; + } + + /** + * 根据会员卡ID查询会员卡详情 + */ + @Operation(summary = "根据会员卡ID查询会员卡详情", description = "用于编辑前回显或小程序端展示") + public Mono getMemberCardById(ServerRequest request) { + Long memberCardId = Long.parseLong(request.pathVariable("memberCardId")); + return memberCardService.findByMemberCardIdAndDeletedAtIsNull(memberCardId) + .flatMap(card -> ServerResponse.ok().bodyValue(card)) + .switchIfEmpty(ServerResponse.notFound().build()); + } + + /** + * 条件查询会员卡列表(分页) + */ + @Operation(summary = "条件查询会员卡列表", description = "后台卡种列表展示,支持多条件组合+分页+排序") + public Mono getMemberCardList(ServerRequest request) { + Integer status = request.queryParam("status").map(Integer::parseInt).orElse(null); + String name = request.queryParam("name").orElse(null); + String type = request.queryParam("type").orElse(null); + Double minPrice = request.queryParam("minPrice").map(Double::parseDouble).orElse(null); + Double maxPrice = request.queryParam("maxPrice").map(Double::parseDouble).orElse(null); + + int page = request.queryParam("page").map(Integer::parseInt).orElse(0); + int size = request.queryParam("size").map(Integer::parseInt).orElse(10); + String sortField = request.queryParam("sortField").orElse("created_at"); + String sortOrder = request.queryParam("sortOrder").orElse("DESC"); + + Pageable pageable = PageRequest.of(page, size, Sort.by(Sort.Direction.fromString(sortOrder), sortField)); + + Mono countMono = memberCardService.countWithConditions(status, name, type, minPrice, maxPrice); + Flux cardsFlux = memberCardService.findWithConditions(status, name, type, minPrice, maxPrice, pageable); + + return Mono.zip(countMono, cardsFlux.collectList()) + .flatMap(tuple -> { + long total = tuple.getT1(); + List list = tuple.getT2(); + + // 使用 PageResult(int page, int pageSize, int total) 构造, + // 它会自动计算 totalPage + PageResult result = new PageResult<>(page, size, (int) total); + result.addAll(list); // 将查询结果填充进去 + + return ServerResponse.ok().bodyValue(result); + }); + } + + /** + * 保存卡种信息(新增或更新) + */ + @Operation(summary = "保存卡种信息", description = "新增或更新会员卡类型") + public Mono saveMemberCard(ServerRequest request) { + return request.bodyToMono(MemberCard.class) + .flatMap(memberCardService::save) + .flatMap(card -> ServerResponse.ok().bodyValue(card)); + } + + /** + * 逻辑删除会员卡(下架) + */ + @Operation(summary = "逻辑删除会员卡", description = "下架卡种,防止已购会员数据异常") + public Mono deleteMemberCard(ServerRequest request) { + Long memberCardId = Long.parseLong(request.pathVariable("memberCardId")); + return memberCardService.logicalDelete(memberCardId) + .flatMap(rows -> { + if (rows > 0) { + return ServerResponse.noContent().build(); + } else { + return ServerResponse.notFound().build(); + } + }); + } + + /** + * 批量查询上架的会员卡 + */ + @Operation(summary = "批量查询上架的会员卡", description = "用于小程序端展示") + public Mono getActiveCards(ServerRequest request) { + Integer status = request.queryParam("status").map(Integer::parseInt).orElse(0); + return ServerResponse.ok().body(memberCardService.findActiveCards(status), MemberCard.class); + } +} \ No newline at end of file diff --git a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/handler/MemberCardRecordHandler.java b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/handler/MemberCardRecordHandler.java new file mode 100644 index 0000000..3485d5b --- /dev/null +++ b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/handler/MemberCardRecordHandler.java @@ -0,0 +1,179 @@ +package cn.novalon.gym.manage.gymmembercard.handler; + +import cn.novalon.gym.manage.gymmembercard.domain.MemberCardRecord; +import cn.novalon.gym.manage.gymmembercard.enums.MemberCardRecordStatus; +import cn.novalon.gym.manage.gymmembercard.sevice.IMemberCardRecordService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Validator; +import lombok.Data; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.stereotype.Component; +import org.springframework.web.reactive.function.server.ServerRequest; +import org.springframework.web.reactive.function.server.ServerResponse; +import reactor.core.publisher.Mono; + +import java.time.LocalDateTime; + +/* + *@Author:shizhounian + *@Date:2026/5/17-05 20:10:22 + */ +@Component +@Tag(name = "会员卡记录管理", description = "会员卡记录相关操作") +public class MemberCardRecordHandler { + + private final IMemberCardRecordService memberCardRecordService; + private final Validator validator; + + public MemberCardRecordHandler(IMemberCardRecordService memberCardRecordService, Validator validator) { + this.memberCardRecordService = memberCardRecordService; + this.validator = validator; + } + + /** + * 会员购卡/后台发卡 + */ + @Operation(summary = "会员购卡/后台发卡", description = "支付成功后插入一条记录,状态为ACTIVE") + public Mono insertActiveRecord(ServerRequest request) { + return request.bodyToMono(MemberCardRecord.class) + .flatMap(memberCardRecordService::insertActiveRecord) + .flatMap(record -> ServerResponse.ok().bodyValue(record)); + } + + /** + * 扣次/扣费 + */ + @Operation(summary = "扣次/扣费", description = "预约团课或私教成功后扣减次数或余额") + public Mono deductUsage(ServerRequest request) { + Long recordId = Long.parseLong(request.pathVariable("id")); + return request.bodyToMono(DeductRequest.class) + .flatMap(body -> memberCardRecordService.deductUsage(recordId, + body.getDeductTimes(), + body.getDeductAmount())) + .flatMap(rows -> { + if (rows > 0) { + return ServerResponse.ok().build(); + } else { + return ServerResponse.badRequest().bodyValue("余额不足或卡无效"); + } + }); + } + + /** + * 续费 + */ + @Operation(summary = "续费", description = "累加剩余次数/余额,顺延到期日期") + public Mono renewCard(ServerRequest request) { + Long recordId = Long.parseLong(request.pathVariable("id")); + return request.bodyToMono(RenewRequest.class) + .flatMap(body -> memberCardRecordService.renewCard(recordId, + body.getAddTimes(), + body.getAddAmount(), + body.getNewExpireTime())) + .flatMap(rows -> { + if (rows > 0) { + return ServerResponse.ok().build(); + } else { + return ServerResponse.notFound().build(); + } + }); + } + + /** + * 状态变更(过期、退款) + */ + @Operation(summary = "状态变更", description = "将卡状态改为EXPIRED或REFUNDED") + public Mono updateStatus(ServerRequest request) { + Long recordId = Long.parseLong(request.pathVariable("id")); + return request.queryParam("status") + .map(s -> { + try { + return MemberCardRecordStatus.valueOf(s.toUpperCase()); + } catch (IllegalArgumentException e) { + return null; + } + }) + .map(status -> memberCardRecordService.updateStatus(recordId, status) + .flatMap(rows -> { + if (rows > 0) { + return ServerResponse.ok().build(); + } else { + return ServerResponse.notFound().build(); + } + })) + .orElse(ServerResponse.badRequest().bodyValue("status 参数缺失或无效")); + } + + /** + * 会员端“我的卡包” + */ + @Operation(summary = "会员我的卡包", description = "查询当前登录会员的所有有效卡") + public Mono getMyCards(ServerRequest request) { + Long memberId = Long.parseLong(request.pathVariable("memberId")); + return ServerResponse.ok().body(memberCardRecordService.findActiveCardsByMemberId(memberId), + MemberCardRecord.class); + } + + /** + * 前台/店长查会员卡(分页) + */ + @Operation(summary = "管理端查询会员卡", description = "按会员ID分页查询所有会员卡记录") + public Mono getMemberCardRecords(ServerRequest request) { + Long memberId = Long.parseLong(request.pathVariable("memberId")); + int page = request.queryParam("page").map(Integer::parseInt).orElse(0); + int size = request.queryParam("size").map(Integer::parseInt).orElse(10); + Pageable pageable = PageRequest.of(page, size, Sort.by("purchase_time").descending()); + return ServerResponse.ok().body(memberCardRecordService.findByMemberId(memberId, pageable), + MemberCardRecord.class); + } + + /** + * 验证次卡是否可用 + */ + @Operation(summary = "验证次卡", description = "校验次卡剩余次数是否足够") + public Mono validateCountCard(ServerRequest request) { + Long recordId = Long.parseLong(request.pathVariable("id")); + Integer requiredTimes = request.queryParam("times").map(Integer::parseInt).orElse(1); + return memberCardRecordService.validateCountCard(recordId, requiredTimes) + .flatMap(card -> ServerResponse.ok().bodyValue(card)) + .switchIfEmpty(ServerResponse.badRequest().bodyValue("次卡不可用")); + } + + /** + * 验证储值卡是否可用 + */ + @Operation(summary = "验证储值卡", description = "校验储值卡余额是否足够") + public Mono validateStoredCard(ServerRequest request) { + Long recordId = Long.parseLong(request.pathVariable("id")); + Double requiredAmount = request.queryParam("amount").map(Double::parseDouble).orElse(0.0); + return memberCardRecordService.validateStoredCard(recordId, requiredAmount) + .flatMap(card -> ServerResponse.ok().bodyValue(card)) + .switchIfEmpty(ServerResponse.badRequest().bodyValue("储值卡不可用")); + } + + /** + * 到期扫描(管理端触发) + */ + @Operation(summary = "到期扫描", description = "扫描并返回已过期的会员卡(最多500条)") + public Mono getExpiredCards(ServerRequest request) { + return ServerResponse.ok().body(memberCardRecordService.findExpiredCards(), MemberCardRecord.class); + } + + // ==================== 内部请求体 DTO ==================== + + @Data + public static class DeductRequest { + private Integer deductTimes; + private Double deductAmount; + } + + @Data + public static class RenewRequest { + private Integer addTimes; + private Double addAmount; + private LocalDateTime newExpireTime; + } +} \ No newline at end of file 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-card/src/main/java/cn/novalon/gym/manage/gymmembercard/handler/MemberCardTransactionHandler.java new file mode 100644 index 0000000..ead4283 --- /dev/null +++ b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/handler/MemberCardTransactionHandler.java @@ -0,0 +1,150 @@ +package cn.novalon.gym.manage.gymmembercard.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 io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Validator; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.stereotype.Component; +import org.springframework.web.reactive.function.server.ServerRequest; +import org.springframework.web.reactive.function.server.ServerResponse; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.time.LocalDateTime; +import java.util.List; + +/* + *@Author:shizhounian + *@Date:2026/5/17-05 20:10:22 + */ +@Component +@Tag(name = "会员卡流水管理", description = "会员卡流水相关操作") +public class MemberCardTransactionHandler { + + private final IMemberCardTransactionsService memberCardTransactionsService; + private final Validator validator; + + public MemberCardTransactionHandler(IMemberCardTransactionsService memberCardTransactionsService, + Validator validator) { + this.memberCardTransactionsService = memberCardTransactionsService; + this.validator = validator; + } + + /** + * 记录每一次变动 + */ + @Operation(summary = "插入流水记录", description = "购卡、扣次、续费、退款、过期时插入流水") + public Mono insertTransaction(ServerRequest request) { + return request.bodyToMono(MemberCardTransactions.class) + .flatMap(memberCardTransactionsService::insertTransaction) + .flatMap(record -> ServerResponse.ok().bodyValue(record)); + } + + /** + * 会员端"使用记录" + */ + @Operation(summary = "会员查询使用记录", description = "按会员ID和时间范围查询流水,支持分页") + public Mono getMemberTransactions(ServerRequest request) { + Long memberId = Long.parseLong(request.pathVariable("memberId")); + LocalDateTime startTime = request.queryParam("startTime") + .map(LocalDateTime::parse).orElse(LocalDateTime.now().minusMonths(1)); + LocalDateTime endTime = request.queryParam("endTime") + .map(LocalDateTime::parse).orElse(LocalDateTime.now()); + 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()); + + return ServerResponse.ok() + .body(memberCardTransactionsService.findByMemberIdAndTimeRange( + memberId, startTime, endTime, pageable), MemberCardTransactions.class); + } + + /** + * 后台"使用记录查询"(条件分页) + */ + @Operation(summary = "管理端流水查询", description = "按会员、卡号、操作类型、时间等条件分页查询流水") + 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); + 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( + memberId, memberCardId, operationType, startTime, endTime); + Flux flux = memberCardTransactionsService.findWithConditions( + memberId, memberCardId, operationType, startTime, endTime, pageable); + + return Mono.zip(countMono, flux.collectList()) + .flatMap(tuple -> { + Long total = tuple.getT1(); + List list = tuple.getT2(); + // 构造 PageResult,内部自动计算总页数 + PageResult result = new PageResult<>(page, size, total.intValue()); + result.addAll(list); + return ServerResponse.ok().bodyValue(result); + }); + } + + /** + * 按卡ID查询流水 + */ + @Operation(summary = "按卡ID查询流水", description = "查看某张卡的所有流水记录") + public Mono getTransactionsByCardId(ServerRequest request) { + Long memberCardId = Long.parseLong(request.pathVariable("cardId")); + return ServerResponse.ok() + .body(memberCardTransactionsService.findByMemberCardId(memberCardId), + MemberCardTransactions.class); + } + + /** + * 统计某卡种的总扣次数 + */ + @Operation(summary = "统计卡种总扣次数", description = "按卡种ID和时间范围统计扣次总数") + public Mono getDeductCountByCardId(ServerRequest request) { + Long memberCardId = Long.parseLong(request.pathVariable("cardId")); + LocalDateTime startTime = request.queryParam("startTime") + .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) + .flatMap(count -> ServerResponse.ok().bodyValue(count)); + } + + /** + * 统计某时间段的续费总金额 + */ + @Operation(summary = "统计续费总金额", description = "按时间段统计续费总金额") + public Mono getRenewAmountByTimeRange(ServerRequest request) { + LocalDateTime startTime = request.queryParam("startTime") + .map(LocalDateTime::parse).orElse(LocalDateTime.now().minusMonths(1)); + LocalDateTime endTime = request.queryParam("endTime") + .map(LocalDateTime::parse).orElse(LocalDateTime.now()); + return memberCardTransactionsService.sumRenewAmountByTimeRange(startTime, endTime) + .flatMap(amount -> ServerResponse.ok().bodyValue(amount)); + } + + /** + * 统计某会员的购卡总金额 + */ + @Operation(summary = "统计会员购卡总金额", description = "按会员ID和时间段统计购卡总金额") + public Mono getPurchaseAmountByMember(ServerRequest request) { + Long memberId = Long.parseLong(request.pathVariable("memberId")); + LocalDateTime startTime = request.queryParam("startTime") + .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) + .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/repository/IMemberCardRecordRepository.java b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/repository/IMemberCardRecordRepository.java new file mode 100644 index 0000000..237b440 --- /dev/null +++ b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/repository/IMemberCardRecordRepository.java @@ -0,0 +1,83 @@ +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; + +import java.time.LocalDateTime; + +public interface IMemberCardRecordRepository { + + /** + * 会员购卡/后台发卡 + * @param record 会员卡记录 + * @return 插入的记录 + */ + Mono insertActiveRecord(MemberCardRecord record); + + /** + * 扣次/扣费(含防超扣校验) + * @param recordId 会员卡记录ID + * @param deductTimes 扣除次数 + * @param deductAmount 扣除金额 + * @return 受影响的行数(0表示余额不足,扣费失败) + */ + Mono deductUsage(Long recordId, Integer deductTimes, Double deductAmount); + + /** + * 续费 + * @param recordId 会员卡记录ID + * @param addTimes 增加次数 + * @param addAmount 增加金额 + * @param newExpireTime 新的到期时间 + * @return 受影响的行数 + */ + Mono renewCard(Long recordId, Integer addTimes, Double addAmount, LocalDateTime newExpireTime); + + /** + * 状态变更 + * @param recordId 会员卡记录ID + * @param status 新状态 + * @return 受影响的行数 + */ + Mono updateStatus(Long recordId, MemberCardRecordStatus status); + + /** + * 会员端"我的卡包" + * @param memberId 会员ID + * @return 有效会员卡列表 + */ + Flux findActiveCardsByMemberId(Long memberId); + + /** + * 前台/店长查会员卡 + * @param memberId 会员ID + * @param pageable 分页参数 + * @return 会员卡列表 + */ + Flux findByMemberId(Long memberId, Pageable pageable); + + /** + * 验证次卡是否可用(仅检验次数和过期时间) + * @param recordId 会员卡记录ID + * @param requiredTimes 需要的次数 + * @return 符合条件的记录,空表示不可用 + */ + Mono validateCountCard(Long recordId, Integer requiredTimes); + + /** + * 验证储值卡是否可用(仅检验余额和过期时间) + * @param recordId 会员卡记录ID + * @param requiredAmount 需要的金额 + * @return 符合条件的记录,空表示不可用 + */ + Mono validateStoredCard(Long recordId, Double requiredAmount); + + /** + * 到期扫描(分批处理,避免内存压力) + * @return 已过期的会员卡记录列表(最多500条) + */ + Flux findExpiredCards(); +} 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 new file mode 100644 index 0000000..8b24397 --- /dev/null +++ b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/repository/IMemberCardRepository.java @@ -0,0 +1,78 @@ +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 new file mode 100644 index 0000000..010556a --- /dev/null +++ b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/repository/IMemberCardTransactionsRepository.java @@ -0,0 +1,91 @@ +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); +} 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 new file mode 100644 index 0000000..9148112 --- /dev/null +++ b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/repository/impl/MemberCardRecordRepositoryImpl.java @@ -0,0 +1,141 @@ +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; + +import java.time.LocalDateTime; + +@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; + } + + /** + * 会员购卡/后台发卡 + * @param record 会员卡记录 + * @return 插入的记录 + */ + @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)); + } + + /** + * 扣次/扣费(含防超扣校验) + * @param recordId 会员卡记录ID + * @param deductTimes 扣除次数 + * @param deductAmount 扣除金额 + * @return 受影响的行数(0表示余额不足,扣费失败) + */ + @Override + public Mono deductUsage(Long recordId, Integer deductTimes, Double deductAmount) { + return memberCardRecordDao.deductUsage(recordId, deductTimes, deductAmount); + } + + /** + * 续费 + * @param recordId 会员卡记录ID + * @param addTimes 增加次数 + * @param addAmount 增加金额 + * @param newExpireTime 新的到期时间 + * @return 受影响的行数 + */ + @Override + public Mono renewCard(Long recordId, Integer addTimes, Double addAmount, LocalDateTime newExpireTime) { + return memberCardRecordDao.renewCard(recordId, addTimes, addAmount, newExpireTime); + } + + /** + * 状态变更 + * @param recordId 会员卡记录ID + * @param status 新状态 + * @return 受影响的行数 + */ + @Override + public Mono updateStatus(Long recordId, MemberCardRecordStatus status) { + return memberCardRecordDao.updateStatus(recordId, status); + } + + /** + * 会员端"我的卡包" + * @param memberId 会员ID + * @return 有效会员卡列表 + */ + @Override + public Flux findActiveCardsByMemberId(Long memberId) { + return memberCardRecordDao.findActiveCardsByMemberId(memberId) + .map(entity -> beanConvertUtil.toBean(entity, MemberCardRecord.class)); + } + + /** + * 前台/店长查会员卡 + * @param memberId 会员ID + * @param pageable 分页参数 + * @return 会员卡列表 + */ + @Override + public Flux findByMemberId(Long memberId, Pageable pageable) { + return memberCardRecordDao.findByMemberId(memberId, pageable) + .map(entity -> beanConvertUtil.toBean(entity, MemberCardRecord.class)); + } + + /** + * 验证次卡是否可用(仅检验次数和过期时间) + * @param recordId 会员卡记录ID + * @param requiredTimes 需要的次数 + * @return 符合条件的记录,空表示不可用 + */ + @Override + public Mono validateCountCard(Long recordId, Integer requiredTimes) { + return memberCardRecordDao.validateCountCard(recordId, requiredTimes) + .map(entity -> beanConvertUtil.toBean(entity, MemberCardRecord.class)); + } + + /** + * 验证储值卡是否可用(仅检验余额和过期时间) + * @param recordId 会员卡记录ID + * @param requiredAmount 需要的金额 + * @return 符合条件的记录,空表示不可用 + */ + @Override + public Mono validateStoredCard(Long recordId, Double requiredAmount) { + return memberCardRecordDao.validateStoredCard(recordId, requiredAmount) + .map(entity -> beanConvertUtil.toBean(entity, MemberCardRecord.class)); + } + + /** + * 到期扫描(分批处理,避免内存压力) + * @return 已过期的会员卡记录列表(最多500条) + */ + @Override + public Flux findExpiredCards() { + return memberCardRecordDao.findExpiredCards() + .map(entity -> beanConvertUtil.toBean(entity, MemberCardRecord.class)); + } +} 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 new file mode 100644 index 0000000..afc3f97 --- /dev/null +++ b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/repository/impl/MemberCardRepositoryImpl.java @@ -0,0 +1,146 @@ +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 new file mode 100644 index 0000000..0704716 --- /dev/null +++ b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/repository/impl/MemberCardTransactionsRepositoryImpl.java @@ -0,0 +1,147 @@ +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); + } +} diff --git a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/sevice/IMemberCardRecordService.java b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/sevice/IMemberCardRecordService.java new file mode 100644 index 0000000..5773df8 --- /dev/null +++ b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/sevice/IMemberCardRecordService.java @@ -0,0 +1,83 @@ +package cn.novalon.gym.manage.gymmembercard.sevice; + +import cn.novalon.gym.manage.gymmembercard.domain.MemberCardRecord; +import cn.novalon.gym.manage.gymmembercard.enums.MemberCardRecordStatus; +import org.springframework.data.domain.Pageable; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.time.LocalDateTime; + +public interface IMemberCardRecordService { + + /** + * 会员购卡/后台发卡 + * @param record 会员卡记录 + * @return 插入的记录 + */ + Mono insertActiveRecord(MemberCardRecord record); + + /** + * 扣次/扣费(含防超扣校验) + * @param recordId 会员卡记录ID + * @param deductTimes 扣除次数 + * @param deductAmount 扣除金额 + * @return 受影响的行数(0表示余额不足,扣费失败) + */ + Mono deductUsage(Long recordId, Integer deductTimes, Double deductAmount); + + /** + * 续费 + * @param recordId 会员卡记录ID + * @param addTimes 增加次数 + * @param addAmount 增加金额 + * @param newExpireTime 新的到期时间 + * @return 受影响的行数 + */ + Mono renewCard(Long recordId, Integer addTimes, Double addAmount, LocalDateTime newExpireTime); + + /** + * 状态变更 + * @param recordId 会员卡记录ID + * @param status 新状态 + * @return 受影响的行数 + */ + Mono updateStatus(Long recordId, MemberCardRecordStatus status); + + /** + * 会员端"我的卡包" + * @param memberId 会员ID + * @return 有效会员卡列表 + */ + Flux findActiveCardsByMemberId(Long memberId); + + /** + * 前台/店长查会员卡 + * @param memberId 会员ID + * @param pageable 分页参数 + * @return 会员卡列表 + */ + Flux findByMemberId(Long memberId, Pageable pageable); + + /** + * 验证次卡是否可用(仅检验次数和过期时间) + * @param recordId 会员卡记录ID + * @param requiredTimes 需要的次数 + * @return 符合条件的记录,空表示不可用 + */ + Mono validateCountCard(Long recordId, Integer requiredTimes); + + /** + * 验证储值卡是否可用(仅检验余额和过期时间) + * @param recordId 会员卡记录ID + * @param requiredAmount 需要的金额 + * @return 符合条件的记录,空表示不可用 + */ + Mono validateStoredCard(Long recordId, Double requiredAmount); + + /** + * 到期扫描(分批处理,避免内存压力) + * @return 已过期的会员卡记录列表(最多500条) + */ + Flux findExpiredCards(); +} diff --git a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/sevice/IMemberCardService.java b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/sevice/IMemberCardService.java new file mode 100644 index 0000000..b1b744d --- /dev/null +++ b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/sevice/IMemberCardService.java @@ -0,0 +1,78 @@ +package cn.novalon.gym.manage.gymmembercard.sevice; + +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 IMemberCardService { + /** + * 根据会员卡ID查询会员卡详情(用于编辑前回显或小程序端展示) + * @param memberCardId 会员卡ID + * @return 会员卡完整信息,如果不存在或已删除则返回空 + */ + Mono findByMemberCardIdAndDeletedAtIsNull(Long memberCardId); + + /** + * 条件查询会员卡列表(后台卡种列表展示,支持多条件组合+分页+排序) + * @param status 会员卡状态(上架/下架) + * @param name 会员卡名称(模糊查询) + * @param type 会员卡类型 + * @param minPrice 最低价格 + * @param maxPrice 最高价格 + * @param pageable 分页和排序参数 + * @return 符合条件的会员卡列表 + */ + Flux findWithConditions(Integer status, String name, String type, + Double minPrice, Double maxPrice, Pageable pageable); + + /** + * 统计符合条件的会员卡总数(配合列表查询使用) + * @param status 会员卡状态 + * @param name 会员卡名称(模糊查询) + * @param type 会员卡类型 + * @param minPrice 最低价格 + * @param maxPrice 最高价格 + * @return 符合条件的会员卡数量 + */ + Mono countWithConditions(Integer status, String name, String type, + Double minPrice, Double maxPrice); + + /** + * 按状态查询会员卡列表(用于小程序端展示可购买卡列表,只展示上架的卡) + * @param status 会员卡状态(通常传上架状态) + * @param pageable 分页和排序参数 + * @return 符合条件的会员卡列表 + */ + Flux findByMemberCardStatusAndDeletedAtIsNull(Integer status, Pageable pageable); + + /** + * 检查会员卡是否已被购买(用于删除前的校验) + * @param memberCardId 会员卡ID + * @return 如果存在关联的会员记录则返回true,否则返回false + */ + Mono existsPurchasedRecord(Long memberCardId); + + /** + * 逻辑删除会员卡(下架卡种,防止已购会员数据异常) + * @param memberCardId 会员卡ID + * @return 受影响的行数 + */ + Mono logicalDelete(Long memberCardId); + + /** + * 保存卡种信息(新增或更新) + * - 新增:entity.memberCardId 为 null 时,插入新记录 + * - 更新:entity.memberCardId 不为 null 时,根据ID更新现有记录 + * @param entity 卡种信息 + * @return 保存后的实体对象 + */ + Mono save(MemberCard entity); + + /** + * 批量查询上架的会员卡(用于小程序端展示) + * @param status 上架状态值 + * @return 上架的会员卡列表 + */ + Flux findActiveCards(Integer status); +} 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 new file mode 100644 index 0000000..526bcb8 --- /dev/null +++ b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/sevice/IMemberCardTransactionsService.java @@ -0,0 +1,91 @@ +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); +} diff --git a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/sevice/impl/MemberCardRecordServiceImpl.java b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/sevice/impl/MemberCardRecordServiceImpl.java new file mode 100644 index 0000000..95b8ebe --- /dev/null +++ b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/sevice/impl/MemberCardRecordServiceImpl.java @@ -0,0 +1,67 @@ +package cn.novalon.gym.manage.gymmembercard.sevice.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 org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.time.LocalDateTime; + +@Service +public class MemberCardRecordServiceImpl implements IMemberCardRecordService { + private final IMemberCardRecordRepository memberCardRecordRepository; + + public MemberCardRecordServiceImpl(IMemberCardRecordRepository memberCardRecordRepository) { + this.memberCardRecordRepository = memberCardRecordRepository; + } + + @Override + public Mono insertActiveRecord(MemberCardRecord record) { + return memberCardRecordRepository.insertActiveRecord(record); + } + + @Override + public Mono deductUsage(Long recordId, Integer deductTimes, Double deductAmount) { + return memberCardRecordRepository.deductUsage(recordId, deductTimes, deductAmount); + } + + @Override + public Mono renewCard(Long recordId, Integer addTimes, Double addAmount, LocalDateTime newExpireTime) { + return memberCardRecordRepository.renewCard(recordId, addTimes, addAmount, newExpireTime); + } + + @Override + public Mono updateStatus(Long recordId, MemberCardRecordStatus 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); + } + + @Override + public Mono validateStoredCard(Long recordId, Double requiredAmount) { + return memberCardRecordRepository.validateStoredCard(recordId, requiredAmount); + } + + @Override + public Flux findExpiredCards() { + return memberCardRecordRepository.findExpiredCards(); + } +} diff --git a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/sevice/impl/MemberCardServiceImpl.java b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/sevice/impl/MemberCardServiceImpl.java new file mode 100644 index 0000000..0ec900c --- /dev/null +++ b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/sevice/impl/MemberCardServiceImpl.java @@ -0,0 +1,65 @@ +package cn.novalon.gym.manage.gymmembercard.sevice.impl; + +import cn.novalon.gym.manage.gymmembercard.domain.MemberCard; +import cn.novalon.gym.manage.gymmembercard.repository.IMemberCardRepository; +import cn.novalon.gym.manage.gymmembercard.repository.impl.MemberCardRepositoryImpl; +import cn.novalon.gym.manage.gymmembercard.sevice.IMemberCardService; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +@Service +public class MemberCardServiceImpl implements IMemberCardService { + private final IMemberCardRepository memberCardRepository; + + public MemberCardServiceImpl(IMemberCardRepository memberCardRepository) { + this.memberCardRepository = memberCardRepository; + } + + @Override + public Mono findByMemberCardIdAndDeletedAtIsNull(Long memberCardId) { + return memberCardRepository.findByMemberCardIdAndDeletedAtIsNull(memberCardId); + } + + @Override + public Flux findWithConditions(Integer status, String name, String type, + Double minPrice, Double maxPrice, Pageable pageable) { + return memberCardRepository.findWithConditions(status, name, type, minPrice, maxPrice, pageable); + } + + @Override + public Mono countWithConditions(Integer status, String name, String type, + Double minPrice, Double maxPrice) { + return memberCardRepository.countWithConditions(status, name, type, minPrice, maxPrice); + } + + @Override + public Flux findByMemberCardStatusAndDeletedAtIsNull(Integer status, Pageable pageable) { + return memberCardRepository.findByMemberCardStatusAndDeletedAtIsNull(status, pageable); + } + + @Override + public Mono existsPurchasedRecord(Long memberCardId) { + return memberCardRepository.existsPurchasedRecord(memberCardId); + } + + @Override + public Mono logicalDelete(Long memberCardId) { + return memberCardRepository.logicalDelete(memberCardId); + } + + public Mono updateSafe(Long memberCardId, MemberCard updateData) { + return ((MemberCardRepositoryImpl) memberCardRepository).updateSafe(memberCardId, updateData); + } + + @Override + public Mono save(MemberCard entity) { + return memberCardRepository.save(entity); + } + + @Override + public Flux findActiveCards(Integer status) { + return memberCardRepository.findActiveCards(status); + } +} 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 new file mode 100644 index 0000000..eddccea --- /dev/null +++ b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/sevice/impl/MemberCardTransactionsServiceImpl.java @@ -0,0 +1,69 @@ +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 org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.time.LocalDateTime; + +@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); + } +} 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-card/src/main/java/cn/novalon/gym/manage/gymmembercard/util/BeanConvertUtil.java new file mode 100644 index 0000000..f9f7e00 --- /dev/null +++ b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/util/BeanConvertUtil.java @@ -0,0 +1,49 @@ +package cn.novalon.gym.manage.gymmembercard.util; +/* + *@Author:shizhounian + *@Date:2026/5/11-05 21:19:04 + */ + +import cn.hutool.core.bean.BeanUtil; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.List; + +/** + * Entity、Domain、VO、DTO转换工具类 + */ +@Component +public class BeanConvertUtil { + + /** + * 单个对象泛型转换 + * @param source 源对象 + * @param targetClass 目标类Class + * @return 转换后的目标对象 + */ + public static T toBean(S source, Class targetClass) { + if (source == null) { + return null; + } + return BeanUtil.copyProperties(source, targetClass); + } + + /** + * 集合批量泛型转换 + * @param sourceList 源对象集合 + * @param targetClass 目标类Class + * @return 转换后的目标对象集合 + */ + public static List toBeanList(List sourceList, Class targetClass) { + if (sourceList == null || sourceList.isEmpty()) { + return List.of(); + } + + List targetList = new ArrayList<>(); + for (S source : sourceList) { + targetList.add(toBean(source, targetClass)); + } + return targetList; + } +} \ No newline at end of file diff --git a/gym-manage-api/manage-app/pom.xml b/gym-manage-api/manage-app/pom.xml index 45fcc60..bc92fbd 100644 --- a/gym-manage-api/manage-app/pom.xml +++ b/gym-manage-api/manage-app/pom.xml @@ -133,6 +133,12 @@ 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/ManageApplication.java b/gym-manage-api/manage-app/src/main/java/cn/novalon/gym/manage/app/ManageApplication.java index f74c2f7..0278f53 100644 --- a/gym-manage-api/manage-app/src/main/java/cn/novalon/gym/manage/app/ManageApplication.java +++ b/gym-manage-api/manage-app/src/main/java/cn/novalon/gym/manage/app/ManageApplication.java @@ -18,7 +18,8 @@ import java.util.List; @SpringBootApplication(scanBasePackages = "cn.novalon.gym.manage", exclude = { ReactiveUserDetailsServiceAutoConfiguration.class }) @EnableR2dbcRepositories(basePackages = { "cn.novalon.gym.manage.db.dao", - "cn.novalon.gym.manage.sys.audit.repository" }) + "cn.novalon.gym.manage.sys.audit.repository" , + "cn.novalon.gym.manage.gymmembercard.dao"}) public class ManageApplication { private static final Logger logger = LoggerFactory.getLogger(ManageApplication.class); 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 c869da3..bf571d8 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,5 +1,8 @@ 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; @@ -51,7 +54,10 @@ public class SystemRouter { SysUserMessageHandler messageHandler, SysFileHandler fileHandler, SysPermissionHandler permissionHandler, - PasswordDiagnosticHandler passwordDiagnosticHandler) { + PasswordDiagnosticHandler passwordDiagnosticHandler, + MemberCardHandler memberCardHandler, + MemberCardRecordHandler memberCardRecordHandler, + MemberCardTransactionHandler memberCardTransactionHandler) { return route() // ========== 诊断路由 ========== @@ -192,7 +198,53 @@ public class SystemRouter { .POST("/api/permissions", permissionHandler::createPermission) .PUT("/api/permissions/{id}", permissionHandler::updatePermission) .DELETE("/api/permissions/{id}", permissionHandler::deletePermission) - + + // ========== 会员卡管理路由 ========== + // 会员卡类型 + // 1. 获取所有会员卡类型 + .GET("/api/memberCard/active", memberCardHandler::getActiveCards) + // 2. 获取会员卡详情 + .GET("/api/memberCard/{memberCardId}", memberCardHandler::getMemberCardById) + // 3. 条件查询会员卡列表 + .GET("/api/memberCard", memberCardHandler::getMemberCardList) + // 4. 新增/更新会员卡 + .POST("/api/memberCard", memberCardHandler::saveMemberCard) + // 5. 逻辑删除会员卡 + .DELETE("/api/memberCard/{memberCardId}", memberCardHandler::deleteMemberCard) + // 会员卡持卡 + // 1. 会员购卡/发卡 + .POST("/api/memberCardRecord", memberCardRecordHandler::insertActiveRecord) + // 2. 会员端“我的卡包” - 按会员ID获取有效卡 + .GET("/api/memberCardRecord/member/{memberId}/active", memberCardRecordHandler::getMyCards) + // 3. 管理端按会员ID分页查所有卡记录 + .GET("/api/memberCardRecord/member/{memberId}", memberCardRecordHandler::getMemberCardRecords) + // 4. 到期扫描 + .GET("/api/memberCardRecord/expired", memberCardRecordHandler::getExpiredCards) + // 5. 扣次/扣费 + .POST("/api/memberCardRecord/{id}/deduct", memberCardRecordHandler::deductUsage) + // 6. 续费 + .POST("/api/memberCardRecord/{id}/renew", memberCardRecordHandler::renewCard) + // 7. 状态变更(过期/退款) + .PUT("/api/memberCardRecord/{id}/status", memberCardRecordHandler::updateStatus) + // 8. 验证次卡 + .GET("/api/memberCardRecord/{id}/validate/count", memberCardRecordHandler::validateCountCard) + // 9. 验证储值卡 + .GET("/api/memberCardRecord/{id}/validate/stored", memberCardRecordHandler::validateStoredCard) + // 会员卡交易 + // 1. 插入流水记录 + .POST("/api/transactions", memberCardTransactionHandler::insertTransaction) + // 2. 后台条件分页查询流水(带多个查询参数) + .GET("/api/transactions", memberCardTransactionHandler::getTransactionsWithConditions) + // 3. 按会员ID查询使用记录(分页 + 时间范围) + .GET("/api/transactions/member/{memberId}", memberCardTransactionHandler::getMemberTransactions) + // 4. 按卡ID查询所有流水记录 + .GET("/api/transactions/card/{cardId}", memberCardTransactionHandler::getTransactionsByCardId) + // 5. 统计某卡种总扣次数 + .GET("/api/transactions/statistics/deduct/card/{cardId}", memberCardTransactionHandler::getDeductCountByCardId) + // 6. 统计某时间段续费总金额 + .GET("/api/transactions/statistics/renew", memberCardTransactionHandler::getRenewAmountByTimeRange) + // 7. 统计某会员购卡总金额 + .GET("/api/transactions/statistics/purchase/member/{memberId}", memberCardTransactionHandler::getPurchaseAmountByMember) .build(); } } diff --git a/gym-manage-api/pom.xml b/gym-manage-api/pom.xml index df2e4c2..3f820b0 100644 --- a/gym-manage-api/pom.xml +++ b/gym-manage-api/pom.xml @@ -36,6 +36,7 @@ manage-sys manage-gateway + gym-member-card manage-app manage-common manage-db diff --git a/gym-manage-web/pnpm-lock.yaml b/gym-manage-web/pnpm-lock.yaml index 63bd44e..cc01eea 100644 --- a/gym-manage-web/pnpm-lock.yaml +++ b/gym-manage-web/pnpm-lock.yaml @@ -59,7 +59,7 @@ importers: version: 6.21.0(eslint@8.57.1)(typescript@5.9.3) '@vitejs/plugin-vue': specifier: ^6.0.3 - version: 6.0.5(vite@7.3.1(@types/node@20.19.37))(vue@3.5.30(typescript@5.9.3)) + version: 6.0.5(vite@7.3.1(@types/node@20.19.37)(terser@5.46.2))(vue@3.5.30(typescript@5.9.3)) '@vitest/coverage-v8': specifier: ^4.1.1 version: 4.1.2(vitest@4.1.0) @@ -81,15 +81,18 @@ importers: prettier: specifier: ^3.1.1 version: 3.8.1 + terser: + specifier: ^5.46.1 + version: 5.46.2 typescript: specifier: ^5.9.3 version: 5.9.3 vite: specifier: ^7.3.1 - version: 7.3.1(@types/node@20.19.37) + version: 7.3.1(@types/node@20.19.37)(terser@5.46.2) vitest: specifier: ^4.0.16 - version: 4.1.0(@types/node@20.19.37)(@vitest/ui@4.1.0)(jsdom@27.4.0)(vite@7.3.1(@types/node@20.19.37)) + version: 4.1.0(@types/node@20.19.37)(@vitest/ui@4.1.0)(jsdom@27.4.0)(vite@7.3.1(@types/node@20.19.37)(terser@5.46.2)) vue-tsc: specifier: ^3.2.2 version: 3.2.5(typescript@5.9.3) @@ -390,10 +393,16 @@ packages: resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} engines: {node: '>=12'} + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + '@jridgewell/resolve-uri@3.1.2': resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} engines: {node: '>=6.0.0'} + '@jridgewell/source-map@0.3.11': + resolution: {integrity: sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==} + '@jridgewell/sourcemap-codec@1.5.5': resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} @@ -464,66 +473,79 @@ packages: resolution: {integrity: sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==} cpu: [arm] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm-musleabihf@4.59.0': resolution: {integrity: sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==} cpu: [arm] os: [linux] + libc: [musl] '@rollup/rollup-linux-arm64-gnu@4.59.0': resolution: {integrity: sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==} cpu: [arm64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm64-musl@4.59.0': resolution: {integrity: sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==} cpu: [arm64] os: [linux] + libc: [musl] '@rollup/rollup-linux-loong64-gnu@4.59.0': resolution: {integrity: sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==} cpu: [loong64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-loong64-musl@4.59.0': resolution: {integrity: sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==} cpu: [loong64] os: [linux] + libc: [musl] '@rollup/rollup-linux-ppc64-gnu@4.59.0': resolution: {integrity: sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==} cpu: [ppc64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-ppc64-musl@4.59.0': resolution: {integrity: sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==} cpu: [ppc64] os: [linux] + libc: [musl] '@rollup/rollup-linux-riscv64-gnu@4.59.0': resolution: {integrity: sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==} cpu: [riscv64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-riscv64-musl@4.59.0': resolution: {integrity: sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==} cpu: [riscv64] os: [linux] + libc: [musl] '@rollup/rollup-linux-s390x-gnu@4.59.0': resolution: {integrity: sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==} cpu: [s390x] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.59.0': resolution: {integrity: sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==} cpu: [x64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-musl@4.59.0': resolution: {integrity: sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==} cpu: [x64] os: [linux] + libc: [musl] '@rollup/rollup-openbsd-x64@4.59.0': resolution: {integrity: sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==} @@ -858,6 +880,9 @@ packages: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} + buffer-from@1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + call-bind-apply-helpers@1.0.2: resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} engines: {node: '>= 0.4'} @@ -889,6 +914,9 @@ packages: resolution: {integrity: sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==} engines: {node: '>=14'} + commander@2.20.3: + resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} + concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} @@ -1631,6 +1659,13 @@ packages: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} + source-map-support@0.5.21: + resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} + + source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + speakingurl@14.0.1: resolution: {integrity: sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==} engines: {node: '>=0.10.0'} @@ -1672,6 +1707,11 @@ packages: symbol-tree@3.2.4: resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} + terser@5.46.2: + resolution: {integrity: sha512-uxfo9fPcSgLDYob/w1FuL0c99MWiJDnv+5qXSQc5+Ki5NjVNsYi66INnMFBjf6uFz6OnX12piJQPF4IpjJTNTw==} + engines: {node: '>=10'} + hasBin: true + text-table@0.2.0: resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} @@ -2138,8 +2178,18 @@ snapshots: wrap-ansi: 8.1.0 wrap-ansi-cjs: wrap-ansi@7.0.0 + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + '@jridgewell/resolve-uri@3.1.2': {} + '@jridgewell/source-map@0.3.11': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + '@jridgewell/sourcemap-codec@1.5.5': {} '@jridgewell/trace-mapping@0.3.31': @@ -2366,10 +2416,10 @@ snapshots: '@ungap/structured-clone@1.3.0': {} - '@vitejs/plugin-vue@6.0.5(vite@7.3.1(@types/node@20.19.37))(vue@3.5.30(typescript@5.9.3))': + '@vitejs/plugin-vue@6.0.5(vite@7.3.1(@types/node@20.19.37)(terser@5.46.2))(vue@3.5.30(typescript@5.9.3))': dependencies: '@rolldown/pluginutils': 1.0.0-rc.2 - vite: 7.3.1(@types/node@20.19.37) + vite: 7.3.1(@types/node@20.19.37)(terser@5.46.2) vue: 3.5.30(typescript@5.9.3) '@vitest/coverage-v8@4.1.2(vitest@4.1.0)': @@ -2384,7 +2434,7 @@ snapshots: obug: 2.1.1 std-env: 4.0.0 tinyrainbow: 3.1.0 - vitest: 4.1.0(@types/node@20.19.37)(@vitest/ui@4.1.0)(jsdom@27.4.0)(vite@7.3.1(@types/node@20.19.37)) + vitest: 4.1.0(@types/node@20.19.37)(@vitest/ui@4.1.0)(jsdom@27.4.0)(vite@7.3.1(@types/node@20.19.37)(terser@5.46.2)) '@vitest/expect@4.1.0': dependencies: @@ -2395,13 +2445,13 @@ snapshots: chai: 6.2.2 tinyrainbow: 3.1.0 - '@vitest/mocker@4.1.0(vite@7.3.1(@types/node@20.19.37))': + '@vitest/mocker@4.1.0(vite@7.3.1(@types/node@20.19.37)(terser@5.46.2))': dependencies: '@vitest/spy': 4.1.0 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 7.3.1(@types/node@20.19.37) + vite: 7.3.1(@types/node@20.19.37)(terser@5.46.2) '@vitest/pretty-format@4.1.0': dependencies: @@ -2434,7 +2484,7 @@ snapshots: sirv: 3.0.2 tinyglobby: 0.2.15 tinyrainbow: 3.1.0 - vitest: 4.1.0(@types/node@20.19.37)(@vitest/ui@4.1.0)(jsdom@27.4.0)(vite@7.3.1(@types/node@20.19.37)) + vitest: 4.1.0(@types/node@20.19.37)(@vitest/ui@4.1.0)(jsdom@27.4.0)(vite@7.3.1(@types/node@20.19.37)(terser@5.46.2)) '@vitest/utils@4.1.0': dependencies: @@ -2642,6 +2692,8 @@ snapshots: dependencies: fill-range: 7.1.1 + buffer-from@1.1.2: {} + call-bind-apply-helpers@1.0.2: dependencies: es-errors: 1.3.0 @@ -2668,6 +2720,8 @@ snapshots: commander@10.0.1: {} + commander@2.20.3: {} + concat-map@0.0.1: {} config-chain@1.1.13: @@ -3456,6 +3510,13 @@ snapshots: source-map-js@1.2.1: {} + source-map-support@0.5.21: + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + + source-map@0.6.1: {} + speakingurl@14.0.1: {} stackback@0.0.2: {} @@ -3494,6 +3555,13 @@ snapshots: symbol-tree@3.2.4: {} + terser@5.46.2: + dependencies: + '@jridgewell/source-map': 0.3.11 + acorn: 8.16.0 + commander: 2.20.3 + source-map-support: 0.5.21 + text-table@0.2.0: {} tinybench@2.9.0: {} @@ -3547,7 +3615,7 @@ snapshots: util-deprecate@1.0.2: {} - vite@7.3.1(@types/node@20.19.37): + vite@7.3.1(@types/node@20.19.37)(terser@5.46.2): dependencies: esbuild: 0.27.4 fdir: 6.5.0(picomatch@4.0.3) @@ -3558,11 +3626,12 @@ snapshots: optionalDependencies: '@types/node': 20.19.37 fsevents: 2.3.3 + terser: 5.46.2 - vitest@4.1.0(@types/node@20.19.37)(@vitest/ui@4.1.0)(jsdom@27.4.0)(vite@7.3.1(@types/node@20.19.37)): + vitest@4.1.0(@types/node@20.19.37)(@vitest/ui@4.1.0)(jsdom@27.4.0)(vite@7.3.1(@types/node@20.19.37)(terser@5.46.2)): dependencies: '@vitest/expect': 4.1.0 - '@vitest/mocker': 4.1.0(vite@7.3.1(@types/node@20.19.37)) + '@vitest/mocker': 4.1.0(vite@7.3.1(@types/node@20.19.37)(terser@5.46.2)) '@vitest/pretty-format': 4.1.0 '@vitest/runner': 4.1.0 '@vitest/snapshot': 4.1.0 @@ -3579,7 +3648,7 @@ snapshots: tinyexec: 1.0.2 tinyglobby: 0.2.15 tinyrainbow: 3.1.0 - vite: 7.3.1(@types/node@20.19.37) + vite: 7.3.1(@types/node@20.19.37)(terser@5.46.2) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 20.19.37