Browse Source

feat(db): 添加special_price_deals表 + product_orders.special_deal_id列 + 实体/Mapper

Sisyphus Agent 5 days ago
parent
commit
4226064542

+ 27 - 0
cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java

@@ -3125,6 +3125,33 @@ insertSysConfigSeed("gift_membership_pay_price", "198000", "送会员商品达
         // 迁移388: activities 添加 poster_template_id 列(小程序端创建活动时选择海报模板并保存,详情页生成海报时默认使用)
         ensureColumn("activities", "poster_template_id",
                 "BIGINT NULL COMMENT '创建时选择的海报模板ID(activity_poster_templates.id, 小程序端海报模板选择)'");
+
+        // 迁移389: 创建 special_price_deals 表(限时特价活动/扫码闪购,见 specs/2026-10-20-limited-time-special-price-design.md)
+        try {
+            jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS special_price_deals (" +
+                    "id BIGINT AUTO_INCREMENT PRIMARY KEY," +
+                    "product_id BIGINT NOT NULL," +
+                    "name VARCHAR(100) NOT NULL," +
+                    "special_price INT NOT NULL COMMENT '特价(分)'," +
+                    "start_time DATETIME," +
+                    "end_time DATETIME," +
+                    "total_quantity INT NOT NULL DEFAULT 0," +
+                    "sold_quantity INT NOT NULL DEFAULT 0," +
+                    "per_user_limit INT NOT NULL DEFAULT 1," +
+                    "allow_coupon TINYINT NOT NULL DEFAULT 0," +
+                    "status VARCHAR(20) NOT NULL DEFAULT 'draft'," +
+                    "created_at DATETIME," +
+                    "updated_at DATETIME," +
+                    "INDEX idx_product (product_id)," +
+                    "INDEX idx_status (status, start_time, end_time)" +
+                    ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='限时特价活动'");
+            log.info("迁移389: 已创建 special_price_deals 表");
+        } catch (Exception e) {
+            log.warn("迁移389: 创建 special_price_deals 表失败(可能已存在): {}", e.getMessage());
+        }
+
+        // 迁移390: product_orders 添加 special_deal_id 列(限时特价订单关联)
+        ensureColumn("product_orders", "special_deal_id", "BIGINT NULL COMMENT '限时特价活动ID(非空=特价单)'");
     }
 
     private void migrateHealthDimensionsTable() {

+ 4 - 0
cfc-backend/src/main/java/com/etotem/cfc/entity/ProductOrder.java

@@ -82,6 +82,9 @@ public class ProductOrder implements Serializable {
     private Date createdAt;
     private Date updatedAt;
 
+    // 限时特价
+    private Long specialDealId;        // 限时特价活动ID(非空=特价单)
+
     // Explicit setters needed due to Lombok issues
     public void setOrderNo(String orderNo) { this.orderNo = orderNo; }
     public void setBuyerId(Long buyerId) { this.buyerId = buyerId; }
@@ -96,6 +99,7 @@ public class ProductOrder implements Serializable {
     public void setCancelAt(Date cancelAt) { this.cancelAt = cancelAt; }
     public void setCreatedAt(Date createdAt) { this.createdAt = createdAt; }
     public void setUpdatedAt(Date updatedAt) { this.updatedAt = updatedAt; }
+    public void setSpecialDealId(Long specialDealId) { this.specialDealId = specialDealId; }
     public void setSupplySystemId(Long supplySystemId) { this.supplySystemId = supplySystemId; }
     public void setSupplierId(Long supplierId) { this.supplierId = supplierId; }
     public void setSupplyCommission(Integer supplyCommission) { this.supplyCommission = supplyCommission; }

+ 40 - 0
cfc-backend/src/main/java/com/etotem/cfc/entity/SpecialPriceDeal.java

@@ -0,0 +1,40 @@
+package com.etotem.cfc.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.Date;
+
+/**
+ * 限时特价活动(扫码闪购)。
+ * 设计见 docs/superpowers/specs/2026-10-20-limited-time-special-price-design.md
+ */
+@Data
+@TableName("special_price_deals")
+public class SpecialPriceDeal implements Serializable {
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    private Long productId;
+    private String name;
+    private Integer specialPrice;   // 特价(分)
+    private Date startTime;
+    private Date endTime;
+    private Integer totalQuantity;
+    private Integer soldQuantity;   // 已售数量(下单即扣)
+    private Integer perUserLimit;
+    private Integer allowCoupon;    // 0=禁止 1=允许
+    private String status;          // draft/active/disabled
+    private Date createdAt;
+    private Date updatedAt;
+
+    // 显式 setter(沿用 ProductOrder 的处理方式,规避 Lombok 字段冲突)
+    public void setStatus(String status) { this.status = status; }
+    public void setSoldQuantity(Integer soldQuantity) { this.soldQuantity = soldQuantity; }
+    public void setTotalQuantity(Integer totalQuantity) { this.totalQuantity = totalQuantity; }
+    public void setUpdatedAt(Date updatedAt) { this.updatedAt = updatedAt; }
+}

+ 23 - 0
cfc-backend/src/main/java/com/etotem/cfc/mapper/SpecialPriceDealMapper.java

@@ -0,0 +1,23 @@
+package com.etotem.cfc.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.etotem.cfc.entity.SpecialPriceDeal;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+
+/**
+ * 限时特价活动 Mapper。
+ * 设计见 docs/superpowers/specs/2026-10-20-limited-time-special-price-design.md
+ */
+@Mapper
+public interface SpecialPriceDealMapper extends BaseMapper<SpecialPriceDeal> {
+
+    /** 原子扣量:WHERE id=? AND status='active' AND 在活动期内 AND sold_quantity + ? <= total_quantity(防并发超卖) */
+    int consumeQuota(@Param("id") Long dealId, @Param("qty") int qty);
+
+    /** 回滚释放:sold_quantity = GREATEST(sold_quantity - ?, 0)(不跌到负数) */
+    int releaseQuota(@Param("id") Long dealId, @Param("qty") int qty);
+
+    /** 统计用户已购件数(pending/paid 等占用额度;仅排除 cancelled/refunded 终态) */
+    int userPurchased(@Param("dealId") Long dealId, @Param("buyerId") Long buyerId);
+}

+ 33 - 0
cfc-backend/src/main/resources/mapper/SpecialPriceDealMapper.xml

@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.etotem.cfc.mapper.SpecialPriceDealMapper">
+
+    <!-- 原子扣量:条件扣减,影响行数为 0 表示已售罄/不在活动期/非 active -->
+    <update id="consumeQuota">
+        UPDATE special_price_deals
+        SET sold_quantity = sold_quantity + #{qty},
+            updated_at    = NOW()
+        WHERE id = #{id}
+          AND status = 'active'
+          AND start_time &lt;= NOW()
+          AND NOW() &lt;= end_time
+          AND sold_quantity + #{qty} &lt;= total_quantity
+    </update>
+
+    <!-- 回滚释放:取消/退款/超时取消时归还额度,GREATEST 保证不跌到负数 -->
+    <update id="releaseQuota">
+        UPDATE special_price_deals
+        SET sold_quantity = GREATEST(sold_quantity - #{qty}, 0),
+            updated_at    = NOW()
+        WHERE id = #{id}
+    </update>
+
+    <select id="userPurchased" resultType="int">
+        SELECT COALESCE(SUM(quantity), 0)
+        FROM product_orders
+        WHERE special_deal_id = #{dealId}
+          AND buyer_id = #{buyerId}
+          AND status NOT IN ('cancelled', 'refunded')
+    </select>
+
+</mapper>

+ 20 - 0
cfc-backend/src/main/resources/schema.sql

@@ -1349,6 +1349,7 @@ audit_reason TEXT COMMENT '审核拒绝原因',
     refund_time DATETIME DEFAULT NULL COMMENT '退款时间',
     created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
     updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+    special_deal_id BIGINT NULL COMMENT '限时特价活动ID(非空=特价单)',
     INDEX idx_buyer_id (buyer_id),
     INDEX idx_family_id (family_id),
     INDEX idx_product_id (product_id),
@@ -1356,6 +1357,25 @@ audit_reason TEXT COMMENT '审核拒绝原因',
     INDEX idx_supply_system_id (supply_system_id)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品订单表';
 
+-- 限时特价活动表
+CREATE TABLE IF NOT EXISTS special_price_deals (
+    id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID',
+    product_id BIGINT NOT NULL COMMENT '商品ID',
+    name VARCHAR(100) NOT NULL COMMENT '活动名称',
+    special_price INT NOT NULL COMMENT '特价(分)',
+    start_time DATETIME COMMENT '开始时间',
+    end_time DATETIME COMMENT '结束时间',
+    total_quantity INT NOT NULL DEFAULT 0 COMMENT '特价总供应量',
+    sold_quantity INT NOT NULL DEFAULT 0 COMMENT '已售数量(下单即扣)',
+    per_user_limit INT NOT NULL DEFAULT 1 COMMENT '每人限购件数',
+    allow_coupon TINYINT NOT NULL DEFAULT 0 COMMENT '是否允许优惠券 1=允许 0=禁止',
+    status VARCHAR(20) NOT NULL DEFAULT 'draft' COMMENT 'draft/active/disabled',
+    created_at DATETIME COMMENT '创建时间',
+    updated_at DATETIME COMMENT '更新时间',
+    INDEX idx_product (product_id),
+    INDEX idx_status (status, start_time, end_time)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='限时特价活动';
+
 -- 商品自取地点表
 CREATE TABLE IF NOT EXISTS product_pickup_locations (
     id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键',