diff --git a/gym-manage-api/gym-coupon/pom.xml b/gym-manage-api/gym-coupon/pom.xml
new file mode 100644
index 0000000..ef9eab0
--- /dev/null
+++ b/gym-manage-api/gym-coupon/pom.xml
@@ -0,0 +1,90 @@
+
+
+ 4.0.0
+
+ cn.novalon.gym.manage
+ gym-manage-api
+ 1.0.0
+ ../pom.xml
+
+ cn.novalon.gym.manage
+ gym-coupon
+ 1.0.0
+ gym-coupon
+ Coupon Management Module
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 21
+
+
+
+ cn.novalon.gym.manage
+ manage-common
+ ${project.version}
+
+
+ cn.novalon.gym.manage
+ manage-sys
+ ${project.version}
+
+
+ cn.novalon.gym.manage
+ manage-db
+ ${project.version}
+
+
+ org.springframework.boot
+ spring-boot-starter-webflux
+
+
+ org.springframework.boot
+ spring-boot-starter-data-r2dbc
+
+
+ org.springframework.boot
+ spring-boot-starter-validation
+
+
+ org.springdoc
+ springdoc-openapi-starter-webflux-ui
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+ io.swagger.core.v3
+ swagger-annotations-jakarta
+ 2.2.43
+ compile
+
+
+ cn.novalon.gym.manage
+ gym-member
+ ${project.version}
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+
diff --git a/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/converter/CouponConverter.java b/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/converter/CouponConverter.java
new file mode 100644
index 0000000..76a86ee
--- /dev/null
+++ b/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/converter/CouponConverter.java
@@ -0,0 +1,75 @@
+package cn.novalon.gym.manage.coupon.converter;
+
+import cn.hutool.core.bean.BeanUtil;
+import cn.novalon.gym.manage.coupon.domain.CouponTemplate;
+import cn.novalon.gym.manage.coupon.domain.MemberCoupon;
+import cn.novalon.gym.manage.coupon.entity.CouponTemplateEntity;
+import cn.novalon.gym.manage.coupon.entity.MemberCouponEntity;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+
+/**
+ * 优惠券相关转换器
+ */
+@Component
+@Slf4j
+public class CouponConverter {
+
+ /**
+ * 将优惠券模板实体转换为领域模型
+ */
+ public CouponTemplate toCouponTemplate(CouponTemplateEntity entity) {
+ if (entity == null) {
+ return null;
+ }
+ CouponTemplate couponTemplate = new CouponTemplate();
+ BeanUtil.copyProperties(entity, couponTemplate);
+ log.debug("转换优惠券模板实体到领域模型:couponId={}", entity.getId());
+ return couponTemplate;
+ }
+
+ /**
+ * 将优惠券模板领域模型转换为实体
+ */
+ public CouponTemplateEntity toCouponTemplateEntity(CouponTemplate domain) {
+ if (domain == null) {
+ return null;
+ }
+ CouponTemplateEntity entity = new CouponTemplateEntity();
+ BeanUtil.copyProperties(domain, entity);
+ if (domain.getId() != null) {
+ entity.markNotNew();
+ }
+ log.debug("转换优惠券模板领域模型到实体:couponId={}", domain.getId());
+ return entity;
+ }
+
+ /**
+ * 将会员优惠券实体转换为领域模型
+ */
+ public MemberCoupon toMemberCoupon(MemberCouponEntity entity) {
+ if (entity == null) {
+ return null;
+ }
+ MemberCoupon memberCoupon = new MemberCoupon();
+ BeanUtil.copyProperties(entity, memberCoupon);
+ log.debug("转换会员优惠券实体到领域模型:memberCouponId={}", entity.getId());
+ return memberCoupon;
+ }
+
+ /**
+ * 将会员优惠券领域模型转换为实体
+ */
+ public MemberCouponEntity toMemberCouponEntity(MemberCoupon domain) {
+ if (domain == null) {
+ return null;
+ }
+ MemberCouponEntity entity = new MemberCouponEntity();
+ BeanUtil.copyProperties(domain, entity);
+ if (domain.getId() != null) {
+ entity.markNotNew();
+ }
+ log.debug("转换会员优惠券领域模型到实体:memberCouponId={}", domain.getId());
+ return entity;
+ }
+}
diff --git a/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/dao/CouponTemplateDao.java b/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/dao/CouponTemplateDao.java
new file mode 100644
index 0000000..65e32f7
--- /dev/null
+++ b/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/dao/CouponTemplateDao.java
@@ -0,0 +1,43 @@
+package cn.novalon.gym.manage.coupon.dao;
+
+import cn.novalon.gym.manage.coupon.entity.CouponTemplateEntity;
+import org.springframework.data.r2dbc.repository.Modifying;
+import org.springframework.data.r2dbc.repository.Query;
+import org.springframework.data.r2dbc.repository.R2dbcRepository;
+import org.springframework.stereotype.Repository;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+import java.time.LocalDateTime;
+
+@Repository
+public interface CouponTemplateDao extends R2dbcRepository {
+
+ Mono findByIdIsAndDeletedAtIsNull(Long id);
+
+ Flux findAllByDeletedAtIsNull();
+
+ Flux findByNameContainingAndDeletedAtIsNull(String name);
+
+ Flux findByCouponTypeAndDeletedAtIsNull(String couponType);
+
+ Flux findByStatusAndDeletedAtIsNull(String status);
+
+ Mono findByClaimCodeAndDeletedAtIsNull(String claimCode);
+
+ @Modifying
+ @Query("UPDATE coupon_template SET deleted_at = :deletedAt WHERE id = :id")
+ Mono softDelete(Long id, LocalDateTime deletedAt);
+
+ @Modifying
+ @Query("UPDATE coupon_template SET status = :status, updated_at = :updatedAt WHERE id = :id")
+ Mono updateStatus(Long id, String status, LocalDateTime updatedAt);
+
+ @Modifying
+ @Query("UPDATE coupon_template SET issued_count = issued_count + :count, updated_at = :updatedAt WHERE id = :id")
+ Mono incrementIssuedCount(Long id, int count, LocalDateTime updatedAt);
+
+ @Modifying
+ @Query("UPDATE coupon_template SET used_count = used_count + :count, updated_at = :updatedAt WHERE id = :id")
+ Mono incrementUsedCount(Long id, int count, LocalDateTime updatedAt);
+}
diff --git a/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/dao/MemberCouponDao.java b/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/dao/MemberCouponDao.java
new file mode 100644
index 0000000..e3cf996
--- /dev/null
+++ b/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/dao/MemberCouponDao.java
@@ -0,0 +1,28 @@
+package cn.novalon.gym.manage.coupon.dao;
+
+import cn.novalon.gym.manage.coupon.entity.MemberCouponEntity;
+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 MemberCouponDao extends R2dbcRepository {
+
+ Flux findByTemplateIdAndDeletedAtIsNull(Long templateId);
+
+ Flux findByMemberIdAndDeletedAtIsNull(Long memberId);
+
+ Mono countByTemplateIdAndMemberIdAndDeletedAtIsNull(Long templateId, Long memberId);
+
+ @Query("SELECT COUNT(*) FROM member_coupon WHERE template_id = :templateId AND status = :status AND deleted_at IS NULL")
+ Mono countByTemplateIdAndStatus(Long templateId, String status);
+
+ Mono findByCouponCodeAndDeletedAtIsNull(String couponCode);
+
+ @Modifying
+ @Query("UPDATE member_coupon SET status = 'EXPIRED', updated_at = :now WHERE status = 'AVAILABLE' AND expire_at < :now AND deleted_at IS NULL")
+ Mono expireAvailableCoupons(java.time.LocalDateTime now);
+}
diff --git a/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/domain/ClaimCouponRequest.java b/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/domain/ClaimCouponRequest.java
new file mode 100644
index 0000000..fda6050
--- /dev/null
+++ b/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/domain/ClaimCouponRequest.java
@@ -0,0 +1,29 @@
+package cn.novalon.gym.manage.coupon.domain;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+
+@Schema(description = "领取码兑换优惠券请求")
+public class ClaimCouponRequest {
+
+ @Schema(description = "会员ID", requiredMode = Schema.RequiredMode.REQUIRED)
+ private Long memberId;
+
+ @Schema(description = "领取码", requiredMode = Schema.RequiredMode.REQUIRED)
+ private String claimCode;
+
+ public Long getMemberId() {
+ return memberId;
+ }
+
+ public void setMemberId(Long memberId) {
+ this.memberId = memberId;
+ }
+
+ public String getClaimCode() {
+ return claimCode;
+ }
+
+ public void setClaimCode(String claimCode) {
+ this.claimCode = claimCode;
+ }
+}
diff --git a/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/domain/CouponStatistics.java b/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/domain/CouponStatistics.java
new file mode 100644
index 0000000..59d2d6c
--- /dev/null
+++ b/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/domain/CouponStatistics.java
@@ -0,0 +1,86 @@
+package cn.novalon.gym.manage.coupon.domain;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+
+import java.math.BigDecimal;
+
+@Schema(description = "优惠券统计数据")
+public class CouponStatistics {
+
+ @Schema(description = "优惠券模板ID")
+ private Long templateId;
+
+ @Schema(description = "已发放数量")
+ private long issuedCount;
+
+ @Schema(description = "已使用数量")
+ private long usedCount;
+
+ @Schema(description = "可用数量")
+ private long availableCount;
+
+ @Schema(description = "已过期数量")
+ private long expiredCount;
+
+ @Schema(description = "核销率(百分比)")
+ private BigDecimal redemptionRate;
+
+ @Schema(description = "累计优惠金额")
+ private BigDecimal totalDiscountAmount;
+
+ public Long getTemplateId() {
+ return templateId;
+ }
+
+ public void setTemplateId(Long templateId) {
+ this.templateId = templateId;
+ }
+
+ public long getIssuedCount() {
+ return issuedCount;
+ }
+
+ public void setIssuedCount(long issuedCount) {
+ this.issuedCount = issuedCount;
+ }
+
+ public long getUsedCount() {
+ return usedCount;
+ }
+
+ public void setUsedCount(long usedCount) {
+ this.usedCount = usedCount;
+ }
+
+ public long getAvailableCount() {
+ return availableCount;
+ }
+
+ public void setAvailableCount(long availableCount) {
+ this.availableCount = availableCount;
+ }
+
+ public long getExpiredCount() {
+ return expiredCount;
+ }
+
+ public void setExpiredCount(long expiredCount) {
+ this.expiredCount = expiredCount;
+ }
+
+ public BigDecimal getRedemptionRate() {
+ return redemptionRate;
+ }
+
+ public void setRedemptionRate(BigDecimal redemptionRate) {
+ this.redemptionRate = redemptionRate;
+ }
+
+ public BigDecimal getTotalDiscountAmount() {
+ return totalDiscountAmount;
+ }
+
+ public void setTotalDiscountAmount(BigDecimal totalDiscountAmount) {
+ this.totalDiscountAmount = totalDiscountAmount;
+ }
+}
diff --git a/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/domain/CouponTemplate.java b/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/domain/CouponTemplate.java
new file mode 100644
index 0000000..9bf309b
--- /dev/null
+++ b/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/domain/CouponTemplate.java
@@ -0,0 +1,209 @@
+package cn.novalon.gym.manage.coupon.domain;
+
+import cn.novalon.gym.manage.sys.core.domain.BaseDomain;
+import io.swagger.v3.oas.annotations.media.Schema;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+@Schema(description = "优惠券模板")
+public class CouponTemplate extends BaseDomain {
+
+ //优惠券名称
+ private String name;
+
+ @Schema(description = "优惠券描述")
+ private String description;
+
+ @Schema(description = "优惠券类型:CASH/DISCOUNT/COURSE/EXPERIENCE", example = "CASH")
+ private String couponType;
+
+ @Schema(description = "优惠值:满减/课程/体验为金额,折扣券为比例(0.9=9折)", example = "20.00")
+ private BigDecimal discountValue;
+
+ @Schema(description = "使用门槛金额", example = "100.00")
+ private BigDecimal thresholdAmount;
+
+ @Schema(description = "有效期类型:FIXED_DATE/DAYS_AFTER_CLAIM", example = "FIXED_DATE")
+ private String validityType;
+
+ @Schema(description = "固定有效期开始时间")
+ private LocalDateTime startTime;
+
+ @Schema(description = "固定有效期结束时间")
+ private LocalDateTime endTime;
+
+ @Schema(description = "领取后有效天数")
+ private Integer validDays;
+
+ @Schema(description = "适用商品范围:ALL/SPECIFIC", example = "ALL")
+ private String applyScope;
+
+ @Schema(description = "指定商品ID列表(JSON数组字符串)")
+ private String applyProductIds;
+
+ @Schema(description = "发放总量,-1表示不限量", example = "1000")
+ private Integer totalQuantity;
+
+ @Schema(description = "每人限领数量", example = "1")
+ private Integer perUserLimit;
+
+ @Schema(description = "是否可叠加使用", example = "false")
+ private Boolean stackable;
+
+ @Schema(description = "领取码")
+ private String claimCode;
+
+ @Schema(description = "状态:DRAFT/ACTIVE/TERMINATED/EXPIRED", example = "DRAFT")
+ private String status;
+
+ @Schema(description = "已发放数量")
+ private Integer issuedCount;
+
+ @Schema(description = "已使用数量")
+ private Integer usedCount;
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+
+ public void setDescription(String description) {
+ this.description = description;
+ }
+
+ public String getCouponType() {
+ return couponType;
+ }
+
+ public void setCouponType(String couponType) {
+ this.couponType = couponType;
+ }
+
+ public BigDecimal getDiscountValue() {
+ return discountValue;
+ }
+
+ public void setDiscountValue(BigDecimal discountValue) {
+ this.discountValue = discountValue;
+ }
+
+ public BigDecimal getThresholdAmount() {
+ return thresholdAmount;
+ }
+
+ public void setThresholdAmount(BigDecimal thresholdAmount) {
+ this.thresholdAmount = thresholdAmount;
+ }
+
+ public String getValidityType() {
+ return validityType;
+ }
+
+ public void setValidityType(String validityType) {
+ this.validityType = validityType;
+ }
+
+ public LocalDateTime getStartTime() {
+ return startTime;
+ }
+
+ public void setStartTime(LocalDateTime startTime) {
+ this.startTime = startTime;
+ }
+
+ public LocalDateTime getEndTime() {
+ return endTime;
+ }
+
+ public void setEndTime(LocalDateTime endTime) {
+ this.endTime = endTime;
+ }
+
+ public Integer getValidDays() {
+ return validDays;
+ }
+
+ public void setValidDays(Integer validDays) {
+ this.validDays = validDays;
+ }
+
+ public String getApplyScope() {
+ return applyScope;
+ }
+
+ public void setApplyScope(String applyScope) {
+ this.applyScope = applyScope;
+ }
+
+ public String getApplyProductIds() {
+ return applyProductIds;
+ }
+
+ public void setApplyProductIds(String applyProductIds) {
+ this.applyProductIds = applyProductIds;
+ }
+
+ public Integer getTotalQuantity() {
+ return totalQuantity;
+ }
+
+ public void setTotalQuantity(Integer totalQuantity) {
+ this.totalQuantity = totalQuantity;
+ }
+
+ public Integer getPerUserLimit() {
+ return perUserLimit;
+ }
+
+ public void setPerUserLimit(Integer perUserLimit) {
+ this.perUserLimit = perUserLimit;
+ }
+
+ public Boolean getStackable() {
+ return stackable;
+ }
+
+ public void setStackable(Boolean stackable) {
+ this.stackable = stackable;
+ }
+
+ public String getClaimCode() {
+ return claimCode;
+ }
+
+ public void setClaimCode(String claimCode) {
+ this.claimCode = claimCode;
+ }
+
+ public String getStatus() {
+ return status;
+ }
+
+ public void setStatus(String status) {
+ this.status = status;
+ }
+
+ public Integer getIssuedCount() {
+ return issuedCount;
+ }
+
+ public void setIssuedCount(Integer issuedCount) {
+ this.issuedCount = issuedCount;
+ }
+
+ public Integer getUsedCount() {
+ return usedCount;
+ }
+
+ public void setUsedCount(Integer usedCount) {
+ this.usedCount = usedCount;
+ }
+}
diff --git a/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/domain/DistributeCouponRequest.java b/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/domain/DistributeCouponRequest.java
new file mode 100644
index 0000000..ebf0fcf
--- /dev/null
+++ b/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/domain/DistributeCouponRequest.java
@@ -0,0 +1,32 @@
+package cn.novalon.gym.manage.coupon.domain;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+
+import java.math.BigDecimal;
+import java.util.List;
+
+@Schema(description = "优惠券发放请求")
+public class DistributeCouponRequest {
+
+ @Schema(description = "目标会员ID列表", requiredMode = Schema.RequiredMode.REQUIRED)
+ private List memberIds;
+
+ @Schema(description = "发放方式:MANUAL/BATCH,默认MANUAL", example = "MANUAL")
+ private String distributeType;
+
+ public List getMemberIds() {
+ return memberIds;
+ }
+
+ public void setMemberIds(List memberIds) {
+ this.memberIds = memberIds;
+ }
+
+ public String getDistributeType() {
+ return distributeType;
+ }
+
+ public void setDistributeType(String distributeType) {
+ this.distributeType = distributeType;
+ }
+}
diff --git a/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/domain/DistributeCouponResult.java b/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/domain/DistributeCouponResult.java
new file mode 100644
index 0000000..2519013
--- /dev/null
+++ b/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/domain/DistributeCouponResult.java
@@ -0,0 +1,49 @@
+package cn.novalon.gym.manage.coupon.domain;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+
+@Schema(description = "优惠券发放结果")
+public class DistributeCouponResult {
+
+ @Schema(description = "成功发放数量")
+ private int successCount;
+
+ @Schema(description = "失败数量")
+ private int failCount;
+
+ @Schema(description = "提示信息")
+ private String message;
+
+ public DistributeCouponResult() {
+ }
+
+ public DistributeCouponResult(int successCount, int failCount, String message) {
+ this.successCount = successCount;
+ this.failCount = failCount;
+ this.message = message;
+ }
+
+ public int getSuccessCount() {
+ return successCount;
+ }
+
+ public void setSuccessCount(int successCount) {
+ this.successCount = successCount;
+ }
+
+ public int getFailCount() {
+ return failCount;
+ }
+
+ public void setFailCount(int failCount) {
+ this.failCount = failCount;
+ }
+
+ public String getMessage() {
+ return message;
+ }
+
+ public void setMessage(String message) {
+ this.message = message;
+ }
+}
diff --git a/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/domain/MemberCoupon.java b/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/domain/MemberCoupon.java
new file mode 100644
index 0000000..788cfd0
--- /dev/null
+++ b/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/domain/MemberCoupon.java
@@ -0,0 +1,109 @@
+package cn.novalon.gym.manage.coupon.domain;
+
+import cn.novalon.gym.manage.sys.core.domain.BaseDomain;
+import io.swagger.v3.oas.annotations.media.Schema;
+
+import java.time.LocalDateTime;
+
+@Schema(description = "会员优惠券")
+public class MemberCoupon extends BaseDomain {
+
+ @Schema(description = "优惠券模板ID")
+ private Long templateId;
+
+ @Schema(description = "会员ID")
+ private Long memberId;
+
+ @Schema(description = "优惠券码")
+ private String couponCode;
+
+ @Schema(description = "状态:AVAILABLE/USED/EXPIRED/INVALID")
+ private String status;
+
+ @Schema(description = "发放方式:MANUAL/BATCH/AUTO/CLAIM")
+ private String distributeType;
+
+ @Schema(description = "领取时间")
+ private LocalDateTime receivedAt;
+
+ @Schema(description = "过期时间")
+ private LocalDateTime expireAt;
+
+ @Schema(description = "使用时间")
+ private LocalDateTime usedAt;
+
+ @Schema(description = "关联订单ID")
+ private Long orderId;
+
+ public Long getTemplateId() {
+ return templateId;
+ }
+
+ public void setTemplateId(Long templateId) {
+ this.templateId = templateId;
+ }
+
+ public Long getMemberId() {
+ return memberId;
+ }
+
+ public void setMemberId(Long memberId) {
+ this.memberId = memberId;
+ }
+
+ public String getCouponCode() {
+ return couponCode;
+ }
+
+ public void setCouponCode(String couponCode) {
+ this.couponCode = couponCode;
+ }
+
+ public String getStatus() {
+ return status;
+ }
+
+ public void setStatus(String status) {
+ this.status = status;
+ }
+
+ public String getDistributeType() {
+ return distributeType;
+ }
+
+ public void setDistributeType(String distributeType) {
+ this.distributeType = distributeType;
+ }
+
+ public LocalDateTime getReceivedAt() {
+ return receivedAt;
+ }
+
+ public void setReceivedAt(LocalDateTime receivedAt) {
+ this.receivedAt = receivedAt;
+ }
+
+ public LocalDateTime getExpireAt() {
+ return expireAt;
+ }
+
+ public void setExpireAt(LocalDateTime expireAt) {
+ this.expireAt = expireAt;
+ }
+
+ public LocalDateTime getUsedAt() {
+ return usedAt;
+ }
+
+ public void setUsedAt(LocalDateTime usedAt) {
+ this.usedAt = usedAt;
+ }
+
+ public Long getOrderId() {
+ return orderId;
+ }
+
+ public void setOrderId(Long orderId) {
+ this.orderId = orderId;
+ }
+}
diff --git a/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/entity/CouponTemplateEntity.java b/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/entity/CouponTemplateEntity.java
new file mode 100644
index 0000000..62c3b8d
--- /dev/null
+++ b/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/entity/CouponTemplateEntity.java
@@ -0,0 +1,210 @@
+package cn.novalon.gym.manage.coupon.entity;
+
+import cn.novalon.gym.manage.db.entity.BaseEntity;
+import org.springframework.data.relational.core.mapping.Column;
+import org.springframework.data.relational.core.mapping.Table;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+@Table("coupon_template")
+public class CouponTemplateEntity extends BaseEntity {
+
+ @Column("name")
+ private String name;
+
+ @Column("description")
+ private String description;
+
+ @Column("coupon_type")
+ private String couponType;
+
+ @Column("discount_value")
+ private BigDecimal discountValue;
+
+ @Column("threshold_amount")
+ private BigDecimal thresholdAmount;
+
+ @Column("validity_type")
+ private String validityType;
+
+ @Column("start_time")
+ private LocalDateTime startTime;
+
+ @Column("end_time")
+ private LocalDateTime endTime;
+
+ @Column("valid_days")
+ private Integer validDays;
+
+ @Column("apply_scope")
+ private String applyScope;
+
+ @Column("apply_product_ids")
+ private String applyProductIds;
+
+ @Column("total_quantity")
+ private Integer totalQuantity;
+
+ @Column("per_user_limit")
+ private Integer perUserLimit;
+
+ @Column("stackable")
+ private Boolean stackable;
+
+ @Column("claim_code")
+ private String claimCode;
+
+ @Column("status")
+ private String status;
+
+ @Column("issued_count")
+ private Integer issuedCount;
+
+ @Column("used_count")
+ private Integer usedCount;
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+
+ public void setDescription(String description) {
+ this.description = description;
+ }
+
+ public String getCouponType() {
+ return couponType;
+ }
+
+ public void setCouponType(String couponType) {
+ this.couponType = couponType;
+ }
+
+ public BigDecimal getDiscountValue() {
+ return discountValue;
+ }
+
+ public void setDiscountValue(BigDecimal discountValue) {
+ this.discountValue = discountValue;
+ }
+
+ public BigDecimal getThresholdAmount() {
+ return thresholdAmount;
+ }
+
+ public void setThresholdAmount(BigDecimal thresholdAmount) {
+ this.thresholdAmount = thresholdAmount;
+ }
+
+ public String getValidityType() {
+ return validityType;
+ }
+
+ public void setValidityType(String validityType) {
+ this.validityType = validityType;
+ }
+
+ public LocalDateTime getStartTime() {
+ return startTime;
+ }
+
+ public void setStartTime(LocalDateTime startTime) {
+ this.startTime = startTime;
+ }
+
+ public LocalDateTime getEndTime() {
+ return endTime;
+ }
+
+ public void setEndTime(LocalDateTime endTime) {
+ this.endTime = endTime;
+ }
+
+ public Integer getValidDays() {
+ return validDays;
+ }
+
+ public void setValidDays(Integer validDays) {
+ this.validDays = validDays;
+ }
+
+ public String getApplyScope() {
+ return applyScope;
+ }
+
+ public void setApplyScope(String applyScope) {
+ this.applyScope = applyScope;
+ }
+
+ public String getApplyProductIds() {
+ return applyProductIds;
+ }
+
+ public void setApplyProductIds(String applyProductIds) {
+ this.applyProductIds = applyProductIds;
+ }
+
+ public Integer getTotalQuantity() {
+ return totalQuantity;
+ }
+
+ public void setTotalQuantity(Integer totalQuantity) {
+ this.totalQuantity = totalQuantity;
+ }
+
+ public Integer getPerUserLimit() {
+ return perUserLimit;
+ }
+
+ public void setPerUserLimit(Integer perUserLimit) {
+ this.perUserLimit = perUserLimit;
+ }
+
+ public Boolean getStackable() {
+ return stackable;
+ }
+
+ public void setStackable(Boolean stackable) {
+ this.stackable = stackable;
+ }
+
+ public String getClaimCode() {
+ return claimCode;
+ }
+
+ public void setClaimCode(String claimCode) {
+ this.claimCode = claimCode;
+ }
+
+ public String getStatus() {
+ return status;
+ }
+
+ public void setStatus(String status) {
+ this.status = status;
+ }
+
+ public Integer getIssuedCount() {
+ return issuedCount;
+ }
+
+ public void setIssuedCount(Integer issuedCount) {
+ this.issuedCount = issuedCount;
+ }
+
+ public Integer getUsedCount() {
+ return usedCount;
+ }
+
+ public void setUsedCount(Integer usedCount) {
+ this.usedCount = usedCount;
+ }
+}
diff --git a/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/entity/MemberCouponEntity.java b/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/entity/MemberCouponEntity.java
new file mode 100644
index 0000000..545b076
--- /dev/null
+++ b/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/entity/MemberCouponEntity.java
@@ -0,0 +1,110 @@
+package cn.novalon.gym.manage.coupon.entity;
+
+import cn.novalon.gym.manage.db.entity.BaseEntity;
+import org.springframework.data.relational.core.mapping.Column;
+import org.springframework.data.relational.core.mapping.Table;
+
+import java.time.LocalDateTime;
+
+@Table("member_coupon")
+public class MemberCouponEntity extends BaseEntity {
+
+ @Column("template_id")
+ private Long templateId;
+
+ @Column("member_id")
+ private Long memberId;
+
+ @Column("coupon_code")
+ private String couponCode;
+
+ @Column("status")
+ private String status;
+
+ @Column("distribute_type")
+ private String distributeType;
+
+ @Column("received_at")
+ private LocalDateTime receivedAt;
+
+ @Column("expire_at")
+ private LocalDateTime expireAt;
+
+ @Column("used_at")
+ private LocalDateTime usedAt;
+
+ @Column("order_id")
+ private Long orderId;
+
+ public Long getTemplateId() {
+ return templateId;
+ }
+
+ public void setTemplateId(Long templateId) {
+ this.templateId = templateId;
+ }
+
+ public Long getMemberId() {
+ return memberId;
+ }
+
+ public void setMemberId(Long memberId) {
+ this.memberId = memberId;
+ }
+
+ public String getCouponCode() {
+ return couponCode;
+ }
+
+ public void setCouponCode(String couponCode) {
+ this.couponCode = couponCode;
+ }
+
+ public String getStatus() {
+ return status;
+ }
+
+ public void setStatus(String status) {
+ this.status = status;
+ }
+
+ public String getDistributeType() {
+ return distributeType;
+ }
+
+ public void setDistributeType(String distributeType) {
+ this.distributeType = distributeType;
+ }
+
+ public LocalDateTime getReceivedAt() {
+ return receivedAt;
+ }
+
+ public void setReceivedAt(LocalDateTime receivedAt) {
+ this.receivedAt = receivedAt;
+ }
+
+ public LocalDateTime getExpireAt() {
+ return expireAt;
+ }
+
+ public void setExpireAt(LocalDateTime expireAt) {
+ this.expireAt = expireAt;
+ }
+
+ public LocalDateTime getUsedAt() {
+ return usedAt;
+ }
+
+ public void setUsedAt(LocalDateTime usedAt) {
+ this.usedAt = usedAt;
+ }
+
+ public Long getOrderId() {
+ return orderId;
+ }
+
+ public void setOrderId(Long orderId) {
+ this.orderId = orderId;
+ }
+}
diff --git a/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/enums/ApplyScope.java b/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/enums/ApplyScope.java
new file mode 100644
index 0000000..b33e9ca
--- /dev/null
+++ b/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/enums/ApplyScope.java
@@ -0,0 +1,11 @@
+package cn.novalon.gym.manage.coupon.enums;
+
+/**
+ * 优惠券适用商品范围
+ */
+public enum ApplyScope {
+ /** 全场通用 */
+ ALL,
+ /** 指定商品 */
+ SPECIFIC
+}
diff --git a/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/enums/CouponTemplateStatus.java b/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/enums/CouponTemplateStatus.java
new file mode 100644
index 0000000..1c5f302
--- /dev/null
+++ b/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/enums/CouponTemplateStatus.java
@@ -0,0 +1,15 @@
+package cn.novalon.gym.manage.coupon.enums;
+
+/**
+ * 优惠券模板状态
+ */
+public enum CouponTemplateStatus {
+ /** 草稿 */
+ DRAFT,
+ /** 进行中 */
+ ACTIVE,
+ /** 已终止 */
+ TERMINATED,
+ /** 已过期 */
+ EXPIRED
+}
diff --git a/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/enums/CouponType.java b/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/enums/CouponType.java
new file mode 100644
index 0000000..3c54205
--- /dev/null
+++ b/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/enums/CouponType.java
@@ -0,0 +1,15 @@
+package cn.novalon.gym.manage.coupon.enums;
+
+/**
+ * 优惠券类型
+ */
+public enum CouponType {
+ /** 满减券 */
+ CASH,
+ /** 折扣券 */
+ DISCOUNT,
+ /** 课程券 */
+ COURSE,
+ /** 体验券(会员体验专用) */
+ EXPERIENCE
+}
diff --git a/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/enums/DistributeType.java b/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/enums/DistributeType.java
new file mode 100644
index 0000000..99a49ee
--- /dev/null
+++ b/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/enums/DistributeType.java
@@ -0,0 +1,15 @@
+package cn.novalon.gym.manage.coupon.enums;
+
+/**
+ * 优惠券发放方式
+ */
+public enum DistributeType {
+ /** 手动发放(指定会员) */
+ MANUAL,
+ /** 批量发放(按会员分组) */
+ BATCH,
+ /** 自动发放(触发规则) */
+ AUTO,
+ /** 领取码/二维码领取 */
+ CLAIM
+}
diff --git a/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/enums/MemberCouponStatus.java b/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/enums/MemberCouponStatus.java
new file mode 100644
index 0000000..972225c
--- /dev/null
+++ b/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/enums/MemberCouponStatus.java
@@ -0,0 +1,15 @@
+package cn.novalon.gym.manage.coupon.enums;
+
+/**
+ * 会员优惠券状态
+ */
+public enum MemberCouponStatus {
+ /** 可用 */
+ AVAILABLE,
+ /** 已使用 */
+ USED,
+ /** 已过期 */
+ EXPIRED,
+ /** 已作废 */
+ INVALID
+}
diff --git a/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/enums/ValidityType.java b/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/enums/ValidityType.java
new file mode 100644
index 0000000..b680c90
--- /dev/null
+++ b/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/enums/ValidityType.java
@@ -0,0 +1,11 @@
+package cn.novalon.gym.manage.coupon.enums;
+
+/**
+ * 优惠券有效期类型
+ */
+public enum ValidityType {
+ /** 固定日期 */
+ FIXED_DATE,
+ /** 领取后X天 */
+ DAYS_AFTER_CLAIM
+}
diff --git a/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/flashsale/converter/FlashSaleConverter.java b/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/flashsale/converter/FlashSaleConverter.java
new file mode 100644
index 0000000..1069d3c
--- /dev/null
+++ b/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/flashsale/converter/FlashSaleConverter.java
@@ -0,0 +1,77 @@
+package cn.novalon.gym.manage.coupon.flashsale.converter;
+
+import cn.hutool.core.bean.BeanUtil;
+import cn.novalon.gym.manage.coupon.flashsale.domain.FlashSaleActivity;
+import cn.novalon.gym.manage.coupon.flashsale.domain.FlashSaleItem;
+import cn.novalon.gym.manage.coupon.flashsale.domain.FlashSaleOrder;
+import cn.novalon.gym.manage.coupon.flashsale.entity.FlashSaleActivityEntity;
+import cn.novalon.gym.manage.coupon.flashsale.entity.FlashSaleItemEntity;
+import cn.novalon.gym.manage.coupon.flashsale.entity.FlashSaleOrderEntity;
+import org.springframework.stereotype.Component;
+
+@Component
+public class FlashSaleConverter {
+
+ public FlashSaleActivity toActivity(FlashSaleActivityEntity entity) {
+ if (entity == null) {
+ return null;
+ }
+ FlashSaleActivity domain = new FlashSaleActivity();
+ BeanUtil.copyProperties(entity, domain);
+ return domain;
+ }
+
+ public FlashSaleActivityEntity toActivityEntity(FlashSaleActivity domain) {
+ if (domain == null) {
+ return null;
+ }
+ FlashSaleActivityEntity entity = new FlashSaleActivityEntity();
+ BeanUtil.copyProperties(domain, entity);
+ if (domain.getId() != null) {
+ entity.markNotNew();
+ }
+ return entity;
+ }
+
+ public FlashSaleItem toItem(FlashSaleItemEntity entity) {
+ if (entity == null) {
+ return null;
+ }
+ FlashSaleItem domain = new FlashSaleItem();
+ BeanUtil.copyProperties(entity, domain);
+ return domain;
+ }
+
+ public FlashSaleItemEntity toItemEntity(FlashSaleItem domain) {
+ if (domain == null) {
+ return null;
+ }
+ FlashSaleItemEntity entity = new FlashSaleItemEntity();
+ BeanUtil.copyProperties(domain, entity);
+ if (domain.getId() != null) {
+ entity.markNotNew();
+ }
+ return entity;
+ }
+
+ public FlashSaleOrder toOrder(FlashSaleOrderEntity entity) {
+ if (entity == null) {
+ return null;
+ }
+ FlashSaleOrder domain = new FlashSaleOrder();
+ BeanUtil.copyProperties(entity, domain);
+ return domain;
+ }
+
+ public FlashSaleOrderEntity toOrderEntity(FlashSaleOrder domain) {
+ if (domain == null) {
+ return null;
+ }
+ FlashSaleOrderEntity entity = new FlashSaleOrderEntity();
+ BeanUtil.copyProperties(domain, entity);
+ if (domain.getId() != null) {
+ entity.markNotNew();
+ }
+ return entity;
+ }
+}
diff --git a/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/flashsale/dao/FlashSaleActivityDao.java b/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/flashsale/dao/FlashSaleActivityDao.java
new file mode 100644
index 0000000..807ab13
--- /dev/null
+++ b/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/flashsale/dao/FlashSaleActivityDao.java
@@ -0,0 +1,31 @@
+package cn.novalon.gym.manage.coupon.flashsale.dao;
+
+import cn.novalon.gym.manage.coupon.flashsale.entity.FlashSaleActivityEntity;
+import org.springframework.data.r2dbc.repository.Modifying;
+import org.springframework.data.r2dbc.repository.Query;
+import org.springframework.data.r2dbc.repository.R2dbcRepository;
+import org.springframework.stereotype.Repository;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+import java.time.LocalDateTime;
+
+@Repository
+public interface FlashSaleActivityDao extends R2dbcRepository {
+
+ Mono findByIdIsAndDeletedAtIsNull(Long id);
+
+ Flux findAllByDeletedAtIsNull();
+
+ Flux findByNameContainingAndDeletedAtIsNull(String name);
+
+ Flux findByStatusAndDeletedAtIsNull(String status);
+
+ @Modifying
+ @Query("UPDATE flash_sale_activity SET deleted_at = :deletedAt WHERE id = :id")
+ Mono softDelete(Long id, LocalDateTime deletedAt);
+
+ @Modifying
+ @Query("UPDATE flash_sale_activity SET status = :status, updated_at = :updatedAt WHERE id = :id")
+ Mono updateStatus(Long id, String status, LocalDateTime updatedAt);
+}
diff --git a/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/flashsale/dao/FlashSaleItemDao.java b/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/flashsale/dao/FlashSaleItemDao.java
new file mode 100644
index 0000000..a48ea6f
--- /dev/null
+++ b/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/flashsale/dao/FlashSaleItemDao.java
@@ -0,0 +1,31 @@
+package cn.novalon.gym.manage.coupon.flashsale.dao;
+
+import cn.novalon.gym.manage.coupon.flashsale.entity.FlashSaleItemEntity;
+import org.springframework.data.r2dbc.repository.Modifying;
+import org.springframework.data.r2dbc.repository.Query;
+import org.springframework.data.r2dbc.repository.R2dbcRepository;
+import org.springframework.stereotype.Repository;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+import java.time.LocalDateTime;
+
+@Repository
+public interface FlashSaleItemDao extends R2dbcRepository {
+
+ Mono findByIdIsAndDeletedAtIsNull(Long id);
+
+ Flux findByActivityIdAndDeletedAtIsNull(Long activityId);
+
+ @Modifying
+ @Query("UPDATE flash_sale_item SET stock = stock - :quantity, updated_at = :updatedAt WHERE id = :id AND stock >= :quantity")
+ Mono deductStock(Long id, int quantity, LocalDateTime updatedAt);
+
+ @Modifying
+ @Query("UPDATE flash_sale_item SET stock = stock + :quantity, updated_at = :updatedAt WHERE id = :id")
+ Mono restoreStock(Long id, int quantity, LocalDateTime updatedAt);
+
+ @Modifying
+ @Query("UPDATE flash_sale_item SET sold_count = sold_count + :count, updated_at = :updatedAt WHERE id = :id")
+ Mono incrementSoldCount(Long id, int count, LocalDateTime updatedAt);
+}
diff --git a/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/flashsale/dao/FlashSaleOrderDao.java b/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/flashsale/dao/FlashSaleOrderDao.java
new file mode 100644
index 0000000..7fb8d7e
--- /dev/null
+++ b/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/flashsale/dao/FlashSaleOrderDao.java
@@ -0,0 +1,38 @@
+package cn.novalon.gym.manage.coupon.flashsale.dao;
+
+import cn.novalon.gym.manage.coupon.flashsale.entity.FlashSaleOrderEntity;
+import org.springframework.data.r2dbc.repository.Modifying;
+import org.springframework.data.r2dbc.repository.Query;
+import org.springframework.data.r2dbc.repository.R2dbcRepository;
+import org.springframework.stereotype.Repository;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+import java.time.LocalDateTime;
+
+@Repository
+public interface FlashSaleOrderDao extends R2dbcRepository {
+
+ Mono findByIdIsAndDeletedAtIsNull(Long id);
+
+ Flux findByMemberIdAndDeletedAtIsNull(Long memberId);
+
+ Flux findByActivityIdAndDeletedAtIsNull(Long activityId);
+
+ Mono countByActivityIdAndMemberIdAndStatusAndDeletedAtIsNull(
+ Long activityId, Long memberId, String status);
+
+ Mono countByItemIdAndMemberIdAndStatusInAndDeletedAtIsNull(
+ Long itemId, Long memberId, java.util.Collection statuses);
+
+ @Query("SELECT * FROM flash_sale_order WHERE status = :status AND expire_at < :now AND deleted_at IS NULL")
+ Flux findExpiredPendingOrders(String status, LocalDateTime now);
+
+ @Modifying
+ @Query("UPDATE flash_sale_order SET status = :status, updated_at = :updatedAt WHERE id = :id")
+ Mono updateStatus(Long id, String status, LocalDateTime updatedAt);
+
+ @Modifying
+ @Query("UPDATE flash_sale_order SET status = :status, pay_at = :payAt, updated_at = :updatedAt WHERE id = :id")
+ Mono markPaid(Long id, String status, LocalDateTime payAt, LocalDateTime updatedAt);
+}
diff --git a/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/flashsale/domain/FlashSaleActivity.java b/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/flashsale/domain/FlashSaleActivity.java
new file mode 100644
index 0000000..5aaec01
--- /dev/null
+++ b/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/flashsale/domain/FlashSaleActivity.java
@@ -0,0 +1,74 @@
+package cn.novalon.gym.manage.coupon.flashsale.domain;
+
+import cn.novalon.gym.manage.sys.core.domain.BaseDomain;
+import io.swagger.v3.oas.annotations.media.Schema;
+
+import java.time.LocalDateTime;
+
+@Schema(description = "秒杀活动")
+public class FlashSaleActivity extends BaseDomain {
+
+ private String name;
+ private String description;
+ private LocalDateTime startTime;
+ private LocalDateTime endTime;
+ private Integer payTimeoutMinutes;
+ private Integer perUserLimit;
+ private String status;
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+
+ public void setDescription(String description) {
+ this.description = description;
+ }
+
+ public LocalDateTime getStartTime() {
+ return startTime;
+ }
+
+ public void setStartTime(LocalDateTime startTime) {
+ this.startTime = startTime;
+ }
+
+ public LocalDateTime getEndTime() {
+ return endTime;
+ }
+
+ public void setEndTime(LocalDateTime endTime) {
+ this.endTime = endTime;
+ }
+
+ public Integer getPayTimeoutMinutes() {
+ return payTimeoutMinutes;
+ }
+
+ public void setPayTimeoutMinutes(Integer payTimeoutMinutes) {
+ this.payTimeoutMinutes = payTimeoutMinutes;
+ }
+
+ public Integer getPerUserLimit() {
+ return perUserLimit;
+ }
+
+ public void setPerUserLimit(Integer perUserLimit) {
+ this.perUserLimit = perUserLimit;
+ }
+
+ public String getStatus() {
+ return status;
+ }
+
+ public void setStatus(String status) {
+ this.status = status;
+ }
+}
diff --git a/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/flashsale/domain/FlashSaleItem.java b/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/flashsale/domain/FlashSaleItem.java
new file mode 100644
index 0000000..e5bbeae
--- /dev/null
+++ b/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/flashsale/domain/FlashSaleItem.java
@@ -0,0 +1,92 @@
+package cn.novalon.gym.manage.coupon.flashsale.domain;
+
+import cn.novalon.gym.manage.sys.core.domain.BaseDomain;
+import io.swagger.v3.oas.annotations.media.Schema;
+
+import java.math.BigDecimal;
+
+@Schema(description = "秒杀商品")
+public class FlashSaleItem extends BaseDomain {
+
+ private Long activityId;
+ private String productType;
+ private Long productId;
+ private String productName;
+ private BigDecimal originalPrice;
+ private BigDecimal seckillPrice;
+ private Integer stock;
+ private Integer soldCount;
+ private Integer perUserLimit;
+
+ public Long getActivityId() {
+ return activityId;
+ }
+
+ public void setActivityId(Long activityId) {
+ this.activityId = activityId;
+ }
+
+ public String getProductType() {
+ return productType;
+ }
+
+ public void setProductType(String productType) {
+ this.productType = productType;
+ }
+
+ public Long getProductId() {
+ return productId;
+ }
+
+ public void setProductId(Long productId) {
+ this.productId = productId;
+ }
+
+ public String getProductName() {
+ return productName;
+ }
+
+ public void setProductName(String productName) {
+ this.productName = productName;
+ }
+
+ public BigDecimal getOriginalPrice() {
+ return originalPrice;
+ }
+
+ public void setOriginalPrice(BigDecimal originalPrice) {
+ this.originalPrice = originalPrice;
+ }
+
+ public BigDecimal getSeckillPrice() {
+ return seckillPrice;
+ }
+
+ public void setSeckillPrice(BigDecimal seckillPrice) {
+ this.seckillPrice = seckillPrice;
+ }
+
+ public Integer getStock() {
+ return stock;
+ }
+
+ public void setStock(Integer stock) {
+ this.stock = stock;
+ }
+
+ public Integer getSoldCount() {
+ return soldCount;
+ }
+
+ public void setSoldCount(Integer soldCount) {
+ this.soldCount = soldCount;
+ }
+
+ public Integer getPerUserLimit() {
+ return perUserLimit;
+ }
+
+ public void setPerUserLimit(Integer perUserLimit) {
+ this.perUserLimit = perUserLimit;
+ }
+}
diff --git a/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/flashsale/domain/FlashSaleOrder.java b/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/flashsale/domain/FlashSaleOrder.java
new file mode 100644
index 0000000..af37089
--- /dev/null
+++ b/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/flashsale/domain/FlashSaleOrder.java
@@ -0,0 +1,84 @@
+package cn.novalon.gym.manage.coupon.flashsale.domain;
+
+import cn.novalon.gym.manage.sys.core.domain.BaseDomain;
+import io.swagger.v3.oas.annotations.media.Schema;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+@Schema(description = "秒杀订单")
+public class FlashSaleOrder extends BaseDomain {
+
+ private Long activityId;
+ private Long itemId;
+ private Long memberId;
+ private Integer quantity;
+ private BigDecimal payAmount;
+ private String status;
+ private LocalDateTime expireAt;
+ private LocalDateTime payAt;
+
+ public Long getActivityId() {
+ return activityId;
+ }
+
+ public void setActivityId(Long activityId) {
+ this.activityId = activityId;
+ }
+
+ public Long getItemId() {
+ return itemId;
+ }
+
+ public void setItemId(Long itemId) {
+ this.itemId = itemId;
+ }
+
+ public Long getMemberId() {
+ return memberId;
+ }
+
+ public void setMemberId(Long memberId) {
+ this.memberId = memberId;
+ }
+
+ public Integer getQuantity() {
+ return quantity;
+ }
+
+ public void setQuantity(Integer quantity) {
+ this.quantity = quantity;
+ }
+
+ public BigDecimal getPayAmount() {
+ return payAmount;
+ }
+
+ public void setPayAmount(BigDecimal payAmount) {
+ this.payAmount = payAmount;
+ }
+
+ public String getStatus() {
+ return status;
+ }
+
+ public void setStatus(String status) {
+ this.status = status;
+ }
+
+ public LocalDateTime getExpireAt() {
+ return expireAt;
+ }
+
+ public void setExpireAt(LocalDateTime expireAt) {
+ this.expireAt = expireAt;
+ }
+
+ public LocalDateTime getPayAt() {
+ return payAt;
+ }
+
+ public void setPayAt(LocalDateTime payAt) {
+ this.payAt = payAt;
+ }
+}
diff --git a/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/flashsale/domain/FlashSaleStatistics.java b/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/flashsale/domain/FlashSaleStatistics.java
new file mode 100644
index 0000000..cce9433
--- /dev/null
+++ b/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/flashsale/domain/FlashSaleStatistics.java
@@ -0,0 +1,82 @@
+package cn.novalon.gym.manage.coupon.flashsale.domain;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+
+import java.math.BigDecimal;
+
+@Schema(description = "秒杀统计数据")
+public class FlashSaleStatistics {
+
+ private Long activityId;
+ private long totalOrders;
+ private long pendingOrders;
+ private long paidOrders;
+ private long cancelledOrders;
+ private long expiredOrders;
+ private long totalSoldQuantity;
+ private BigDecimal totalRevenue;
+
+ public Long getActivityId() {
+ return activityId;
+ }
+
+ public void setActivityId(Long activityId) {
+ this.activityId = activityId;
+ }
+
+ public long getTotalOrders() {
+ return totalOrders;
+ }
+
+ public void setTotalOrders(long totalOrders) {
+ this.totalOrders = totalOrders;
+ }
+
+ public long getPendingOrders() {
+ return pendingOrders;
+ }
+
+ public void setPendingOrders(long pendingOrders) {
+ this.pendingOrders = pendingOrders;
+ }
+
+ public long getPaidOrders() {
+ return paidOrders;
+ }
+
+ public void setPaidOrders(long paidOrders) {
+ this.paidOrders = paidOrders;
+ }
+
+ public long getCancelledOrders() {
+ return cancelledOrders;
+ }
+
+ public void setCancelledOrders(long cancelledOrders) {
+ this.cancelledOrders = cancelledOrders;
+ }
+
+ public long getExpiredOrders() {
+ return expiredOrders;
+ }
+
+ public void setExpiredOrders(long expiredOrders) {
+ this.expiredOrders = expiredOrders;
+ }
+
+ public long getTotalSoldQuantity() {
+ return totalSoldQuantity;
+ }
+
+ public void setTotalSoldQuantity(long totalSoldQuantity) {
+ this.totalSoldQuantity = totalSoldQuantity;
+ }
+
+ public BigDecimal getTotalRevenue() {
+ return totalRevenue;
+ }
+
+ public void setTotalRevenue(BigDecimal totalRevenue) {
+ this.totalRevenue = totalRevenue;
+ }
+}
diff --git a/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/flashsale/domain/GrabRequest.java b/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/flashsale/domain/GrabRequest.java
new file mode 100644
index 0000000..cbb594a
--- /dev/null
+++ b/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/flashsale/domain/GrabRequest.java
@@ -0,0 +1,35 @@
+package cn.novalon.gym.manage.coupon.flashsale.domain;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+
+@Schema(description = "秒杀抢购请求")
+public class GrabRequest {
+
+ private Long itemId;
+ private Long memberId;
+ private Integer quantity;
+
+ public Long getItemId() {
+ return itemId;
+ }
+
+ public void setItemId(Long itemId) {
+ this.itemId = itemId;
+ }
+
+ public Long getMemberId() {
+ return memberId;
+ }
+
+ public void setMemberId(Long memberId) {
+ this.memberId = memberId;
+ }
+
+ public Integer getQuantity() {
+ return quantity;
+ }
+
+ public void setQuantity(Integer quantity) {
+ this.quantity = quantity;
+ }
+}
diff --git a/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/flashsale/entity/FlashSaleActivityEntity.java b/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/flashsale/entity/FlashSaleActivityEntity.java
new file mode 100644
index 0000000..9a043db
--- /dev/null
+++ b/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/flashsale/entity/FlashSaleActivityEntity.java
@@ -0,0 +1,88 @@
+package cn.novalon.gym.manage.coupon.flashsale.entity;
+
+import cn.novalon.gym.manage.db.entity.BaseEntity;
+import org.springframework.data.relational.core.mapping.Column;
+import org.springframework.data.relational.core.mapping.Table;
+
+import java.time.LocalDateTime;
+
+@Table("flash_sale_activity")
+public class FlashSaleActivityEntity extends BaseEntity {
+
+ @Column("name")
+ private String name;
+
+ @Column("description")
+ private String description;
+
+ @Column("start_time")
+ private LocalDateTime startTime;
+
+ @Column("end_time")
+ private LocalDateTime endTime;
+
+ @Column("pay_timeout_minutes")
+ private Integer payTimeoutMinutes;
+
+ @Column("per_user_limit")
+ private Integer perUserLimit;
+
+ @Column("status")
+ private String status;
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+
+ public void setDescription(String description) {
+ this.description = description;
+ }
+
+ public LocalDateTime getStartTime() {
+ return startTime;
+ }
+
+ public void setStartTime(LocalDateTime startTime) {
+ this.startTime = startTime;
+ }
+
+ public LocalDateTime getEndTime() {
+ return endTime;
+ }
+
+ public void setEndTime(LocalDateTime endTime) {
+ this.endTime = endTime;
+ }
+
+ public Integer getPayTimeoutMinutes() {
+ return payTimeoutMinutes;
+ }
+
+ public void setPayTimeoutMinutes(Integer payTimeoutMinutes) {
+ this.payTimeoutMinutes = payTimeoutMinutes;
+ }
+
+ public Integer getPerUserLimit() {
+ return perUserLimit;
+ }
+
+ public void setPerUserLimit(Integer perUserLimit) {
+ this.perUserLimit = perUserLimit;
+ }
+
+ public String getStatus() {
+ return status;
+ }
+
+ public void setStatus(String status) {
+ this.status = status;
+ }
+}
diff --git a/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/flashsale/entity/FlashSaleItemEntity.java b/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/flashsale/entity/FlashSaleItemEntity.java
new file mode 100644
index 0000000..00c1e0f
--- /dev/null
+++ b/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/flashsale/entity/FlashSaleItemEntity.java
@@ -0,0 +1,110 @@
+package cn.novalon.gym.manage.coupon.flashsale.entity;
+
+import cn.novalon.gym.manage.db.entity.BaseEntity;
+import org.springframework.data.relational.core.mapping.Column;
+import org.springframework.data.relational.core.mapping.Table;
+
+import java.math.BigDecimal;
+
+@Table("flash_sale_item")
+public class FlashSaleItemEntity extends BaseEntity {
+
+ @Column("activity_id")
+ private Long activityId;
+
+ @Column("product_type")
+ private String productType;
+
+ @Column("product_id")
+ private Long productId;
+
+ @Column("product_name")
+ private String productName;
+
+ @Column("original_price")
+ private BigDecimal originalPrice;
+
+ @Column("seckill_price")
+ private BigDecimal seckillPrice;
+
+ @Column("stock")
+ private Integer stock;
+
+ @Column("sold_count")
+ private Integer soldCount;
+
+ @Column("per_user_limit")
+ private Integer perUserLimit;
+
+ public Long getActivityId() {
+ return activityId;
+ }
+
+ public void setActivityId(Long activityId) {
+ this.activityId = activityId;
+ }
+
+ public String getProductType() {
+ return productType;
+ }
+
+ public void setProductType(String productType) {
+ this.productType = productType;
+ }
+
+ public Long getProductId() {
+ return productId;
+ }
+
+ public void setProductId(Long productId) {
+ this.productId = productId;
+ }
+
+ public String getProductName() {
+ return productName;
+ }
+
+ public void setProductName(String productName) {
+ this.productName = productName;
+ }
+
+ public BigDecimal getOriginalPrice() {
+ return originalPrice;
+ }
+
+ public void setOriginalPrice(BigDecimal originalPrice) {
+ this.originalPrice = originalPrice;
+ }
+
+ public BigDecimal getSeckillPrice() {
+ return seckillPrice;
+ }
+
+ public void setSeckillPrice(BigDecimal seckillPrice) {
+ this.seckillPrice = seckillPrice;
+ }
+
+ public Integer getStock() {
+ return stock;
+ }
+
+ public void setStock(Integer stock) {
+ this.stock = stock;
+ }
+
+ public Integer getSoldCount() {
+ return soldCount;
+ }
+
+ public void setSoldCount(Integer soldCount) {
+ this.soldCount = soldCount;
+ }
+
+ public Integer getPerUserLimit() {
+ return perUserLimit;
+ }
+
+ public void setPerUserLimit(Integer perUserLimit) {
+ this.perUserLimit = perUserLimit;
+ }
+}
diff --git a/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/flashsale/entity/FlashSaleOrderEntity.java b/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/flashsale/entity/FlashSaleOrderEntity.java
new file mode 100644
index 0000000..11d801c
--- /dev/null
+++ b/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/flashsale/entity/FlashSaleOrderEntity.java
@@ -0,0 +1,100 @@
+package cn.novalon.gym.manage.coupon.flashsale.entity;
+
+import cn.novalon.gym.manage.db.entity.BaseEntity;
+import org.springframework.data.relational.core.mapping.Column;
+import org.springframework.data.relational.core.mapping.Table;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+@Table("flash_sale_order")
+public class FlashSaleOrderEntity extends BaseEntity {
+
+ @Column("activity_id")
+ private Long activityId;
+
+ @Column("item_id")
+ private Long itemId;
+
+ @Column("member_id")
+ private Long memberId;
+
+ @Column("quantity")
+ private Integer quantity;
+
+ @Column("pay_amount")
+ private BigDecimal payAmount;
+
+ @Column("status")
+ private String status;
+
+ @Column("expire_at")
+ private LocalDateTime expireAt;
+
+ @Column("pay_at")
+ private LocalDateTime payAt;
+
+ public Long getActivityId() {
+ return activityId;
+ }
+
+ public void setActivityId(Long activityId) {
+ this.activityId = activityId;
+ }
+
+ public Long getItemId() {
+ return itemId;
+ }
+
+ public void setItemId(Long itemId) {
+ this.itemId = itemId;
+ }
+
+ public Long getMemberId() {
+ return memberId;
+ }
+
+ public void setMemberId(Long memberId) {
+ this.memberId = memberId;
+ }
+
+ public Integer getQuantity() {
+ return quantity;
+ }
+
+ public void setQuantity(Integer quantity) {
+ this.quantity = quantity;
+ }
+
+ public BigDecimal getPayAmount() {
+ return payAmount;
+ }
+
+ public void setPayAmount(BigDecimal payAmount) {
+ this.payAmount = payAmount;
+ }
+
+ public String getStatus() {
+ return status;
+ }
+
+ public void setStatus(String status) {
+ this.status = status;
+ }
+
+ public LocalDateTime getExpireAt() {
+ return expireAt;
+ }
+
+ public void setExpireAt(LocalDateTime expireAt) {
+ this.expireAt = expireAt;
+ }
+
+ public LocalDateTime getPayAt() {
+ return payAt;
+ }
+
+ public void setPayAt(LocalDateTime payAt) {
+ this.payAt = payAt;
+ }
+}
diff --git a/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/flashsale/enums/FlashSaleActivityStatus.java b/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/flashsale/enums/FlashSaleActivityStatus.java
new file mode 100644
index 0000000..0703763
--- /dev/null
+++ b/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/flashsale/enums/FlashSaleActivityStatus.java
@@ -0,0 +1,11 @@
+package cn.novalon.gym.manage.coupon.flashsale.enums;
+
+/**
+ * 秒杀活动状态
+ */
+public enum FlashSaleActivityStatus {
+ DRAFT,
+ ACTIVE,
+ TERMINATED,
+ EXPIRED
+}
diff --git a/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/flashsale/enums/FlashSaleOrderStatus.java b/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/flashsale/enums/FlashSaleOrderStatus.java
new file mode 100644
index 0000000..33a991e
--- /dev/null
+++ b/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/flashsale/enums/FlashSaleOrderStatus.java
@@ -0,0 +1,11 @@
+package cn.novalon.gym.manage.coupon.flashsale.enums;
+
+/**
+ * 秒杀订单状态
+ */
+public enum FlashSaleOrderStatus {
+ PENDING,
+ PAID,
+ CANCELLED,
+ EXPIRED
+}
diff --git a/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/flashsale/handler/FlashSaleHandler.java b/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/flashsale/handler/FlashSaleHandler.java
new file mode 100644
index 0000000..5c35bd5
--- /dev/null
+++ b/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/flashsale/handler/FlashSaleHandler.java
@@ -0,0 +1,219 @@
+package cn.novalon.gym.manage.coupon.flashsale.handler;
+
+import cn.novalon.gym.manage.common.dto.PageRequest;
+import cn.novalon.gym.manage.coupon.flashsale.domain.FlashSaleActivity;
+import cn.novalon.gym.manage.coupon.flashsale.domain.FlashSaleItem;
+import cn.novalon.gym.manage.coupon.flashsale.domain.FlashSaleOrder;
+import cn.novalon.gym.manage.coupon.flashsale.domain.GrabRequest;
+import cn.novalon.gym.manage.coupon.flashsale.service.IFlashSaleActivityService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+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.util.HashMap;
+import java.util.Map;
+
+@Component
+@Tag(name = "秒杀管理", description = "秒杀相关操作")
+public class FlashSaleHandler {
+
+ private final IFlashSaleActivityService flashSaleService;
+
+ public FlashSaleHandler(IFlashSaleActivityService flashSaleService) {
+ this.flashSaleService = flashSaleService;
+ }
+
+ @Operation(summary = "获取所有秒杀活动")
+ public Mono getAllActivities(ServerRequest request) {
+ boolean includeDeleted = Boolean.parseBoolean(request.queryParam("includeDeleted").orElse("false"));
+ return ServerResponse.ok()
+ .body(flashSaleService.findAll(includeDeleted), FlashSaleActivity.class);
+ }
+
+ @Operation(summary = "分页获取秒杀活动")
+ public Mono getActivitiesByPage(ServerRequest request) {
+ return request.bodyToMono(PageRequest.class)
+ .flatMap(pageRequest -> {
+ String status = request.queryParam("status").orElse(null);
+ normalizePageRequest(pageRequest);
+ return flashSaleService.findByPage(pageRequest, status)
+ .flatMap(response -> ServerResponse.ok().bodyValue(response));
+ });
+ }
+
+ @Operation(summary = "搜索秒杀活动")
+ public Mono searchActivities(ServerRequest request) {
+ String keyword = request.queryParam("keyword").orElse("");
+ String status = request.queryParam("status").orElse(null);
+ return ServerResponse.ok()
+ .body(flashSaleService.findByKeywordAndStatus(keyword, status), FlashSaleActivity.class);
+ }
+
+ @Operation(summary = "根据ID获取秒杀活动")
+ public Mono getActivityById(ServerRequest request) {
+ Long id = Long.valueOf(request.pathVariable("id"));
+ return flashSaleService.findById(id)
+ .flatMap(activity -> ServerResponse.ok().bodyValue(activity))
+ .switchIfEmpty(ServerResponse.notFound().build());
+ }
+
+ @Operation(summary = "创建秒杀活动")
+ public Mono createActivity(ServerRequest request) {
+ return request.bodyToMono(FlashSaleActivity.class)
+ .flatMap(activity -> {
+ if (activity.getName() == null || activity.getName().isEmpty()) {
+ return badRequest("活动名称不能为空");
+ }
+ return flashSaleService.create(activity)
+ .flatMap(created -> successResponse("秒杀活动创建成功", created))
+ .onErrorResume(this::errorResponse);
+ });
+ }
+
+ @Operation(summary = "更新秒杀活动")
+ public Mono updateActivity(ServerRequest request) {
+ Long id = Long.valueOf(request.pathVariable("id"));
+ return request.bodyToMono(FlashSaleActivity.class)
+ .flatMap(activity -> flashSaleService.update(id, activity)
+ .flatMap(updated -> successResponse("秒杀活动更新成功", updated))
+ .onErrorResume(this::errorResponse));
+ }
+
+ @Operation(summary = "删除秒杀活动")
+ public Mono deleteActivity(ServerRequest request) {
+ Long id = Long.valueOf(request.pathVariable("id"));
+ return flashSaleService.delete(id)
+ .then(Mono.defer(() -> successResponse("秒杀活动删除成功", null)))
+ .onErrorResume(this::errorResponse);
+ }
+
+ @Operation(summary = "发布秒杀活动")
+ public Mono publishActivity(ServerRequest request) {
+ Long id = Long.valueOf(request.pathVariable("id"));
+ return flashSaleService.publish(id)
+ .flatMap(activity -> successResponse("秒杀活动发布成功", activity))
+ .onErrorResume(this::errorResponse);
+ }
+
+ @Operation(summary = "终止秒杀活动")
+ public Mono terminateActivity(ServerRequest request) {
+ Long id = Long.valueOf(request.pathVariable("id"));
+ return flashSaleService.terminate(id)
+ .flatMap(activity -> successResponse("秒杀活动已终止", activity))
+ .onErrorResume(this::errorResponse);
+ }
+
+ @Operation(summary = "获取秒杀统计")
+ public Mono getStatistics(ServerRequest request) {
+ Long id = Long.valueOf(request.pathVariable("id"));
+ return flashSaleService.getStatistics(id)
+ .flatMap(stats -> ServerResponse.ok().bodyValue(stats))
+ .onErrorResume(this::errorResponse);
+ }
+
+ @Operation(summary = "获取秒杀商品列表")
+ public Mono getItems(ServerRequest request) {
+ String activityIdStr = request.queryParam("activityId").orElse(null);
+ if (activityIdStr == null) {
+ return badRequest("activityId不能为空");
+ }
+ Long activityId = Long.valueOf(activityIdStr);
+ return ServerResponse.ok()
+ .body(flashSaleService.findItemsByActivityId(activityId), FlashSaleItem.class);
+ }
+
+ @Operation(summary = "创建秒杀商品")
+ public Mono createItem(ServerRequest request) {
+ return request.bodyToMono(FlashSaleItem.class)
+ .flatMap(item -> flashSaleService.createItem(item)
+ .flatMap(created -> successResponse("秒杀商品创建成功", created))
+ .onErrorResume(this::errorResponse));
+ }
+
+ @Operation(summary = "更新秒杀商品")
+ public Mono updateItem(ServerRequest request) {
+ Long id = Long.valueOf(request.pathVariable("id"));
+ return request.bodyToMono(FlashSaleItem.class)
+ .flatMap(item -> flashSaleService.updateItem(id, item)
+ .flatMap(updated -> successResponse("秒杀商品更新成功", updated))
+ .onErrorResume(this::errorResponse));
+ }
+
+ @Operation(summary = "秒杀抢购")
+ public Mono grab(ServerRequest request) {
+ return request.bodyToMono(GrabRequest.class)
+ .flatMap(body -> {
+ if (body.getItemId() == null || body.getMemberId() == null) {
+ return badRequest("itemId和memberId不能为空");
+ }
+ return flashSaleService.grab(body)
+ .flatMap(order -> successResponse("抢购成功,请尽快支付", order))
+ .onErrorResume(this::errorResponse);
+ });
+ }
+
+ @Operation(summary = "支付秒杀订单")
+ public Mono payOrder(ServerRequest request) {
+ Long orderId = Long.valueOf(request.pathVariable("orderId"));
+ return flashSaleService.payOrder(orderId)
+ .flatMap(order -> successResponse("支付成功", order))
+ .onErrorResume(this::errorResponse);
+ }
+
+ @Operation(summary = "取消秒杀订单")
+ public Mono cancelOrder(ServerRequest request) {
+ Long orderId = Long.valueOf(request.pathVariable("orderId"));
+ return flashSaleService.cancelOrder(orderId)
+ .flatMap(order -> successResponse("订单已取消", order))
+ .onErrorResume(this::errorResponse);
+ }
+
+ @Operation(summary = "获取会员秒杀订单")
+ public Mono getOrdersByMember(ServerRequest request) {
+ Long memberId = Long.valueOf(request.pathVariable("memberId"));
+ return ServerResponse.ok()
+ .body(flashSaleService.findOrdersByMemberId(memberId), FlashSaleOrder.class);
+ }
+
+ private void normalizePageRequest(PageRequest pageRequest) {
+ if (pageRequest.getPage() < 0) {
+ pageRequest.setPage(0);
+ }
+ if (pageRequest.getSize() <= 0 || pageRequest.getSize() > 100) {
+ pageRequest.setSize(10);
+ }
+ if (pageRequest.getSort() == null || pageRequest.getSort().isEmpty()) {
+ pageRequest.setSort("id");
+ }
+ if (pageRequest.getOrder() == null || pageRequest.getOrder().isEmpty()) {
+ pageRequest.setOrder("desc");
+ }
+ }
+
+ private Mono successResponse(String message, Object data) {
+ Map response = new HashMap<>();
+ response.put("success", true);
+ response.put("message", message);
+ if (data != null) {
+ response.put("data", data);
+ }
+ return ServerResponse.ok().bodyValue(response);
+ }
+
+ private Mono badRequest(String message) {
+ Map error = new HashMap<>();
+ error.put("success", false);
+ error.put("message", message);
+ return ServerResponse.badRequest().bodyValue(error);
+ }
+
+ private Mono errorResponse(Throwable error) {
+ Map response = new HashMap<>();
+ response.put("success", false);
+ response.put("message", error.getMessage());
+ return ServerResponse.badRequest().bodyValue(response);
+ }
+}
diff --git a/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/flashsale/repository/IFlashSaleActivityRepository.java b/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/flashsale/repository/IFlashSaleActivityRepository.java
new file mode 100644
index 0000000..c66cbea
--- /dev/null
+++ b/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/flashsale/repository/IFlashSaleActivityRepository.java
@@ -0,0 +1,26 @@
+package cn.novalon.gym.manage.coupon.flashsale.repository;
+
+import cn.novalon.gym.manage.coupon.flashsale.domain.FlashSaleActivity;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+public interface IFlashSaleActivityRepository {
+
+ Mono findById(Long id);
+
+ Flux findAll(boolean includeDeleted);
+
+ Flux findByKeyword(String keyword);
+
+ Flux findByStatus(String status);
+
+ Flux findByKeywordAndStatus(String keyword, String status);
+
+ Mono save(FlashSaleActivity activity);
+
+ Mono update(FlashSaleActivity activity);
+
+ Mono deleteById(Long id);
+
+ Mono updateStatus(Long id, String status);
+}
diff --git a/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/flashsale/repository/IFlashSaleItemRepository.java b/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/flashsale/repository/IFlashSaleItemRepository.java
new file mode 100644
index 0000000..6afa37b
--- /dev/null
+++ b/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/flashsale/repository/IFlashSaleItemRepository.java
@@ -0,0 +1,22 @@
+package cn.novalon.gym.manage.coupon.flashsale.repository;
+
+import cn.novalon.gym.manage.coupon.flashsale.domain.FlashSaleItem;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+public interface IFlashSaleItemRepository {
+
+ Mono findById(Long id);
+
+ Flux findByActivityId(Long activityId);
+
+ Mono save(FlashSaleItem item);
+
+ Mono update(FlashSaleItem item);
+
+ Mono deductStock(Long id, int quantity);
+
+ Mono restoreStock(Long id, int quantity);
+
+ Mono incrementSoldCount(Long id, int count);
+}
diff --git a/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/flashsale/repository/IFlashSaleOrderRepository.java b/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/flashsale/repository/IFlashSaleOrderRepository.java
new file mode 100644
index 0000000..0bae51c
--- /dev/null
+++ b/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/flashsale/repository/IFlashSaleOrderRepository.java
@@ -0,0 +1,26 @@
+package cn.novalon.gym.manage.coupon.flashsale.repository;
+
+import cn.novalon.gym.manage.coupon.flashsale.domain.FlashSaleOrder;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+import java.util.Collection;
+
+public interface IFlashSaleOrderRepository {
+
+ Mono findById(Long id);
+
+ Flux findByMemberId(Long memberId);
+
+ Flux findByActivityId(Long activityId);
+
+ Flux findExpiredPendingOrders();
+
+ Mono countByItemIdAndMemberIdAndStatusIn(Long itemId, Long memberId, Collection statuses);
+
+ Mono save(FlashSaleOrder order);
+
+ Mono updateStatus(Long id, String status);
+
+ Mono markPaid(Long id, String status);
+}
diff --git a/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/flashsale/repository/impl/FlashSaleActivityRepository.java b/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/flashsale/repository/impl/FlashSaleActivityRepository.java
new file mode 100644
index 0000000..1dc999a
--- /dev/null
+++ b/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/flashsale/repository/impl/FlashSaleActivityRepository.java
@@ -0,0 +1,109 @@
+package cn.novalon.gym.manage.coupon.flashsale.repository.impl;
+
+import cn.novalon.gym.manage.coupon.flashsale.converter.FlashSaleConverter;
+import cn.novalon.gym.manage.coupon.flashsale.dao.FlashSaleActivityDao;
+import cn.novalon.gym.manage.coupon.flashsale.domain.FlashSaleActivity;
+import cn.novalon.gym.manage.coupon.flashsale.entity.FlashSaleActivityEntity;
+import cn.novalon.gym.manage.coupon.flashsale.repository.IFlashSaleActivityRepository;
+import org.springframework.stereotype.Repository;
+import org.springframework.transaction.annotation.Transactional;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+import java.time.LocalDateTime;
+
+@Repository
+@Transactional
+public class FlashSaleActivityRepository implements IFlashSaleActivityRepository {
+
+ private final FlashSaleActivityDao activityDao;
+ private final FlashSaleConverter converter;
+
+ public FlashSaleActivityRepository(FlashSaleActivityDao activityDao, FlashSaleConverter converter) {
+ this.activityDao = activityDao;
+ this.converter = converter;
+ }
+
+ @Override
+ public Mono findById(Long id) {
+ return activityDao.findByIdIsAndDeletedAtIsNull(id).map(converter::toActivity);
+ }
+
+ @Override
+ public Flux findAll(boolean includeDeleted) {
+ if (includeDeleted) {
+ return activityDao.findAll().map(converter::toActivity);
+ }
+ return activityDao.findAllByDeletedAtIsNull().map(converter::toActivity);
+ }
+
+ @Override
+ public Flux findByKeyword(String keyword) {
+ if (keyword == null || keyword.isEmpty()) {
+ return findAll(false);
+ }
+ return activityDao.findByNameContainingAndDeletedAtIsNull(keyword).map(converter::toActivity);
+ }
+
+ @Override
+ public Flux findByStatus(String status) {
+ if (status == null || status.isEmpty()) {
+ return findAll(false);
+ }
+ return activityDao.findByStatusAndDeletedAtIsNull(status).map(converter::toActivity);
+ }
+
+ @Override
+ public Flux findByKeywordAndStatus(String keyword, String status) {
+ Flux result = findByKeyword(keyword);
+ if (status != null && !status.isEmpty()) {
+ result = result.filter(a -> status.equals(a.getStatus()));
+ }
+ return result;
+ }
+
+ @Override
+ public Mono save(FlashSaleActivity activity) {
+ return activityDao.save(converter.toActivityEntity(activity)).map(converter::toActivity);
+ }
+
+ @Override
+ public Mono update(FlashSaleActivity activity) {
+ return activityDao.findByIdIsAndDeletedAtIsNull(activity.getId())
+ .switchIfEmpty(Mono.error(new RuntimeException("秒杀活动不存在")))
+ .flatMap(existing -> {
+ existing.markNotNew();
+ if (activity.getName() != null) {
+ existing.setName(activity.getName());
+ }
+ if (activity.getDescription() != null) {
+ existing.setDescription(activity.getDescription());
+ }
+ if (activity.getStartTime() != null) {
+ existing.setStartTime(activity.getStartTime());
+ }
+ if (activity.getEndTime() != null) {
+ existing.setEndTime(activity.getEndTime());
+ }
+ if (activity.getPayTimeoutMinutes() != null) {
+ existing.setPayTimeoutMinutes(activity.getPayTimeoutMinutes());
+ }
+ if (activity.getPerUserLimit() != null) {
+ existing.setPerUserLimit(activity.getPerUserLimit());
+ }
+ existing.setUpdatedAt(LocalDateTime.now());
+ return activityDao.save(existing);
+ })
+ .map(converter::toActivity);
+ }
+
+ @Override
+ public Mono deleteById(Long id) {
+ return activityDao.softDelete(id, LocalDateTime.now()).then();
+ }
+
+ @Override
+ public Mono updateStatus(Long id, String status) {
+ return activityDao.updateStatus(id, status, LocalDateTime.now()).then();
+ }
+}
diff --git a/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/flashsale/repository/impl/FlashSaleItemRepository.java b/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/flashsale/repository/impl/FlashSaleItemRepository.java
new file mode 100644
index 0000000..64d8db1
--- /dev/null
+++ b/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/flashsale/repository/impl/FlashSaleItemRepository.java
@@ -0,0 +1,90 @@
+package cn.novalon.gym.manage.coupon.flashsale.repository.impl;
+
+import cn.novalon.gym.manage.coupon.flashsale.converter.FlashSaleConverter;
+import cn.novalon.gym.manage.coupon.flashsale.dao.FlashSaleItemDao;
+import cn.novalon.gym.manage.coupon.flashsale.domain.FlashSaleItem;
+import cn.novalon.gym.manage.coupon.flashsale.entity.FlashSaleItemEntity;
+import cn.novalon.gym.manage.coupon.flashsale.repository.IFlashSaleItemRepository;
+import org.springframework.stereotype.Repository;
+import org.springframework.transaction.annotation.Transactional;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+import java.time.LocalDateTime;
+
+@Repository
+@Transactional
+public class FlashSaleItemRepository implements IFlashSaleItemRepository {
+
+ private final FlashSaleItemDao itemDao;
+ private final FlashSaleConverter converter;
+
+ public FlashSaleItemRepository(FlashSaleItemDao itemDao, FlashSaleConverter converter) {
+ this.itemDao = itemDao;
+ this.converter = converter;
+ }
+
+ @Override
+ public Mono findById(Long id) {
+ return itemDao.findByIdIsAndDeletedAtIsNull(id).map(converter::toItem);
+ }
+
+ @Override
+ public Flux findByActivityId(Long activityId) {
+ return itemDao.findByActivityIdAndDeletedAtIsNull(activityId).map(converter::toItem);
+ }
+
+ @Override
+ public Mono save(FlashSaleItem item) {
+ return itemDao.save(converter.toItemEntity(item)).map(converter::toItem);
+ }
+
+ @Override
+ public Mono update(FlashSaleItem item) {
+ return itemDao.findByIdIsAndDeletedAtIsNull(item.getId())
+ .switchIfEmpty(Mono.error(new RuntimeException("秒杀商品不存在")))
+ .flatMap(existing -> {
+ existing.markNotNew();
+ if (item.getProductType() != null) {
+ existing.setProductType(item.getProductType());
+ }
+ if (item.getProductId() != null) {
+ existing.setProductId(item.getProductId());
+ }
+ if (item.getProductName() != null) {
+ existing.setProductName(item.getProductName());
+ }
+ if (item.getOriginalPrice() != null) {
+ existing.setOriginalPrice(item.getOriginalPrice());
+ }
+ if (item.getSeckillPrice() != null) {
+ existing.setSeckillPrice(item.getSeckillPrice());
+ }
+ if (item.getStock() != null) {
+ existing.setStock(item.getStock());
+ }
+ if (item.getPerUserLimit() != null) {
+ existing.setPerUserLimit(item.getPerUserLimit());
+ }
+ existing.setUpdatedAt(LocalDateTime.now());
+ return itemDao.save(existing);
+ })
+ .map(converter::toItem);
+ }
+
+ @Override
+ public Mono deductStock(Long id, int quantity) {
+ return itemDao.deductStock(id, quantity, LocalDateTime.now())
+ .map(rows -> rows != null && rows > 0);
+ }
+
+ @Override
+ public Mono restoreStock(Long id, int quantity) {
+ return itemDao.restoreStock(id, quantity, LocalDateTime.now()).then();
+ }
+
+ @Override
+ public Mono incrementSoldCount(Long id, int count) {
+ return itemDao.incrementSoldCount(id, count, LocalDateTime.now()).then();
+ }
+}
diff --git a/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/flashsale/repository/impl/FlashSaleOrderRepository.java b/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/flashsale/repository/impl/FlashSaleOrderRepository.java
new file mode 100644
index 0000000..842c679
--- /dev/null
+++ b/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/flashsale/repository/impl/FlashSaleOrderRepository.java
@@ -0,0 +1,68 @@
+package cn.novalon.gym.manage.coupon.flashsale.repository.impl;
+
+import cn.novalon.gym.manage.coupon.flashsale.converter.FlashSaleConverter;
+import cn.novalon.gym.manage.coupon.flashsale.dao.FlashSaleOrderDao;
+import cn.novalon.gym.manage.coupon.flashsale.domain.FlashSaleOrder;
+import cn.novalon.gym.manage.coupon.flashsale.enums.FlashSaleOrderStatus;
+import cn.novalon.gym.manage.coupon.flashsale.repository.IFlashSaleOrderRepository;
+import org.springframework.stereotype.Repository;
+import org.springframework.transaction.annotation.Transactional;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+import java.time.LocalDateTime;
+import java.util.Collection;
+
+@Repository
+@Transactional
+public class FlashSaleOrderRepository implements IFlashSaleOrderRepository {
+
+ private final FlashSaleOrderDao orderDao;
+ private final FlashSaleConverter converter;
+
+ public FlashSaleOrderRepository(FlashSaleOrderDao orderDao, FlashSaleConverter converter) {
+ this.orderDao = orderDao;
+ this.converter = converter;
+ }
+
+ @Override
+ public Mono findById(Long id) {
+ return orderDao.findByIdIsAndDeletedAtIsNull(id).map(converter::toOrder);
+ }
+
+ @Override
+ public Flux findByMemberId(Long memberId) {
+ return orderDao.findByMemberIdAndDeletedAtIsNull(memberId).map(converter::toOrder);
+ }
+
+ @Override
+ public Flux findByActivityId(Long activityId) {
+ return orderDao.findByActivityIdAndDeletedAtIsNull(activityId).map(converter::toOrder);
+ }
+
+ @Override
+ public Flux findExpiredPendingOrders() {
+ return orderDao.findExpiredPendingOrders(
+ FlashSaleOrderStatus.PENDING.name(), LocalDateTime.now()).map(converter::toOrder);
+ }
+
+ @Override
+ public Mono countByItemIdAndMemberIdAndStatusIn(Long itemId, Long memberId, Collection statuses) {
+ return orderDao.countByItemIdAndMemberIdAndStatusInAndDeletedAtIsNull(itemId, memberId, statuses);
+ }
+
+ @Override
+ public Mono save(FlashSaleOrder order) {
+ return orderDao.save(converter.toOrderEntity(order)).map(converter::toOrder);
+ }
+
+ @Override
+ public Mono updateStatus(Long id, String status) {
+ return orderDao.updateStatus(id, status, LocalDateTime.now()).then();
+ }
+
+ @Override
+ public Mono markPaid(Long id, String status) {
+ return orderDao.markPaid(id, status, LocalDateTime.now(), LocalDateTime.now()).then();
+ }
+}
diff --git a/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/flashsale/scheduler/FlashSaleOrderExpireScheduler.java b/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/flashsale/scheduler/FlashSaleOrderExpireScheduler.java
new file mode 100644
index 0000000..ce581c1
--- /dev/null
+++ b/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/flashsale/scheduler/FlashSaleOrderExpireScheduler.java
@@ -0,0 +1,35 @@
+package cn.novalon.gym.manage.coupon.flashsale.scheduler;
+
+import cn.novalon.gym.manage.coupon.flashsale.service.IFlashSaleActivityService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Component;
+
+/**
+ * 秒杀订单过期定时任务
+ *
+ * 功能:定期检查已过期的待支付订单,取消订单并恢复库存
+ */
+@Component
+public class FlashSaleOrderExpireScheduler {
+
+ private static final Logger logger = LoggerFactory.getLogger(FlashSaleOrderExpireScheduler.class);
+
+ private final IFlashSaleActivityService flashSaleService;
+
+ public FlashSaleOrderExpireScheduler(IFlashSaleActivityService flashSaleService) {
+ this.flashSaleService = flashSaleService;
+ }
+
+ @Scheduled(fixedRate = 60000)
+ public void processExpiredOrders() {
+ logger.debug("定时任务开始检查过期秒杀订单");
+
+ flashSaleService.processExpiredOrders()
+ .subscribe(
+ count -> logger.debug("定时任务完成,处理了 {} 个过期秒杀订单", count),
+ error -> logger.error("秒杀订单过期定时任务执行失败:{}", error.getMessage(), error)
+ );
+ }
+}
diff --git a/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/flashsale/service/IFlashSaleActivityService.java b/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/flashsale/service/IFlashSaleActivityService.java
new file mode 100644
index 0000000..d008e34
--- /dev/null
+++ b/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/flashsale/service/IFlashSaleActivityService.java
@@ -0,0 +1,50 @@
+package cn.novalon.gym.manage.coupon.flashsale.service;
+
+import cn.novalon.gym.manage.common.dto.PageRequest;
+import cn.novalon.gym.manage.common.dto.PageResponse;
+import cn.novalon.gym.manage.coupon.flashsale.domain.FlashSaleActivity;
+import cn.novalon.gym.manage.coupon.flashsale.domain.FlashSaleItem;
+import cn.novalon.gym.manage.coupon.flashsale.domain.FlashSaleOrder;
+import cn.novalon.gym.manage.coupon.flashsale.domain.FlashSaleStatistics;
+import cn.novalon.gym.manage.coupon.flashsale.domain.GrabRequest;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+public interface IFlashSaleActivityService {
+
+ Mono findById(Long id);
+
+ Flux findAll(boolean includeDeleted);
+
+ Flux findByKeywordAndStatus(String keyword, String status);
+
+ Mono> findByPage(PageRequest pageRequest, String status);
+
+ Mono create(FlashSaleActivity activity);
+
+ Mono update(Long id, FlashSaleActivity activity);
+
+ Mono delete(Long id);
+
+ Mono publish(Long id);
+
+ Mono terminate(Long id);
+
+ Flux findItemsByActivityId(Long activityId);
+
+ Mono createItem(FlashSaleItem item);
+
+ Mono updateItem(Long id, FlashSaleItem item);
+
+ Mono grab(GrabRequest request);
+
+ Mono payOrder(Long orderId);
+
+ Mono cancelOrder(Long orderId);
+
+ Flux findOrdersByMemberId(Long memberId);
+
+ Mono getStatistics(Long id);
+
+ Mono processExpiredOrders();
+}
diff --git a/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/flashsale/service/impl/FlashSaleActivityService.java b/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/flashsale/service/impl/FlashSaleActivityService.java
new file mode 100644
index 0000000..05d1618
--- /dev/null
+++ b/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/flashsale/service/impl/FlashSaleActivityService.java
@@ -0,0 +1,390 @@
+package cn.novalon.gym.manage.coupon.flashsale.service.impl;
+
+import cn.novalon.gym.manage.common.dto.PageRequest;
+import cn.novalon.gym.manage.common.dto.PageResponse;
+import cn.novalon.gym.manage.common.util.SnowflakeId;
+import cn.novalon.gym.manage.coupon.flashsale.domain.FlashSaleActivity;
+import cn.novalon.gym.manage.coupon.flashsale.domain.FlashSaleItem;
+import cn.novalon.gym.manage.coupon.flashsale.domain.FlashSaleOrder;
+import cn.novalon.gym.manage.coupon.flashsale.domain.FlashSaleStatistics;
+import cn.novalon.gym.manage.coupon.flashsale.domain.GrabRequest;
+import cn.novalon.gym.manage.coupon.flashsale.enums.FlashSaleActivityStatus;
+import cn.novalon.gym.manage.coupon.flashsale.enums.FlashSaleOrderStatus;
+import cn.novalon.gym.manage.coupon.flashsale.repository.IFlashSaleActivityRepository;
+import cn.novalon.gym.manage.coupon.flashsale.repository.IFlashSaleItemRepository;
+import cn.novalon.gym.manage.coupon.flashsale.repository.IFlashSaleOrderRepository;
+import cn.novalon.gym.manage.coupon.flashsale.service.IFlashSaleActivityService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Service;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.List;
+
+@Service
+public class FlashSaleActivityService implements IFlashSaleActivityService {
+
+ private static final Logger logger = LoggerFactory.getLogger(FlashSaleActivityService.class);
+
+ private final IFlashSaleActivityRepository activityRepository;
+ private final IFlashSaleItemRepository itemRepository;
+ private final IFlashSaleOrderRepository orderRepository;
+
+ public FlashSaleActivityService(IFlashSaleActivityRepository activityRepository,
+ IFlashSaleItemRepository itemRepository,
+ IFlashSaleOrderRepository orderRepository) {
+ this.activityRepository = activityRepository;
+ this.itemRepository = itemRepository;
+ this.orderRepository = orderRepository;
+ }
+
+ @Override
+ public Mono findById(Long id) {
+ return activityRepository.findById(id);
+ }
+
+ @Override
+ public Flux findAll(boolean includeDeleted) {
+ return activityRepository.findAll(includeDeleted);
+ }
+
+ @Override
+ public Flux findByKeywordAndStatus(String keyword, String status) {
+ return activityRepository.findByKeywordAndStatus(keyword, status);
+ }
+
+ @Override
+ public Mono> findByPage(PageRequest pageRequest, String status) {
+ int page = Math.max(pageRequest.getPage(), 0);
+ int size = pageRequest.getSize() <= 0 || pageRequest.getSize() > 100 ? 10 : pageRequest.getSize();
+ String keyword = pageRequest.getKeyword();
+
+ return activityRepository.findByKeywordAndStatus(keyword, status)
+ .sort(Comparator.comparing(FlashSaleActivity::getCreatedAt,
+ Comparator.nullsLast(Comparator.reverseOrder())))
+ .collectList()
+ .map(list -> {
+ long total = list.size();
+ int fromIndex = Math.min(page * size, list.size());
+ int toIndex = Math.min(fromIndex + size, list.size());
+ List content = list.subList(fromIndex, toIndex);
+ int totalPages = size == 0 ? 0 : (int) Math.ceil((double) total / size);
+ return new PageResponse<>(content, totalPages, total, page, size);
+ });
+ }
+
+ @Override
+ public Mono create(FlashSaleActivity activity) {
+ return validateActivity(activity)
+ .flatMap(validated -> {
+ validated.generateId();
+ validated.setStatus(FlashSaleActivityStatus.DRAFT.name());
+ applyDefaults(validated);
+ return activityRepository.save(validated);
+ })
+ .doOnSuccess(a -> logger.info("秒杀活动创建成功 - id={}, name={}", a.getId(), a.getName()));
+ }
+
+ @Override
+ public Mono update(Long id, FlashSaleActivity activity) {
+ return activityRepository.findById(id)
+ .switchIfEmpty(Mono.error(new RuntimeException("秒杀活动不存在")))
+ .flatMap(existing -> {
+ if (!FlashSaleActivityStatus.DRAFT.name().equals(existing.getStatus())) {
+ return Mono.error(new RuntimeException("仅草稿状态的秒杀活动可编辑"));
+ }
+ activity.setId(id);
+ return validateActivity(activity)
+ .flatMap(activityRepository::update);
+ })
+ .doOnSuccess(a -> logger.info("秒杀活动更新成功 - id={}", id));
+ }
+
+ @Override
+ public Mono delete(Long id) {
+ return activityRepository.findById(id)
+ .switchIfEmpty(Mono.error(new RuntimeException("秒杀活动不存在")))
+ .flatMap(existing -> {
+ if (!FlashSaleActivityStatus.DRAFT.name().equals(existing.getStatus())) {
+ return Mono.error(new RuntimeException("仅草稿状态的秒杀活动可删除"));
+ }
+ return activityRepository.deleteById(id);
+ });
+ }
+
+ @Override
+ public Mono publish(Long id) {
+ return activityRepository.findById(id)
+ .switchIfEmpty(Mono.error(new RuntimeException("秒杀活动不存在")))
+ .flatMap(existing -> {
+ if (!FlashSaleActivityStatus.DRAFT.name().equals(existing.getStatus())) {
+ return Mono.error(new RuntimeException("仅草稿状态的秒杀活动可发布"));
+ }
+ return validateActivity(existing)
+ .flatMap(validated -> activityRepository.updateStatus(id, FlashSaleActivityStatus.ACTIVE.name())
+ .then(activityRepository.findById(id)));
+ })
+ .doOnSuccess(a -> logger.info("秒杀活动发布成功 - id={}", id));
+ }
+
+ @Override
+ public Mono terminate(Long id) {
+ return activityRepository.findById(id)
+ .switchIfEmpty(Mono.error(new RuntimeException("秒杀活动不存在")))
+ .flatMap(existing -> {
+ if (!FlashSaleActivityStatus.ACTIVE.name().equals(existing.getStatus())) {
+ return Mono.error(new RuntimeException("仅进行中的秒杀活动可终止"));
+ }
+ return activityRepository.updateStatus(id, FlashSaleActivityStatus.TERMINATED.name())
+ .then(activityRepository.findById(id));
+ })
+ .doOnSuccess(a -> logger.info("秒杀活动已终止 - id={}", id));
+ }
+
+ @Override
+ public Flux findItemsByActivityId(Long activityId) {
+ return itemRepository.findByActivityId(activityId);
+ }
+
+ @Override
+ public Mono createItem(FlashSaleItem item) {
+ return validateItem(item)
+ .flatMap(validated -> activityRepository.findById(validated.getActivityId())
+ .switchIfEmpty(Mono.error(new RuntimeException("秒杀活动不存在")))
+ .flatMap(activity -> {
+ if (!FlashSaleActivityStatus.DRAFT.name().equals(activity.getStatus())) {
+ return Mono.error(new RuntimeException("仅草稿状态的秒杀活动可添加商品"));
+ }
+ validated.setId(SnowflakeId.nextId());
+ validated.setSoldCount(0);
+ if (validated.getPerUserLimit() == null) {
+ validated.setPerUserLimit(activity.getPerUserLimit() != null ? activity.getPerUserLimit() : 1);
+ }
+ return itemRepository.save(validated);
+ }));
+ }
+
+ @Override
+ public Mono updateItem(Long id, FlashSaleItem item) {
+ return itemRepository.findById(id)
+ .switchIfEmpty(Mono.error(new RuntimeException("秒杀商品不存在")))
+ .flatMap(existing -> activityRepository.findById(existing.getActivityId())
+ .flatMap(activity -> {
+ if (!FlashSaleActivityStatus.DRAFT.name().equals(activity.getStatus())) {
+ return Mono.error(new RuntimeException("活动已发布,不可修改商品"));
+ }
+ item.setId(id);
+ return validateItem(item)
+ .flatMap(itemRepository::update);
+ }));
+ }
+
+ @Override
+ public Mono grab(GrabRequest request) {
+ if (request.getItemId() == null || request.getMemberId() == null) {
+ return Mono.error(new RuntimeException("itemId和memberId不能为空"));
+ }
+ int quantity = request.getQuantity() != null && request.getQuantity() > 0 ? request.getQuantity() : 1;
+
+ return itemRepository.findById(request.getItemId())
+ .switchIfEmpty(Mono.error(new RuntimeException("秒杀商品不存在")))
+ .flatMap(item -> activityRepository.findById(item.getActivityId())
+ .switchIfEmpty(Mono.error(new RuntimeException("秒杀活动不存在")))
+ .flatMap(activity -> validateActivityForGrab(activity)
+ .then(checkUserLimit(item, activity, request.getMemberId(), quantity))
+ .then(itemRepository.deductStock(item.getId(), quantity))
+ .flatMap(success -> {
+ if (!success) {
+ return Mono.error(new RuntimeException("库存不足"));
+ }
+ return createOrder(item, activity, request.getMemberId(), quantity)
+ .onErrorResume(e -> itemRepository.restoreStock(item.getId(), quantity)
+ .then(Mono.error(e)));
+ })));
+ }
+
+ @Override
+ public Mono payOrder(Long orderId) {
+ return orderRepository.findById(orderId)
+ .switchIfEmpty(Mono.error(new RuntimeException("秒杀订单不存在")))
+ .flatMap(order -> {
+ if (!FlashSaleOrderStatus.PENDING.name().equals(order.getStatus())) {
+ return Mono.error(new RuntimeException("订单状态不允许支付"));
+ }
+ if (order.getExpireAt() != null && order.getExpireAt().isBefore(LocalDateTime.now())) {
+ return Mono.error(new RuntimeException("订单已过期"));
+ }
+ return orderRepository.markPaid(orderId, FlashSaleOrderStatus.PAID.name())
+ .then(itemRepository.incrementSoldCount(order.getItemId(), order.getQuantity()))
+ .then(orderRepository.findById(orderId));
+ })
+ .doOnSuccess(o -> logger.info("秒杀订单支付成功 - orderId={}", orderId));
+ }
+
+ @Override
+ public Mono cancelOrder(Long orderId) {
+ return orderRepository.findById(orderId)
+ .switchIfEmpty(Mono.error(new RuntimeException("秒杀订单不存在")))
+ .flatMap(order -> {
+ if (!FlashSaleOrderStatus.PENDING.name().equals(order.getStatus())) {
+ return Mono.error(new RuntimeException("仅待支付订单可取消"));
+ }
+ return orderRepository.updateStatus(orderId, FlashSaleOrderStatus.CANCELLED.name())
+ .then(itemRepository.restoreStock(order.getItemId(), order.getQuantity()))
+ .then(orderRepository.findById(orderId));
+ })
+ .doOnSuccess(o -> logger.info("秒杀订单已取消 - orderId={}", orderId));
+ }
+
+ @Override
+ public Flux findOrdersByMemberId(Long memberId) {
+ return orderRepository.findByMemberId(memberId);
+ }
+
+ @Override
+ public Mono getStatistics(Long id) {
+ return activityRepository.findById(id)
+ .switchIfEmpty(Mono.error(new RuntimeException("秒杀活动不存在")))
+ .flatMap(activity -> orderRepository.findByActivityId(id)
+ .collectList()
+ .map(orders -> {
+ FlashSaleStatistics stats = new FlashSaleStatistics();
+ stats.setActivityId(id);
+ stats.setTotalOrders(orders.size());
+ stats.setPendingOrders(countByStatus(orders, FlashSaleOrderStatus.PENDING));
+ stats.setPaidOrders(countByStatus(orders, FlashSaleOrderStatus.PAID));
+ stats.setCancelledOrders(countByStatus(orders, FlashSaleOrderStatus.CANCELLED));
+ stats.setExpiredOrders(countByStatus(orders, FlashSaleOrderStatus.EXPIRED));
+
+ long soldQty = orders.stream()
+ .filter(o -> FlashSaleOrderStatus.PAID.name().equals(o.getStatus()))
+ .mapToLong(o -> o.getQuantity() != null ? o.getQuantity() : 0)
+ .sum();
+ stats.setTotalSoldQuantity(soldQty);
+
+ BigDecimal revenue = orders.stream()
+ .filter(o -> FlashSaleOrderStatus.PAID.name().equals(o.getStatus()))
+ .map(o -> o.getPayAmount() != null ? o.getPayAmount() : BigDecimal.ZERO)
+ .reduce(BigDecimal.ZERO, BigDecimal::add);
+ stats.setTotalRevenue(revenue);
+
+ return stats;
+ }));
+ }
+
+ @Override
+ public Mono processExpiredOrders() {
+ return orderRepository.findExpiredPendingOrders()
+ .flatMap(order -> orderRepository.updateStatus(order.getId(), FlashSaleOrderStatus.EXPIRED.name())
+ .then(itemRepository.restoreStock(order.getItemId(), order.getQuantity()))
+ .thenReturn(1L))
+ .reduce(0L, Long::sum)
+ .doOnSuccess(count -> {
+ if (count > 0) {
+ logger.info("已处理 {} 个过期秒杀订单", count);
+ }
+ });
+ }
+
+ private Mono createOrder(FlashSaleItem item, FlashSaleActivity activity,
+ Long memberId, int quantity) {
+ int timeoutMinutes = activity.getPayTimeoutMinutes() != null ? activity.getPayTimeoutMinutes() : 5;
+ BigDecimal payAmount = item.getSeckillPrice().multiply(BigDecimal.valueOf(quantity));
+
+ FlashSaleOrder order = new FlashSaleOrder();
+ order.setId(SnowflakeId.nextId());
+ order.setActivityId(activity.getId());
+ order.setItemId(item.getId());
+ order.setMemberId(memberId);
+ order.setQuantity(quantity);
+ order.setPayAmount(payAmount);
+ order.setStatus(FlashSaleOrderStatus.PENDING.name());
+ order.setExpireAt(LocalDateTime.now().plusMinutes(timeoutMinutes));
+
+ return orderRepository.save(order);
+ }
+
+ private Mono checkUserLimit(FlashSaleItem item, FlashSaleActivity activity,
+ Long memberId, int quantity) {
+ int itemLimit = item.getPerUserLimit() != null ? item.getPerUserLimit() : 1;
+ int activityLimit = activity.getPerUserLimit() != null ? activity.getPerUserLimit() : 1;
+ int effectiveLimit = Math.min(itemLimit, activityLimit);
+
+ return orderRepository.countByItemIdAndMemberIdAndStatusIn(
+ item.getId(), memberId,
+ Arrays.asList(FlashSaleOrderStatus.PENDING.name(), FlashSaleOrderStatus.PAID.name()))
+ .flatMap(count -> {
+ if (count + quantity > effectiveLimit) {
+ return Mono.error(new RuntimeException("超出每人限购数量"));
+ }
+ return Mono.empty();
+ });
+ }
+
+ private Mono validateActivityForGrab(FlashSaleActivity activity) {
+ if (!FlashSaleActivityStatus.ACTIVE.name().equals(activity.getStatus())) {
+ return Mono.error(new RuntimeException("秒杀活动未开始或已结束"));
+ }
+ LocalDateTime now = LocalDateTime.now();
+ if (activity.getStartTime() != null && now.isBefore(activity.getStartTime())) {
+ return Mono.error(new RuntimeException("秒杀活动尚未开始"));
+ }
+ if (activity.getEndTime() != null && now.isAfter(activity.getEndTime())) {
+ return Mono.error(new RuntimeException("秒杀活动已结束"));
+ }
+ return Mono.empty();
+ }
+
+ private Mono validateActivity(FlashSaleActivity activity) {
+ if (activity.getName() == null || activity.getName().isBlank()) {
+ return Mono.error(new RuntimeException("活动名称不能为空"));
+ }
+ if (activity.getStartTime() == null || activity.getEndTime() == null) {
+ return Mono.error(new RuntimeException("活动开始和结束时间不能为空"));
+ }
+ if (activity.getEndTime().isBefore(activity.getStartTime())) {
+ return Mono.error(new RuntimeException("结束时间不能早于开始时间"));
+ }
+ return Mono.just(activity);
+ }
+
+ private Mono validateItem(FlashSaleItem item) {
+ if (item.getActivityId() == null) {
+ return Mono.error(new RuntimeException("活动ID不能为空"));
+ }
+ if (item.getProductType() == null || item.getProductName() == null) {
+ return Mono.error(new RuntimeException("商品类型和名称不能为空"));
+ }
+ if (item.getProductId() == null) {
+ return Mono.error(new RuntimeException("商品ID不能为空"));
+ }
+ if (item.getOriginalPrice() == null || item.getSeckillPrice() == null) {
+ return Mono.error(new RuntimeException("原价和秒杀价不能为空"));
+ }
+ if (item.getSeckillPrice().compareTo(item.getOriginalPrice()) >= 0) {
+ return Mono.error(new RuntimeException("秒杀价必须低于原价"));
+ }
+ if (item.getStock() == null || item.getStock() < 0) {
+ return Mono.error(new RuntimeException("库存不能为空且不能为负数"));
+ }
+ return Mono.just(item);
+ }
+
+ private void applyDefaults(FlashSaleActivity activity) {
+ if (activity.getPayTimeoutMinutes() == null) {
+ activity.setPayTimeoutMinutes(5);
+ }
+ if (activity.getPerUserLimit() == null) {
+ activity.setPerUserLimit(1);
+ }
+ }
+
+ private long countByStatus(List orders, FlashSaleOrderStatus status) {
+ return orders.stream().filter(o -> status.name().equals(o.getStatus())).count();
+ }
+}
diff --git a/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/groupbuy/converter/GroupBuyConverter.java b/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/groupbuy/converter/GroupBuyConverter.java
new file mode 100644
index 0000000..2abf7b8
--- /dev/null
+++ b/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/groupbuy/converter/GroupBuyConverter.java
@@ -0,0 +1,77 @@
+package cn.novalon.gym.manage.coupon.groupbuy.converter;
+
+import cn.hutool.core.bean.BeanUtil;
+import cn.novalon.gym.manage.coupon.groupbuy.domain.GroupBuyActivity;
+import cn.novalon.gym.manage.coupon.groupbuy.domain.GroupBuyParticipant;
+import cn.novalon.gym.manage.coupon.groupbuy.domain.GroupBuyTeam;
+import cn.novalon.gym.manage.coupon.groupbuy.entity.GroupBuyActivityEntity;
+import cn.novalon.gym.manage.coupon.groupbuy.entity.GroupBuyParticipantEntity;
+import cn.novalon.gym.manage.coupon.groupbuy.entity.GroupBuyTeamEntity;
+import org.springframework.stereotype.Component;
+
+@Component
+public class GroupBuyConverter {
+
+ public GroupBuyActivity toActivity(GroupBuyActivityEntity entity) {
+ if (entity == null) {
+ return null;
+ }
+ GroupBuyActivity domain = new GroupBuyActivity();
+ BeanUtil.copyProperties(entity, domain);
+ return domain;
+ }
+
+ public GroupBuyActivityEntity toActivityEntity(GroupBuyActivity domain) {
+ if (domain == null) {
+ return null;
+ }
+ GroupBuyActivityEntity entity = new GroupBuyActivityEntity();
+ BeanUtil.copyProperties(domain, entity);
+ if (domain.getId() != null) {
+ entity.markNotNew();
+ }
+ return entity;
+ }
+
+ public GroupBuyTeam toTeam(GroupBuyTeamEntity entity) {
+ if (entity == null) {
+ return null;
+ }
+ GroupBuyTeam domain = new GroupBuyTeam();
+ BeanUtil.copyProperties(entity, domain);
+ return domain;
+ }
+
+ public GroupBuyTeamEntity toTeamEntity(GroupBuyTeam domain) {
+ if (domain == null) {
+ return null;
+ }
+ GroupBuyTeamEntity entity = new GroupBuyTeamEntity();
+ BeanUtil.copyProperties(domain, entity);
+ if (domain.getId() != null) {
+ entity.markNotNew();
+ }
+ return entity;
+ }
+
+ public GroupBuyParticipant toParticipant(GroupBuyParticipantEntity entity) {
+ if (entity == null) {
+ return null;
+ }
+ GroupBuyParticipant domain = new GroupBuyParticipant();
+ BeanUtil.copyProperties(entity, domain);
+ return domain;
+ }
+
+ public GroupBuyParticipantEntity toParticipantEntity(GroupBuyParticipant domain) {
+ if (domain == null) {
+ return null;
+ }
+ GroupBuyParticipantEntity entity = new GroupBuyParticipantEntity();
+ BeanUtil.copyProperties(domain, entity);
+ if (domain.getId() != null) {
+ entity.markNotNew();
+ }
+ return entity;
+ }
+}
diff --git a/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/groupbuy/dao/GroupBuyActivityDao.java b/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/groupbuy/dao/GroupBuyActivityDao.java
new file mode 100644
index 0000000..1a1bb07
--- /dev/null
+++ b/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/groupbuy/dao/GroupBuyActivityDao.java
@@ -0,0 +1,35 @@
+package cn.novalon.gym.manage.coupon.groupbuy.dao;
+
+import cn.novalon.gym.manage.coupon.groupbuy.entity.GroupBuyActivityEntity;
+import org.springframework.data.r2dbc.repository.Modifying;
+import org.springframework.data.r2dbc.repository.Query;
+import org.springframework.data.r2dbc.repository.R2dbcRepository;
+import org.springframework.stereotype.Repository;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+import java.time.LocalDateTime;
+
+@Repository
+public interface GroupBuyActivityDao extends R2dbcRepository {
+
+ Mono findByIdIsAndDeletedAtIsNull(Long id);
+
+ Flux findAllByDeletedAtIsNull();
+
+ Flux findByNameContainingAndDeletedAtIsNull(String name);
+
+ Flux findByStatusAndDeletedAtIsNull(String status);
+
+ @Modifying
+ @Query("UPDATE group_buy_activity SET deleted_at = :deletedAt WHERE id = :id")
+ Mono softDelete(Long id, LocalDateTime deletedAt);
+
+ @Modifying
+ @Query("UPDATE group_buy_activity SET status = :status, updated_at = :updatedAt WHERE id = :id")
+ Mono updateStatus(Long id, String status, LocalDateTime updatedAt);
+
+ @Modifying
+ @Query("UPDATE group_buy_activity SET sold_count = sold_count + :count, updated_at = :updatedAt WHERE id = :id")
+ Mono incrementSoldCount(Long id, int count, LocalDateTime updatedAt);
+}
diff --git a/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/groupbuy/dao/GroupBuyParticipantDao.java b/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/groupbuy/dao/GroupBuyParticipantDao.java
new file mode 100644
index 0000000..2632c09
--- /dev/null
+++ b/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/groupbuy/dao/GroupBuyParticipantDao.java
@@ -0,0 +1,35 @@
+package cn.novalon.gym.manage.coupon.groupbuy.dao;
+
+import cn.novalon.gym.manage.coupon.groupbuy.entity.GroupBuyParticipantEntity;
+import org.springframework.data.r2dbc.repository.Modifying;
+import org.springframework.data.r2dbc.repository.Query;
+import org.springframework.data.r2dbc.repository.R2dbcRepository;
+import org.springframework.stereotype.Repository;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+import java.time.LocalDateTime;
+
+@Repository
+public interface GroupBuyParticipantDao extends R2dbcRepository {
+
+ Flux findByTeamIdAndDeletedAtIsNull(Long teamId);
+
+ Flux findByActivityIdAndDeletedAtIsNull(Long activityId);
+
+ @Query("""
+ SELECT p.* FROM group_buy_participant p
+ INNER JOIN group_buy_team t ON p.team_id = t.id
+ WHERE p.activity_id = :activityId AND p.member_id = :memberId
+ AND p.status = :participantStatus AND t.status = :teamStatus
+ AND p.deleted_at IS NULL AND t.deleted_at IS NULL
+ """)
+ Flux findActiveParticipantInFormingTeam(
+ Long activityId, Long memberId, String participantStatus, String teamStatus);
+
+ Mono countByActivityIdAndDeletedAtIsNull(Long activityId);
+
+ @Modifying
+ @Query("UPDATE group_buy_participant SET status = :status, updated_at = :updatedAt WHERE team_id = :teamId")
+ Mono updateStatusByTeamId(Long teamId, String status, LocalDateTime updatedAt);
+}
diff --git a/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/groupbuy/dao/GroupBuyTeamDao.java b/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/groupbuy/dao/GroupBuyTeamDao.java
new file mode 100644
index 0000000..044d83f
--- /dev/null
+++ b/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/groupbuy/dao/GroupBuyTeamDao.java
@@ -0,0 +1,38 @@
+package cn.novalon.gym.manage.coupon.groupbuy.dao;
+
+import cn.novalon.gym.manage.coupon.groupbuy.entity.GroupBuyTeamEntity;
+import org.springframework.data.r2dbc.repository.Modifying;
+import org.springframework.data.r2dbc.repository.Query;
+import org.springframework.data.r2dbc.repository.R2dbcRepository;
+import org.springframework.stereotype.Repository;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+import java.time.LocalDateTime;
+
+@Repository
+public interface GroupBuyTeamDao extends R2dbcRepository {
+
+ Mono findByIdIsAndDeletedAtIsNull(Long id);
+
+ Flux findByActivityIdAndDeletedAtIsNull(Long activityId);
+
+ Flux findByActivityIdAndStatusAndDeletedAtIsNull(Long activityId, String status);
+
+ Flux findByStatusAndDeletedAtIsNull(String status);
+
+ @Query("SELECT * FROM group_buy_team WHERE status = :status AND expire_at < :now AND deleted_at IS NULL")
+ Flux findExpiredFormingTeams(String status, LocalDateTime now);
+
+ @Modifying
+ @Query("UPDATE group_buy_team SET status = :status, updated_at = :updatedAt WHERE id = :id")
+ Mono updateStatus(Long id, String status, LocalDateTime updatedAt);
+
+ @Modifying
+ @Query("UPDATE group_buy_team SET current_members = current_members + :count, updated_at = :updatedAt WHERE id = :id")
+ Mono incrementCurrentMembers(Long id, int count, LocalDateTime updatedAt);
+
+ @Modifying
+ @Query("UPDATE group_buy_team SET status = :status, success_at = :successAt, updated_at = :updatedAt WHERE id = :id")
+ Mono markSuccess(Long id, String status, LocalDateTime successAt, LocalDateTime updatedAt);
+}
diff --git a/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/groupbuy/domain/CreateTeamRequest.java b/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/groupbuy/domain/CreateTeamRequest.java
new file mode 100644
index 0000000..dbc49d0
--- /dev/null
+++ b/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/groupbuy/domain/CreateTeamRequest.java
@@ -0,0 +1,26 @@
+package cn.novalon.gym.manage.coupon.groupbuy.domain;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+
+@Schema(description = "创建拼团团队请求")
+public class CreateTeamRequest {
+
+ private Long activityId;
+ private Long memberId;
+
+ public Long getActivityId() {
+ return activityId;
+ }
+
+ public void setActivityId(Long activityId) {
+ this.activityId = activityId;
+ }
+
+ public Long getMemberId() {
+ return memberId;
+ }
+
+ public void setMemberId(Long memberId) {
+ this.memberId = memberId;
+ }
+}
diff --git a/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/groupbuy/domain/GroupBuyActivity.java b/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/groupbuy/domain/GroupBuyActivity.java
new file mode 100644
index 0000000..f674043
--- /dev/null
+++ b/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/groupbuy/domain/GroupBuyActivity.java
@@ -0,0 +1,138 @@
+package cn.novalon.gym.manage.coupon.groupbuy.domain;
+
+import cn.novalon.gym.manage.sys.core.domain.BaseDomain;
+import io.swagger.v3.oas.annotations.media.Schema;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+@Schema(description = "拼团活动")
+public class GroupBuyActivity extends BaseDomain {
+
+ private String name;
+ private String description;
+ private String productType;
+ private Long productId;
+ private String productName;
+ private BigDecimal originalPrice;
+ private BigDecimal groupPrice;
+ private Integer requiredMembers;
+ private Integer validHours;
+ private LocalDateTime startTime;
+ private LocalDateTime endTime;
+ private Integer stock;
+ private Integer soldCount;
+ private String status;
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+
+ public void setDescription(String description) {
+ this.description = description;
+ }
+
+ public String getProductType() {
+ return productType;
+ }
+
+ public void setProductType(String productType) {
+ this.productType = productType;
+ }
+
+ public Long getProductId() {
+ return productId;
+ }
+
+ public void setProductId(Long productId) {
+ this.productId = productId;
+ }
+
+ public String getProductName() {
+ return productName;
+ }
+
+ public void setProductName(String productName) {
+ this.productName = productName;
+ }
+
+ public BigDecimal getOriginalPrice() {
+ return originalPrice;
+ }
+
+ public void setOriginalPrice(BigDecimal originalPrice) {
+ this.originalPrice = originalPrice;
+ }
+
+ public BigDecimal getGroupPrice() {
+ return groupPrice;
+ }
+
+ public void setGroupPrice(BigDecimal groupPrice) {
+ this.groupPrice = groupPrice;
+ }
+
+ public Integer getRequiredMembers() {
+ return requiredMembers;
+ }
+
+ public void setRequiredMembers(Integer requiredMembers) {
+ this.requiredMembers = requiredMembers;
+ }
+
+ public Integer getValidHours() {
+ return validHours;
+ }
+
+ public void setValidHours(Integer validHours) {
+ this.validHours = validHours;
+ }
+
+ public LocalDateTime getStartTime() {
+ return startTime;
+ }
+
+ public void setStartTime(LocalDateTime startTime) {
+ this.startTime = startTime;
+ }
+
+ public LocalDateTime getEndTime() {
+ return endTime;
+ }
+
+ public void setEndTime(LocalDateTime endTime) {
+ this.endTime = endTime;
+ }
+
+ public Integer getStock() {
+ return stock;
+ }
+
+ public void setStock(Integer stock) {
+ this.stock = stock;
+ }
+
+ public Integer getSoldCount() {
+ return soldCount;
+ }
+
+ public void setSoldCount(Integer soldCount) {
+ this.soldCount = soldCount;
+ }
+
+ public String getStatus() {
+ return status;
+ }
+
+ public void setStatus(String status) {
+ this.status = status;
+ }
+}
diff --git a/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/groupbuy/domain/GroupBuyParticipant.java b/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/groupbuy/domain/GroupBuyParticipant.java
new file mode 100644
index 0000000..ef1f6e7
--- /dev/null
+++ b/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/groupbuy/domain/GroupBuyParticipant.java
@@ -0,0 +1,65 @@
+package cn.novalon.gym.manage.coupon.groupbuy.domain;
+
+import cn.novalon.gym.manage.sys.core.domain.BaseDomain;
+import io.swagger.v3.oas.annotations.media.Schema;
+
+import java.time.LocalDateTime;
+
+@Schema(description = "拼团参与人")
+public class GroupBuyParticipant extends BaseDomain {
+
+ private Long teamId;
+ private Long activityId;
+ private Long memberId;
+ private Boolean isLeader;
+ private String status;
+ private LocalDateTime joinAt;
+
+ public Long getTeamId() {
+ return teamId;
+ }
+
+ public void setTeamId(Long teamId) {
+ this.teamId = teamId;
+ }
+
+ public Long getActivityId() {
+ return activityId;
+ }
+
+ public void setActivityId(Long activityId) {
+ this.activityId = activityId;
+ }
+
+ public Long getMemberId() {
+ return memberId;
+ }
+
+ public void setMemberId(Long memberId) {
+ this.memberId = memberId;
+ }
+
+ public Boolean getIsLeader() {
+ return isLeader;
+ }
+
+ public void setIsLeader(Boolean isLeader) {
+ this.isLeader = isLeader;
+ }
+
+ public String getStatus() {
+ return status;
+ }
+
+ public void setStatus(String status) {
+ this.status = status;
+ }
+
+ public LocalDateTime getJoinAt() {
+ return joinAt;
+ }
+
+ public void setJoinAt(LocalDateTime joinAt) {
+ this.joinAt = joinAt;
+ }
+}
diff --git a/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/groupbuy/domain/GroupBuyStatistics.java b/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/groupbuy/domain/GroupBuyStatistics.java
new file mode 100644
index 0000000..83b8e9e
--- /dev/null
+++ b/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/groupbuy/domain/GroupBuyStatistics.java
@@ -0,0 +1,91 @@
+package cn.novalon.gym.manage.coupon.groupbuy.domain;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+
+import java.math.BigDecimal;
+
+@Schema(description = "拼团统计数据")
+public class GroupBuyStatistics {
+
+ private Long activityId;
+ private long totalTeams;
+ private long formingTeams;
+ private long successTeams;
+ private long failedTeams;
+ private long cancelledTeams;
+ private long totalParticipants;
+ private long soldCount;
+ private BigDecimal totalRevenue;
+
+ public Long getActivityId() {
+ return activityId;
+ }
+
+ public void setActivityId(Long activityId) {
+ this.activityId = activityId;
+ }
+
+ public long getTotalTeams() {
+ return totalTeams;
+ }
+
+ public void setTotalTeams(long totalTeams) {
+ this.totalTeams = totalTeams;
+ }
+
+ public long getFormingTeams() {
+ return formingTeams;
+ }
+
+ public void setFormingTeams(long formingTeams) {
+ this.formingTeams = formingTeams;
+ }
+
+ public long getSuccessTeams() {
+ return successTeams;
+ }
+
+ public void setSuccessTeams(long successTeams) {
+ this.successTeams = successTeams;
+ }
+
+ public long getFailedTeams() {
+ return failedTeams;
+ }
+
+ public void setFailedTeams(long failedTeams) {
+ this.failedTeams = failedTeams;
+ }
+
+ public long getCancelledTeams() {
+ return cancelledTeams;
+ }
+
+ public void setCancelledTeams(long cancelledTeams) {
+ this.cancelledTeams = cancelledTeams;
+ }
+
+ public long getTotalParticipants() {
+ return totalParticipants;
+ }
+
+ public void setTotalParticipants(long totalParticipants) {
+ this.totalParticipants = totalParticipants;
+ }
+
+ public long getSoldCount() {
+ return soldCount;
+ }
+
+ public void setSoldCount(long soldCount) {
+ this.soldCount = soldCount;
+ }
+
+ public BigDecimal getTotalRevenue() {
+ return totalRevenue;
+ }
+
+ public void setTotalRevenue(BigDecimal totalRevenue) {
+ this.totalRevenue = totalRevenue;
+ }
+}
diff --git a/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/groupbuy/domain/GroupBuyTeam.java b/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/groupbuy/domain/GroupBuyTeam.java
new file mode 100644
index 0000000..27eaba7
--- /dev/null
+++ b/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/groupbuy/domain/GroupBuyTeam.java
@@ -0,0 +1,74 @@
+package cn.novalon.gym.manage.coupon.groupbuy.domain;
+
+import cn.novalon.gym.manage.sys.core.domain.BaseDomain;
+import io.swagger.v3.oas.annotations.media.Schema;
+
+import java.time.LocalDateTime;
+
+@Schema(description = "拼团团队")
+public class GroupBuyTeam extends BaseDomain {
+
+ private Long activityId;
+ private Long leaderMemberId;
+ private Integer requiredMembers;
+ private Integer currentMembers;
+ private String status;
+ private LocalDateTime expireAt;
+ private LocalDateTime successAt;
+
+ public Long getActivityId() {
+ return activityId;
+ }
+
+ public void setActivityId(Long activityId) {
+ this.activityId = activityId;
+ }
+
+ public Long getLeaderMemberId() {
+ return leaderMemberId;
+ }
+
+ public void setLeaderMemberId(Long leaderMemberId) {
+ this.leaderMemberId = leaderMemberId;
+ }
+
+ public Integer getRequiredMembers() {
+ return requiredMembers;
+ }
+
+ public void setRequiredMembers(Integer requiredMembers) {
+ this.requiredMembers = requiredMembers;
+ }
+
+ public Integer getCurrentMembers() {
+ return currentMembers;
+ }
+
+ public void setCurrentMembers(Integer currentMembers) {
+ this.currentMembers = currentMembers;
+ }
+
+ public String getStatus() {
+ return status;
+ }
+
+ public void setStatus(String status) {
+ this.status = status;
+ }
+
+ public LocalDateTime getExpireAt() {
+ return expireAt;
+ }
+
+ public void setExpireAt(LocalDateTime expireAt) {
+ this.expireAt = expireAt;
+ }
+
+ public LocalDateTime getSuccessAt() {
+ return successAt;
+ }
+
+ public void setSuccessAt(LocalDateTime successAt) {
+ this.successAt = successAt;
+ }
+}
diff --git a/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/groupbuy/domain/JoinTeamRequest.java b/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/groupbuy/domain/JoinTeamRequest.java
new file mode 100644
index 0000000..8e49ef0
--- /dev/null
+++ b/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/groupbuy/domain/JoinTeamRequest.java
@@ -0,0 +1,17 @@
+package cn.novalon.gym.manage.coupon.groupbuy.domain;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+
+@Schema(description = "加入拼团团队请求")
+public class JoinTeamRequest {
+
+ private Long memberId;
+
+ public Long getMemberId() {
+ return memberId;
+ }
+
+ public void setMemberId(Long memberId) {
+ this.memberId = memberId;
+ }
+}
diff --git a/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/groupbuy/entity/GroupBuyActivityEntity.java b/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/groupbuy/entity/GroupBuyActivityEntity.java
new file mode 100644
index 0000000..643ae14
--- /dev/null
+++ b/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/groupbuy/entity/GroupBuyActivityEntity.java
@@ -0,0 +1,166 @@
+package cn.novalon.gym.manage.coupon.groupbuy.entity;
+
+import cn.novalon.gym.manage.db.entity.BaseEntity;
+import org.springframework.data.relational.core.mapping.Column;
+import org.springframework.data.relational.core.mapping.Table;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+@Table("group_buy_activity")
+public class GroupBuyActivityEntity extends BaseEntity {
+
+ @Column("name")
+ private String name;
+
+ @Column("description")
+ private String description;
+
+ @Column("product_type")
+ private String productType;
+
+ @Column("product_id")
+ private Long productId;
+
+ @Column("product_name")
+ private String productName;
+
+ @Column("original_price")
+ private BigDecimal originalPrice;
+
+ @Column("group_price")
+ private BigDecimal groupPrice;
+
+ @Column("required_members")
+ private Integer requiredMembers;
+
+ @Column("valid_hours")
+ private Integer validHours;
+
+ @Column("start_time")
+ private LocalDateTime startTime;
+
+ @Column("end_time")
+ private LocalDateTime endTime;
+
+ @Column("stock")
+ private Integer stock;
+
+ @Column("sold_count")
+ private Integer soldCount;
+
+ @Column("status")
+ private String status;
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+
+ public void setDescription(String description) {
+ this.description = description;
+ }
+
+ public String getProductType() {
+ return productType;
+ }
+
+ public void setProductType(String productType) {
+ this.productType = productType;
+ }
+
+ public Long getProductId() {
+ return productId;
+ }
+
+ public void setProductId(Long productId) {
+ this.productId = productId;
+ }
+
+ public String getProductName() {
+ return productName;
+ }
+
+ public void setProductName(String productName) {
+ this.productName = productName;
+ }
+
+ public BigDecimal getOriginalPrice() {
+ return originalPrice;
+ }
+
+ public void setOriginalPrice(BigDecimal originalPrice) {
+ this.originalPrice = originalPrice;
+ }
+
+ public BigDecimal getGroupPrice() {
+ return groupPrice;
+ }
+
+ public void setGroupPrice(BigDecimal groupPrice) {
+ this.groupPrice = groupPrice;
+ }
+
+ public Integer getRequiredMembers() {
+ return requiredMembers;
+ }
+
+ public void setRequiredMembers(Integer requiredMembers) {
+ this.requiredMembers = requiredMembers;
+ }
+
+ public Integer getValidHours() {
+ return validHours;
+ }
+
+ public void setValidHours(Integer validHours) {
+ this.validHours = validHours;
+ }
+
+ public LocalDateTime getStartTime() {
+ return startTime;
+ }
+
+ public void setStartTime(LocalDateTime startTime) {
+ this.startTime = startTime;
+ }
+
+ public LocalDateTime getEndTime() {
+ return endTime;
+ }
+
+ public void setEndTime(LocalDateTime endTime) {
+ this.endTime = endTime;
+ }
+
+ public Integer getStock() {
+ return stock;
+ }
+
+ public void setStock(Integer stock) {
+ this.stock = stock;
+ }
+
+ public Integer getSoldCount() {
+ return soldCount;
+ }
+
+ public void setSoldCount(Integer soldCount) {
+ this.soldCount = soldCount;
+ }
+
+ public String getStatus() {
+ return status;
+ }
+
+ public void setStatus(String status) {
+ this.status = status;
+ }
+}
diff --git a/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/groupbuy/entity/GroupBuyParticipantEntity.java b/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/groupbuy/entity/GroupBuyParticipantEntity.java
new file mode 100644
index 0000000..b3e178a
--- /dev/null
+++ b/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/groupbuy/entity/GroupBuyParticipantEntity.java
@@ -0,0 +1,77 @@
+package cn.novalon.gym.manage.coupon.groupbuy.entity;
+
+import cn.novalon.gym.manage.db.entity.BaseEntity;
+import org.springframework.data.relational.core.mapping.Column;
+import org.springframework.data.relational.core.mapping.Table;
+
+import java.time.LocalDateTime;
+
+@Table("group_buy_participant")
+public class GroupBuyParticipantEntity extends BaseEntity {
+
+ @Column("team_id")
+ private Long teamId;
+
+ @Column("activity_id")
+ private Long activityId;
+
+ @Column("member_id")
+ private Long memberId;
+
+ @Column("is_leader")
+ private Boolean isLeader;
+
+ @Column("status")
+ private String status;
+
+ @Column("join_at")
+ private LocalDateTime joinAt;
+
+ public Long getTeamId() {
+ return teamId;
+ }
+
+ public void setTeamId(Long teamId) {
+ this.teamId = teamId;
+ }
+
+ public Long getActivityId() {
+ return activityId;
+ }
+
+ public void setActivityId(Long activityId) {
+ this.activityId = activityId;
+ }
+
+ public Long getMemberId() {
+ return memberId;
+ }
+
+ public void setMemberId(Long memberId) {
+ this.memberId = memberId;
+ }
+
+ public Boolean getIsLeader() {
+ return isLeader;
+ }
+
+ public void setIsLeader(Boolean isLeader) {
+ this.isLeader = isLeader;
+ }
+
+ public String getStatus() {
+ return status;
+ }
+
+ public void setStatus(String status) {
+ this.status = status;
+ }
+
+ public LocalDateTime getJoinAt() {
+ return joinAt;
+ }
+
+ public void setJoinAt(LocalDateTime joinAt) {
+ this.joinAt = joinAt;
+ }
+}
diff --git a/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/groupbuy/entity/GroupBuyTeamEntity.java b/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/groupbuy/entity/GroupBuyTeamEntity.java
new file mode 100644
index 0000000..f1ce386
--- /dev/null
+++ b/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/groupbuy/entity/GroupBuyTeamEntity.java
@@ -0,0 +1,88 @@
+package cn.novalon.gym.manage.coupon.groupbuy.entity;
+
+import cn.novalon.gym.manage.db.entity.BaseEntity;
+import org.springframework.data.relational.core.mapping.Column;
+import org.springframework.data.relational.core.mapping.Table;
+
+import java.time.LocalDateTime;
+
+@Table("group_buy_team")
+public class GroupBuyTeamEntity extends BaseEntity {
+
+ @Column("activity_id")
+ private Long activityId;
+
+ @Column("leader_member_id")
+ private Long leaderMemberId;
+
+ @Column("required_members")
+ private Integer requiredMembers;
+
+ @Column("current_members")
+ private Integer currentMembers;
+
+ @Column("status")
+ private String status;
+
+ @Column("expire_at")
+ private LocalDateTime expireAt;
+
+ @Column("success_at")
+ private LocalDateTime successAt;
+
+ public Long getActivityId() {
+ return activityId;
+ }
+
+ public void setActivityId(Long activityId) {
+ this.activityId = activityId;
+ }
+
+ public Long getLeaderMemberId() {
+ return leaderMemberId;
+ }
+
+ public void setLeaderMemberId(Long leaderMemberId) {
+ this.leaderMemberId = leaderMemberId;
+ }
+
+ public Integer getRequiredMembers() {
+ return requiredMembers;
+ }
+
+ public void setRequiredMembers(Integer requiredMembers) {
+ this.requiredMembers = requiredMembers;
+ }
+
+ public Integer getCurrentMembers() {
+ return currentMembers;
+ }
+
+ public void setCurrentMembers(Integer currentMembers) {
+ this.currentMembers = currentMembers;
+ }
+
+ public String getStatus() {
+ return status;
+ }
+
+ public void setStatus(String status) {
+ this.status = status;
+ }
+
+ public LocalDateTime getExpireAt() {
+ return expireAt;
+ }
+
+ public void setExpireAt(LocalDateTime expireAt) {
+ this.expireAt = expireAt;
+ }
+
+ public LocalDateTime getSuccessAt() {
+ return successAt;
+ }
+
+ public void setSuccessAt(LocalDateTime successAt) {
+ this.successAt = successAt;
+ }
+}
diff --git a/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/groupbuy/enums/GroupBuyActivityStatus.java b/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/groupbuy/enums/GroupBuyActivityStatus.java
new file mode 100644
index 0000000..11d1e41
--- /dev/null
+++ b/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/groupbuy/enums/GroupBuyActivityStatus.java
@@ -0,0 +1,11 @@
+package cn.novalon.gym.manage.coupon.groupbuy.enums;
+
+/**
+ * 拼团活动状态
+ */
+public enum GroupBuyActivityStatus {
+ DRAFT,
+ ACTIVE,
+ TERMINATED,
+ EXPIRED
+}
diff --git a/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/groupbuy/enums/GroupBuyParticipantStatus.java b/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/groupbuy/enums/GroupBuyParticipantStatus.java
new file mode 100644
index 0000000..3328ef9
--- /dev/null
+++ b/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/groupbuy/enums/GroupBuyParticipantStatus.java
@@ -0,0 +1,9 @@
+package cn.novalon.gym.manage.coupon.groupbuy.enums;
+
+/**
+ * 拼团参与人状态
+ */
+public enum GroupBuyParticipantStatus {
+ JOINED,
+ CANCELLED
+}
diff --git a/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/groupbuy/enums/GroupBuyTeamStatus.java b/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/groupbuy/enums/GroupBuyTeamStatus.java
new file mode 100644
index 0000000..32e2a25
--- /dev/null
+++ b/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/groupbuy/enums/GroupBuyTeamStatus.java
@@ -0,0 +1,11 @@
+package cn.novalon.gym.manage.coupon.groupbuy.enums;
+
+/**
+ * 拼团团队状态
+ */
+public enum GroupBuyTeamStatus {
+ FORMING,
+ SUCCESS,
+ FAILED,
+ CANCELLED
+}
diff --git a/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/groupbuy/enums/ProductType.java b/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/groupbuy/enums/ProductType.java
new file mode 100644
index 0000000..e624775
--- /dev/null
+++ b/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/groupbuy/enums/ProductType.java
@@ -0,0 +1,10 @@
+package cn.novalon.gym.manage.coupon.groupbuy.enums;
+
+/**
+ * 商品类型
+ */
+public enum ProductType {
+ COURSE,
+ MEMBER_CARD,
+ PRODUCT
+}
diff --git a/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/groupbuy/handler/GroupBuyHandler.java b/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/groupbuy/handler/GroupBuyHandler.java
new file mode 100644
index 0000000..d6b59e5
--- /dev/null
+++ b/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/groupbuy/handler/GroupBuyHandler.java
@@ -0,0 +1,220 @@
+package cn.novalon.gym.manage.coupon.groupbuy.handler;
+
+import cn.novalon.gym.manage.common.dto.PageRequest;
+import cn.novalon.gym.manage.coupon.groupbuy.domain.CreateTeamRequest;
+import cn.novalon.gym.manage.coupon.groupbuy.domain.GroupBuyActivity;
+import cn.novalon.gym.manage.coupon.groupbuy.domain.GroupBuyTeam;
+import cn.novalon.gym.manage.coupon.groupbuy.domain.JoinTeamRequest;
+import cn.novalon.gym.manage.coupon.groupbuy.service.IGroupBuyActivityService;
+import cn.novalon.gym.manage.coupon.groupbuy.service.IGroupBuyTeamService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+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.util.HashMap;
+import java.util.Map;
+
+@Component
+@Tag(name = "拼团管理", description = "拼团相关操作")
+public class GroupBuyHandler {
+
+ private final IGroupBuyActivityService activityService;
+ private final IGroupBuyTeamService teamService;
+
+ public GroupBuyHandler(IGroupBuyActivityService activityService, IGroupBuyTeamService teamService) {
+ this.activityService = activityService;
+ this.teamService = teamService;
+ }
+
+ @Operation(summary = "获取所有拼团活动")
+ public Mono getAllActivities(ServerRequest request) {
+ boolean includeDeleted = Boolean.parseBoolean(request.queryParam("includeDeleted").orElse("false"));
+ return ServerResponse.ok()
+ .body(activityService.findAll(includeDeleted), GroupBuyActivity.class);
+ }
+
+ @Operation(summary = "分页获取拼团活动")
+ public Mono getActivitiesByPage(ServerRequest request) {
+ return request.bodyToMono(PageRequest.class)
+ .flatMap(pageRequest -> {
+ String status = request.queryParam("status").orElse(null);
+ normalizePageRequest(pageRequest);
+ return activityService.findByPage(pageRequest, status)
+ .flatMap(response -> ServerResponse.ok().bodyValue(response));
+ });
+ }
+
+ @Operation(summary = "搜索拼团活动")
+ public Mono searchActivities(ServerRequest request) {
+ String keyword = request.queryParam("keyword").orElse("");
+ String status = request.queryParam("status").orElse(null);
+ return ServerResponse.ok()
+ .body(activityService.findByKeywordAndStatus(keyword, status), GroupBuyActivity.class);
+ }
+
+ @Operation(summary = "根据ID获取拼团活动")
+ public Mono getActivityById(ServerRequest request) {
+ Long id = Long.valueOf(request.pathVariable("id"));
+ return activityService.findById(id)
+ .flatMap(activity -> ServerResponse.ok().bodyValue(activity))
+ .switchIfEmpty(ServerResponse.notFound().build());
+ }
+
+ @Operation(summary = "创建拼团活动")
+ public Mono createActivity(ServerRequest request) {
+ return request.bodyToMono(GroupBuyActivity.class)
+ .flatMap(activity -> {
+ if (activity.getName() == null || activity.getName().isEmpty()) {
+ return badRequest("活动名称不能为空");
+ }
+ return activityService.create(activity)
+ .flatMap(created -> successResponse("拼团活动创建成功", created))
+ .onErrorResume(this::errorResponse);
+ });
+ }
+
+ @Operation(summary = "更新拼团活动")
+ public Mono updateActivity(ServerRequest request) {
+ Long id = Long.valueOf(request.pathVariable("id"));
+ return request.bodyToMono(GroupBuyActivity.class)
+ .flatMap(activity -> activityService.update(id, activity)
+ .flatMap(updated -> successResponse("拼团活动更新成功", updated))
+ .onErrorResume(this::errorResponse));
+ }
+
+ @Operation(summary = "删除拼团活动")
+ public Mono deleteActivity(ServerRequest request) {
+ Long id = Long.valueOf(request.pathVariable("id"));
+ return activityService.delete(id)
+ .then(Mono.defer(() -> successResponse("拼团活动删除成功", null)))
+ .onErrorResume(this::errorResponse);
+ }
+
+ @Operation(summary = "发布拼团活动")
+ public Mono publishActivity(ServerRequest request) {
+ Long id = Long.valueOf(request.pathVariable("id"));
+ return activityService.publish(id)
+ .flatMap(activity -> successResponse("拼团活动发布成功", activity))
+ .onErrorResume(this::errorResponse);
+ }
+
+ @Operation(summary = "终止拼团活动")
+ public Mono terminateActivity(ServerRequest request) {
+ Long id = Long.valueOf(request.pathVariable("id"));
+ return activityService.terminate(id)
+ .flatMap(activity -> successResponse("拼团活动已终止", activity))
+ .onErrorResume(this::errorResponse);
+ }
+
+ @Operation(summary = "获取拼团统计")
+ public Mono getStatistics(ServerRequest request) {
+ Long id = Long.valueOf(request.pathVariable("id"));
+ return activityService.getStatistics(id)
+ .flatMap(stats -> ServerResponse.ok().bodyValue(stats))
+ .onErrorResume(this::errorResponse);
+ }
+
+ @Operation(summary = "创建拼团团队")
+ public Mono createTeam(ServerRequest request) {
+ return request.bodyToMono(CreateTeamRequest.class)
+ .flatMap(body -> {
+ if (body.getActivityId() == null || body.getMemberId() == null) {
+ return badRequest("activityId和memberId不能为空");
+ }
+ return teamService.createTeam(body)
+ .flatMap(team -> successResponse("拼团团队创建成功", team))
+ .onErrorResume(this::errorResponse);
+ });
+ }
+
+ @Operation(summary = "加入拼团团队")
+ public Mono joinTeam(ServerRequest request) {
+ Long teamId = Long.valueOf(request.pathVariable("teamId"));
+ return request.bodyToMono(JoinTeamRequest.class)
+ .flatMap(body -> {
+ if (body.getMemberId() == null) {
+ return badRequest("memberId不能为空");
+ }
+ return teamService.joinTeam(teamId, body)
+ .flatMap(team -> successResponse("加入拼团成功", team))
+ .onErrorResume(this::errorResponse);
+ });
+ }
+
+ @Operation(summary = "取消拼团团队")
+ public Mono cancelTeam(ServerRequest request) {
+ Long teamId = Long.valueOf(request.pathVariable("teamId"));
+ return teamService.cancelTeam(teamId)
+ .flatMap(team -> successResponse("拼团团队已取消", team))
+ .onErrorResume(this::errorResponse);
+ }
+
+ @Operation(summary = "查询拼团团队列表")
+ public Mono getTeams(ServerRequest request) {
+ String activityIdStr = request.queryParam("activityId").orElse(null);
+ if (activityIdStr == null) {
+ return badRequest("activityId不能为空");
+ }
+ Long activityId = Long.valueOf(activityIdStr);
+ String status = request.queryParam("status").orElse(null);
+ return ServerResponse.ok()
+ .body(teamService.findTeams(activityId, status), GroupBuyTeam.class);
+ }
+
+ @Operation(summary = "获取拼团团队详情")
+ public Mono getTeamById(ServerRequest request) {
+ Long teamId = Long.valueOf(request.pathVariable("teamId"));
+ return teamService.findTeamById(teamId)
+ .flatMap(team -> teamService.findParticipantsByTeamId(teamId)
+ .collectList()
+ .flatMap(participants -> {
+ Map result = new HashMap<>();
+ result.put("team", team);
+ result.put("participants", participants);
+ return ServerResponse.ok().bodyValue(result);
+ }))
+ .switchIfEmpty(ServerResponse.notFound().build());
+ }
+
+ private void normalizePageRequest(PageRequest pageRequest) {
+ if (pageRequest.getPage() < 0) {
+ pageRequest.setPage(0);
+ }
+ if (pageRequest.getSize() <= 0 || pageRequest.getSize() > 100) {
+ pageRequest.setSize(10);
+ }
+ if (pageRequest.getSort() == null || pageRequest.getSort().isEmpty()) {
+ pageRequest.setSort("id");
+ }
+ if (pageRequest.getOrder() == null || pageRequest.getOrder().isEmpty()) {
+ pageRequest.setOrder("desc");
+ }
+ }
+
+ private Mono successResponse(String message, Object data) {
+ Map response = new HashMap<>();
+ response.put("success", true);
+ response.put("message", message);
+ if (data != null) {
+ response.put("data", data);
+ }
+ return ServerResponse.ok().bodyValue(response);
+ }
+
+ private Mono badRequest(String message) {
+ Map error = new HashMap<>();
+ error.put("success", false);
+ error.put("message", message);
+ return ServerResponse.badRequest().bodyValue(error);
+ }
+
+ private Mono errorResponse(Throwable error) {
+ Map response = new HashMap<>();
+ response.put("success", false);
+ response.put("message", error.getMessage());
+ return ServerResponse.badRequest().bodyValue(response);
+ }
+}
diff --git a/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/groupbuy/repository/IGroupBuyActivityRepository.java b/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/groupbuy/repository/IGroupBuyActivityRepository.java
new file mode 100644
index 0000000..a69e411
--- /dev/null
+++ b/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/groupbuy/repository/IGroupBuyActivityRepository.java
@@ -0,0 +1,28 @@
+package cn.novalon.gym.manage.coupon.groupbuy.repository;
+
+import cn.novalon.gym.manage.coupon.groupbuy.domain.GroupBuyActivity;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+public interface IGroupBuyActivityRepository {
+
+ Mono findById(Long id);
+
+ Flux findAll(boolean includeDeleted);
+
+ Flux findByKeyword(String keyword);
+
+ Flux findByStatus(String status);
+
+ Flux findByKeywordAndStatus(String keyword, String status);
+
+ Mono save(GroupBuyActivity activity);
+
+ Mono update(GroupBuyActivity activity);
+
+ Mono deleteById(Long id);
+
+ Mono updateStatus(Long id, String status);
+
+ Mono incrementSoldCount(Long id, int count);
+}
diff --git a/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/groupbuy/repository/IGroupBuyParticipantRepository.java b/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/groupbuy/repository/IGroupBuyParticipantRepository.java
new file mode 100644
index 0000000..44dbf4c
--- /dev/null
+++ b/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/groupbuy/repository/IGroupBuyParticipantRepository.java
@@ -0,0 +1,20 @@
+package cn.novalon.gym.manage.coupon.groupbuy.repository;
+
+import cn.novalon.gym.manage.coupon.groupbuy.domain.GroupBuyParticipant;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+public interface IGroupBuyParticipantRepository {
+
+ Flux findByTeamId(Long teamId);
+
+ Flux findByActivityId(Long activityId);
+
+ Mono existsInFormingTeam(Long activityId, Long memberId);
+
+ Mono countByActivityId(Long activityId);
+
+ Mono save(GroupBuyParticipant participant);
+
+ Mono updateStatusByTeamId(Long teamId, String status);
+}
diff --git a/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/groupbuy/repository/IGroupBuyTeamRepository.java b/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/groupbuy/repository/IGroupBuyTeamRepository.java
new file mode 100644
index 0000000..3438570
--- /dev/null
+++ b/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/groupbuy/repository/IGroupBuyTeamRepository.java
@@ -0,0 +1,24 @@
+package cn.novalon.gym.manage.coupon.groupbuy.repository;
+
+import cn.novalon.gym.manage.coupon.groupbuy.domain.GroupBuyTeam;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+public interface IGroupBuyTeamRepository {
+
+ Mono findById(Long id);
+
+ Flux findByActivityId(Long activityId);
+
+ Flux findByActivityIdAndStatus(Long activityId, String status);
+
+ Flux findExpiredFormingTeams();
+
+ Mono save(GroupBuyTeam team);
+
+ Mono updateStatus(Long id, String status);
+
+ Mono incrementCurrentMembers(Long id, int count);
+
+ Mono markSuccess(Long id, String status);
+}
diff --git a/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/groupbuy/repository/impl/GroupBuyActivityRepository.java b/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/groupbuy/repository/impl/GroupBuyActivityRepository.java
new file mode 100644
index 0000000..c731a42
--- /dev/null
+++ b/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/groupbuy/repository/impl/GroupBuyActivityRepository.java
@@ -0,0 +1,133 @@
+package cn.novalon.gym.manage.coupon.groupbuy.repository.impl;
+
+import cn.novalon.gym.manage.coupon.groupbuy.converter.GroupBuyConverter;
+import cn.novalon.gym.manage.coupon.groupbuy.dao.GroupBuyActivityDao;
+import cn.novalon.gym.manage.coupon.groupbuy.domain.GroupBuyActivity;
+import cn.novalon.gym.manage.coupon.groupbuy.entity.GroupBuyActivityEntity;
+import cn.novalon.gym.manage.coupon.groupbuy.repository.IGroupBuyActivityRepository;
+import org.springframework.stereotype.Repository;
+import org.springframework.transaction.annotation.Transactional;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+import java.time.LocalDateTime;
+
+@Repository
+@Transactional
+public class GroupBuyActivityRepository implements IGroupBuyActivityRepository {
+
+ private final GroupBuyActivityDao activityDao;
+ private final GroupBuyConverter converter;
+
+ public GroupBuyActivityRepository(GroupBuyActivityDao activityDao, GroupBuyConverter converter) {
+ this.activityDao = activityDao;
+ this.converter = converter;
+ }
+
+ @Override
+ public Mono findById(Long id) {
+ return activityDao.findByIdIsAndDeletedAtIsNull(id).map(converter::toActivity);
+ }
+
+ @Override
+ public Flux findAll(boolean includeDeleted) {
+ if (includeDeleted) {
+ return activityDao.findAll().map(converter::toActivity);
+ }
+ return activityDao.findAllByDeletedAtIsNull().map(converter::toActivity);
+ }
+
+ @Override
+ public Flux findByKeyword(String keyword) {
+ if (keyword == null || keyword.isEmpty()) {
+ return findAll(false);
+ }
+ return activityDao.findByNameContainingAndDeletedAtIsNull(keyword).map(converter::toActivity);
+ }
+
+ @Override
+ public Flux findByStatus(String status) {
+ if (status == null || status.isEmpty()) {
+ return findAll(false);
+ }
+ return activityDao.findByStatusAndDeletedAtIsNull(status).map(converter::toActivity);
+ }
+
+ @Override
+ public Flux findByKeywordAndStatus(String keyword, String status) {
+ Flux result = findByKeyword(keyword);
+ if (status != null && !status.isEmpty()) {
+ result = result.filter(a -> status.equals(a.getStatus()));
+ }
+ return result;
+ }
+
+ @Override
+ public Mono save(GroupBuyActivity activity) {
+ GroupBuyActivityEntity entity = converter.toActivityEntity(activity);
+ return activityDao.save(entity).map(converter::toActivity);
+ }
+
+ @Override
+ public Mono update(GroupBuyActivity activity) {
+ return activityDao.findByIdIsAndDeletedAtIsNull(activity.getId())
+ .switchIfEmpty(Mono.error(new RuntimeException("拼团活动不存在")))
+ .flatMap(existing -> {
+ existing.markNotNew();
+ if (activity.getName() != null) {
+ existing.setName(activity.getName());
+ }
+ if (activity.getDescription() != null) {
+ existing.setDescription(activity.getDescription());
+ }
+ if (activity.getProductType() != null) {
+ existing.setProductType(activity.getProductType());
+ }
+ if (activity.getProductId() != null) {
+ existing.setProductId(activity.getProductId());
+ }
+ if (activity.getProductName() != null) {
+ existing.setProductName(activity.getProductName());
+ }
+ if (activity.getOriginalPrice() != null) {
+ existing.setOriginalPrice(activity.getOriginalPrice());
+ }
+ if (activity.getGroupPrice() != null) {
+ existing.setGroupPrice(activity.getGroupPrice());
+ }
+ if (activity.getRequiredMembers() != null) {
+ existing.setRequiredMembers(activity.getRequiredMembers());
+ }
+ if (activity.getValidHours() != null) {
+ existing.setValidHours(activity.getValidHours());
+ }
+ if (activity.getStartTime() != null) {
+ existing.setStartTime(activity.getStartTime());
+ }
+ if (activity.getEndTime() != null) {
+ existing.setEndTime(activity.getEndTime());
+ }
+ if (activity.getStock() != null) {
+ existing.setStock(activity.getStock());
+ }
+ existing.setUpdatedAt(LocalDateTime.now());
+ return activityDao.save(existing);
+ })
+ .map(converter::toActivity);
+ }
+
+ @Override
+ public Mono deleteById(Long id) {
+ return activityDao.softDelete(id, LocalDateTime.now()).then();
+ }
+
+ @Override
+ public Mono updateStatus(Long id, String status) {
+ return activityDao.updateStatus(id, status, LocalDateTime.now()).then();
+ }
+
+ @Override
+ public Mono incrementSoldCount(Long id, int count) {
+ return activityDao.incrementSoldCount(id, count, LocalDateTime.now()).then();
+ }
+}
diff --git a/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/groupbuy/repository/impl/GroupBuyParticipantRepository.java b/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/groupbuy/repository/impl/GroupBuyParticipantRepository.java
new file mode 100644
index 0000000..1403367
--- /dev/null
+++ b/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/groupbuy/repository/impl/GroupBuyParticipantRepository.java
@@ -0,0 +1,61 @@
+package cn.novalon.gym.manage.coupon.groupbuy.repository.impl;
+
+import cn.novalon.gym.manage.coupon.groupbuy.converter.GroupBuyConverter;
+import cn.novalon.gym.manage.coupon.groupbuy.dao.GroupBuyParticipantDao;
+import cn.novalon.gym.manage.coupon.groupbuy.domain.GroupBuyParticipant;
+import cn.novalon.gym.manage.coupon.groupbuy.enums.GroupBuyParticipantStatus;
+import cn.novalon.gym.manage.coupon.groupbuy.enums.GroupBuyTeamStatus;
+import cn.novalon.gym.manage.coupon.groupbuy.repository.IGroupBuyParticipantRepository;
+import org.springframework.stereotype.Repository;
+import org.springframework.transaction.annotation.Transactional;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+import java.time.LocalDateTime;
+
+@Repository
+@Transactional
+public class GroupBuyParticipantRepository implements IGroupBuyParticipantRepository {
+
+ private final GroupBuyParticipantDao participantDao;
+ private final GroupBuyConverter converter;
+
+ public GroupBuyParticipantRepository(GroupBuyParticipantDao participantDao, GroupBuyConverter converter) {
+ this.participantDao = participantDao;
+ this.converter = converter;
+ }
+
+ @Override
+ public Flux findByTeamId(Long teamId) {
+ return participantDao.findByTeamIdAndDeletedAtIsNull(teamId).map(converter::toParticipant);
+ }
+
+ @Override
+ public Flux findByActivityId(Long activityId) {
+ return participantDao.findByActivityIdAndDeletedAtIsNull(activityId).map(converter::toParticipant);
+ }
+
+ @Override
+ public Mono existsInFormingTeam(Long activityId, Long memberId) {
+ return participantDao.findActiveParticipantInFormingTeam(
+ activityId, memberId,
+ GroupBuyParticipantStatus.JOINED.name(),
+ GroupBuyTeamStatus.FORMING.name())
+ .hasElements();
+ }
+
+ @Override
+ public Mono countByActivityId(Long activityId) {
+ return participantDao.countByActivityIdAndDeletedAtIsNull(activityId);
+ }
+
+ @Override
+ public Mono save(GroupBuyParticipant participant) {
+ return participantDao.save(converter.toParticipantEntity(participant)).map(converter::toParticipant);
+ }
+
+ @Override
+ public Mono updateStatusByTeamId(Long teamId, String status) {
+ return participantDao.updateStatusByTeamId(teamId, status, LocalDateTime.now()).then();
+ }
+}
diff --git a/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/groupbuy/repository/impl/GroupBuyTeamRepository.java b/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/groupbuy/repository/impl/GroupBuyTeamRepository.java
new file mode 100644
index 0000000..8f88500
--- /dev/null
+++ b/gym-manage-api/gym-coupon/src/main/java/cn/novalon/gym/manage/coupon/groupbuy/repository/impl/GroupBuyTeamRepository.java
@@ -0,0 +1,70 @@
+package cn.novalon.gym.manage.coupon.groupbuy.repository.impl;
+
+import cn.novalon.gym.manage.coupon.groupbuy.converter.GroupBuyConverter;
+import cn.novalon.gym.manage.coupon.groupbuy.dao.GroupBuyTeamDao;
+import cn.novalon.gym.manage.coupon.groupbuy.domain.GroupBuyTeam;
+import cn.novalon.gym.manage.coupon.groupbuy.enums.GroupBuyTeamStatus;
+import cn.novalon.gym.manage.coupon.groupbuy.repository.IGroupBuyTeamRepository;
+import org.springframework.stereotype.Repository;
+import org.springframework.transaction.annotation.Transactional;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+import java.time.LocalDateTime;
+
+@Repository
+@Transactional
+public class GroupBuyTeamRepository implements IGroupBuyTeamRepository {
+
+ private final GroupBuyTeamDao teamDao;
+ private final GroupBuyConverter converter;
+
+ public GroupBuyTeamRepository(GroupBuyTeamDao teamDao, GroupBuyConverter converter) {
+ this.teamDao = teamDao;
+ this.converter = converter;
+ }
+
+ @Override
+ public Mono findById(Long id) {
+ return teamDao.findByIdIsAndDeletedAtIsNull(id).map(converter::toTeam);
+ }
+
+ @Override
+ public Flux findByActivityId(Long activityId) {
+ return teamDao.findByActivityIdAndDeletedAtIsNull(activityId).map(converter::toTeam);
+ }
+
+ @Override
+ public Flux findByActivityIdAndStatus(Long activityId, String status) {
+ if (status == null || status.isEmpty()) {
+ return findByActivityId(activityId);
+ }
+ return teamDao.findByActivityIdAndStatusAndDeletedAtIsNull(activityId, status).map(converter::toTeam);
+ }
+
+ @Override
+ public Flux findExpiredFormingTeams() {
+ return teamDao.findExpiredFormingTeams(
+ GroupBuyTeamStatus.FORMING.name(), LocalDateTime.now()).map(converter::toTeam);
+ }
+
+ @Override
+ public Mono save(GroupBuyTeam team) {
+ return teamDao.save(converter.toTeamEntity(team)).map(converter::toTeam);
+ }
+
+ @Override
+ public Mono updateStatus(Long id, String status) {
+ return teamDao.updateStatus(id, status, LocalDateTime.now()).then();
+ }
+
+ @Override
+ public Mono incrementCurrentMembers(Long id, int count) {
+ return teamDao.incrementCurrentMembers(id, count, LocalDateTime.now()).then();
+ }
+
+ @Override
+ public Mono