Sfoglia il codice sorgente

Merge remote-tracking branch 'origin/cfclub' into cfclub

E2E Test Bot 1 settimana fa
parent
commit
ff18214799

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

@@ -3397,6 +3397,9 @@ insertSysConfigSeed("gift_membership_pay_price", "198000", "送会员商品达
                 "BIGINT DEFAULT NULL COMMENT '关联商城商品 products.id,NULL=不可链接'");
                 "BIGINT DEFAULT NULL COMMENT '关联商城商品 products.id,NULL=不可链接'");
         // 迁移433: 家庭指标聚合方式配置表(五维能量汇总/珍珠图/章鱼图汇集配置)
         // 迁移433: 家庭指标聚合方式配置表(五维能量汇总/珍珠图/章鱼图汇集配置)
         createFamilyMetricAggConfigTable();
         createFamilyMetricAggConfigTable();
+
+        // 迁移434: SKU商品组合明细表 + 订单记录下单SKU(组合SKU识别)
+        migrateSkuCombo();
     }
     }
 
 
     /**
     /**
@@ -14529,6 +14532,39 @@ log.info("迁移298: 已为无 openid 的 child 账号补齐 family_members 记
         }
         }
     }
     }
 
 
+    // ==================== 迁移434:SKU 商品组合 ====================
+
+    void migrateSkuCombo() {
+        try {
+            jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS `product_sku_combo_items` ("
+                    + "`id` BIGINT AUTO_INCREMENT PRIMARY KEY, "
+                    + "`sku_id` BIGINT NOT NULL COMMENT '组合SKU ID(product_skus.id)', "
+                    + "`child_product_id` BIGINT NOT NULL COMMENT '组成商品ID', "
+                    + "`child_sku_id` BIGINT NULL COMMENT '组成商品SKU ID(组成商品有SKU时必填)', "
+                    + "`quantity` INT NOT NULL DEFAULT 1 COMMENT '该组成商品数量', "
+                    + "`created_at` DATETIME DEFAULT CURRENT_TIMESTAMP, "
+                    + "INDEX `idx_sku` (`sku_id`), "
+                    + "INDEX `idx_child_product` (`child_product_id`) "
+                    + ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品SKU组合明细'");
+            log.info("迁移434: 已创建 product_sku_combo_items 表");
+        } catch (Exception e) {
+            log.warn("迁移434: 创建 product_sku_combo_items 表失败(可能已存在): {}", e.getMessage());
+        }
+        try {
+            Integer colExists = jdbcTemplate.queryForObject(
+                    "SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'product_orders' AND COLUMN_NAME = 'sku_id'",
+                    Integer.class);
+            if (colExists == null || colExists == 0) {
+                jdbcTemplate.execute("ALTER TABLE product_orders ADD COLUMN sku_id BIGINT NULL COMMENT '下单SKU ID(组合SKU识别用)' AFTER product_type");
+                log.info("迁移434: 已为 product_orders 增加 sku_id 列");
+            } else {
+                log.info("迁移434: product_orders.sku_id 列已存在,跳过");
+            }
+        } catch (Exception e) {
+            log.warn("迁移434: 检查/添加 product_orders.sku_id 列失败: {}", e.getMessage());
+        }
+    }
+
     // ==================== 迁移429:用户关注指标收藏(T04 方案A) ====================
     // ==================== 迁移429:用户关注指标收藏(T04 方案A) ====================
 
 
     /**
     /**
@@ -14843,6 +14879,20 @@ log.info("迁移298: 已为无 openid 的 child 账号补齐 family_members 记
         } catch (Exception e) {
         } catch (Exception e) {
             log.warn("迁移426: 创建 attribution_reports 表失败(可能已存在): {}", e.getMessage());
             log.warn("迁移426: 创建 attribution_reports 表失败(可能已存在): {}", e.getMessage());
         }
         }
+
+        // 迁移427: product_bundle_items 增加套餐SKU锚点列(订单套餐自动定价)
+        // bundle_sku_id 非空 = 定价套餐行(套餐价取该SKU.price,CartService/ProductOrderService 匹配用);
+        // 为空 = 仅出库联动行(InventoryService 语义,不参与定价)。
+        ensureColumn("product_bundle_items", "bundle_sku_id",
+                "BIGINT NULL COMMENT '套餐SKU锚点:非空=订单套餐自动定价(套餐价取该SKU.price);空=仅出库联动'");
+        // 同时放开 bundle_product_id 可空:定价套餐行置 NULL,避免 InventoryService
+        // (按 bundle_product_id 关联、仅 productType=bundle 商品)误触发出库联动。
+        try {
+            jdbcTemplate.execute("ALTER TABLE product_bundle_items MODIFY COLUMN bundle_product_id BIGINT NULL COMMENT '套餐商品ID(出库联动行=套餐商品ID;定价套餐行置NULL)'");
+            log.info("迁移427: product_bundle_items.bundle_product_id 已放开可空");
+        } catch (Exception e) {
+            log.warn("迁移427: 放开 bundle_product_id 可空失败(可能已放开): {}", e.getMessage());
+        }
     }
     }
 
 
     private static String text(JsonNode row, String field) {
     private static String text(JsonNode row, String field) {

+ 2 - 0
cfc-backend/src/main/java/com/etotem/cfc/entity/ProductBundleItem.java

@@ -14,6 +14,8 @@ public class ProductBundleItem implements Serializable {
     @TableId(type = IdType.AUTO)
     @TableId(type = IdType.AUTO)
     private Long id;
     private Long id;
     private Long bundleProductId;
     private Long bundleProductId;
+    /** 套餐SKU锚点:非空=定价套餐行(套餐价取该SKU.price,用于订单套餐自动定价匹配);为空=仅出库联动行 */
+    private Long bundleSkuId;
     private Long childProductId;
     private Long childProductId;
     private Long childSkuId;
     private Long childSkuId;
     private Integer quantity;
     private Integer quantity;

+ 15 - 1
cfc-backend/src/main/resources/schema.sql

@@ -1306,6 +1306,7 @@ CREATE TABLE IF NOT EXISTS product_orders (
     product_id BIGINT NOT NULL COMMENT '商品ID',
     product_id BIGINT NOT NULL COMMENT '商品ID',
     product_name VARCHAR(200) COMMENT '商品名称',
     product_name VARCHAR(200) COMMENT '商品名称',
     product_type VARCHAR(32) COMMENT '商品类型',
     product_type VARCHAR(32) COMMENT '商品类型',
+    sku_id BIGINT COMMENT '下单SKU ID(组合SKU识别用)',
     buyer_id BIGINT NOT NULL COMMENT '购买者ID',
     buyer_id BIGINT NOT NULL COMMENT '购买者ID',
     family_id BIGINT COMMENT '家庭ID',
     family_id BIGINT COMMENT '家庭ID',
     quantity INT NOT NULL DEFAULT 1 COMMENT '数量',
     quantity INT NOT NULL DEFAULT 1 COMMENT '数量',
@@ -5583,7 +5584,8 @@ CREATE TABLE IF NOT EXISTS system_prompt_versions (
 -- 套餐商品组成明细表(套餐 = 商品 productType=bundle,子商品在 product_bundle_items 中配置)
 -- 套餐商品组成明细表(套餐 = 商品 productType=bundle,子商品在 product_bundle_items 中配置)
 CREATE TABLE IF NOT EXISTS product_bundle_items (
 CREATE TABLE IF NOT EXISTS product_bundle_items (
     id BIGINT AUTO_INCREMENT PRIMARY KEY,
     id BIGINT AUTO_INCREMENT PRIMARY KEY,
-    bundle_product_id BIGINT NOT NULL COMMENT '套餐商品ID',
+    bundle_product_id BIGINT DEFAULT NULL COMMENT '套餐商品ID(定价套餐行置NULL以避免误触发出库联动;出库联动行=$套餐商品ID)',
+    bundle_sku_id BIGINT DEFAULT NULL COMMENT '套餐SKU锚点:非空=定价套餐行(订单套餐自动定价,套餐价取该SKU.price);空=仅出库联动',
     child_product_id BIGINT NOT NULL COMMENT '子商品ID',
     child_product_id BIGINT NOT NULL COMMENT '子商品ID',
     child_sku_id BIGINT DEFAULT NULL COMMENT '子SKU ID(为空时直接扣 product.stock)',
     child_sku_id BIGINT DEFAULT NULL COMMENT '子SKU ID(为空时直接扣 product.stock)',
     quantity INT NOT NULL DEFAULT 1 COMMENT '子商品数量',
     quantity INT NOT NULL DEFAULT 1 COMMENT '子商品数量',
@@ -5592,6 +5594,18 @@ CREATE TABLE IF NOT EXISTS product_bundle_items (
     INDEX idx_child (child_product_id)
     INDEX idx_child (child_product_id)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='套餐商品组成明细';
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='套餐商品组成明细';
 
 
+-- 商品SKU组合明细表(组合SKU = 多个商品/子SKU + 数量,整体手填售价售卖)
+CREATE TABLE IF NOT EXISTS product_sku_combo_items (
+    id BIGINT AUTO_INCREMENT PRIMARY KEY,
+    sku_id BIGINT NOT NULL COMMENT '组合SKU ID(product_skus.id)',
+    child_product_id BIGINT NOT NULL COMMENT '组成商品ID',
+    child_sku_id BIGINT DEFAULT NULL COMMENT '组成商品SKU ID(组成商品有SKU时必填)',
+    quantity INT NOT NULL DEFAULT 1 COMMENT '该组成商品数量',
+    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+    INDEX idx_sku (sku_id),
+    INDEX idx_child_product (child_product_id)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品SKU组合明细';
+
 -- 联系人表(cfclub 联系人管理)
 -- 联系人表(cfclub 联系人管理)
 CREATE TABLE IF NOT EXISTS contacts (
 CREATE TABLE IF NOT EXISTS contacts (
     id BIGINT AUTO_INCREMENT PRIMARY KEY,
     id BIGINT AUTO_INCREMENT PRIMARY KEY,

+ 12 - 12
docs/superpowers/plans/2026-10-31-sku-combo.md

@@ -14,7 +14,7 @@
 
 
 - 所有代码改动在远程 `/app/cfc`(SSH `root@111.228.6.214`)执行;本地 `/root/cfc` 只是壳。
 - 所有代码改动在远程 `/app/cfc`(SSH `root@111.228.6.214`)执行;本地 `/root/cfc` 只是壳。
 - 分支 `cfclub`;每任务完成即提交(`git fetch origin && git pull --rebase origin cfclub` 后提交)。
 - 分支 `cfclub`;每任务完成即提交(`git fetch origin && git pull --rebase origin cfclub` 后提交)。
-- 迁移编号固定 **432**,并同步 `cfc-backend/src/main/resources/schema.sql`。
+- 迁移编号固定 **434**,并同步 `cfc-backend/src/main/resources/schema.sql`。
 - 不改动 `product_bundle_items`(套餐)逻辑。
 - 不改动 `product_bundle_items`(套餐)逻辑。
 - 不改动现有非组合、非 linked、非 bundle 订单的任何路径。
 - 不改动现有非组合、非 linked、非 bundle 订单的任何路径。
 - 组合订单:**不扣父商品库存**;取消/退款/后台关闭 3 处必须回补组成商品。
 - 组合订单:**不扣父商品库存**;取消/退款/后台关闭 3 处必须回补组成商品。
@@ -36,7 +36,7 @@
 
 
 ---
 ---
 
 
-### Task 1: 迁移 432 + schema.sql
+### Task 1: 迁移 434 + schema.sql
 
 
 **Files:**
 **Files:**
 - Modify: `cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java`(`runMigrations()` 末尾 + 新增 `migrateSkuCombo()`)
 - Modify: `cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java`(`runMigrations()` 末尾 + 新增 `migrateSkuCombo()`)
@@ -48,14 +48,14 @@
 - [ ] **Step 1: 在 `runMigrations()` 末尾(`migrateFamilyNameBackfill();` 之后)追加调用**
 - [ ] **Step 1: 在 `runMigrations()` 末尾(`migrateFamilyNameBackfill();` 之后)追加调用**
 
 
 ```java
 ```java
-        // 迁移432: SKU商品组合明细表 + 订单记录下单SKU(组合SKU识别)
+        // 迁移434: SKU商品组合明细表 + 订单记录下单SKU(组合SKU识别)
         migrateSkuCombo();
         migrateSkuCombo();
 ```
 ```
 
 
 - [ ] **Step 2: 在 `createCouponGiftTransferTable()` 之后新增迁移方法(幂等;参照迁移 428 写法)**
 - [ ] **Step 2: 在 `createCouponGiftTransferTable()` 之后新增迁移方法(幂等;参照迁移 428 写法)**
 
 
 ```java
 ```java
-    // ==================== 迁移432:SKU 商品组合 ====================
+    // ==================== 迁移434:SKU 商品组合 ====================
 
 
     void migrateSkuCombo() {
     void migrateSkuCombo() {
         try {
         try {
@@ -69,9 +69,9 @@
                     + "INDEX `idx_sku` (`sku_id`), "
                     + "INDEX `idx_sku` (`sku_id`), "
                     + "INDEX `idx_child_product` (`child_product_id`) "
                     + "INDEX `idx_child_product` (`child_product_id`) "
                     + ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品SKU组合明细'");
                     + ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品SKU组合明细'");
-            log.info("迁移432: 已创建 product_sku_combo_items 表");
+            log.info("迁移434: 已创建 product_sku_combo_items 表");
         } catch (Exception e) {
         } catch (Exception e) {
-            log.warn("迁移432: 创建 product_sku_combo_items 表失败(可能已存在): {}", e.getMessage());
+            log.warn("迁移434: 创建 product_sku_combo_items 表失败(可能已存在): {}", e.getMessage());
         }
         }
         try {
         try {
             Integer colExists = jdbcTemplate.queryForObject(
             Integer colExists = jdbcTemplate.queryForObject(
@@ -79,12 +79,12 @@
                     Integer.class);
                     Integer.class);
             if (colExists == null || colExists == 0) {
             if (colExists == null || colExists == 0) {
                 jdbcTemplate.execute("ALTER TABLE product_orders ADD COLUMN sku_id BIGINT NULL COMMENT '下单SKU ID(组合SKU识别用)' AFTER product_type");
                 jdbcTemplate.execute("ALTER TABLE product_orders ADD COLUMN sku_id BIGINT NULL COMMENT '下单SKU ID(组合SKU识别用)' AFTER product_type");
-                log.info("迁移432: 已为 product_orders 增加 sku_id 列");
+                log.info("迁移434: 已为 product_orders 增加 sku_id 列");
             } else {
             } else {
-                log.info("迁移432: product_orders.sku_id 列已存在,跳过");
+                log.info("迁移434: product_orders.sku_id 列已存在,跳过");
             }
             }
         } catch (Exception e) {
         } catch (Exception e) {
-            log.warn("迁移432: 检查/添加 product_orders.sku_id 列失败: {}", e.getMessage());
+            log.warn("迁移434: 检查/添加 product_orders.sku_id 列失败: {}", e.getMessage());
         }
         }
     }
     }
 ```
 ```
@@ -120,7 +120,7 @@ Expected: BUILD SUCCESS(无输出错误)
 
 
 ```bash
 ```bash
 cd /app/cfc && git add cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java cfc-backend/src/main/resources/schema.sql
 cd /app/cfc && git add cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java cfc-backend/src/main/resources/schema.sql
-git commit -m "feat(backend): 迁移432 SKU商品组合明细表与订单sku_id"
+git commit -m "feat(backend): 迁移434 SKU商品组合明细表与订单sku_id"
 ```
 ```
 
 
 ---
 ---
@@ -1292,11 +1292,11 @@ systemctl status cfc-backend --no-pager | head -5
 ss -tlnp | grep 9082
 ss -tlnp | grep 9082
 curl -s -o /dev/null -w "%{http_code}\n" -X POST http://localhost:9082/api/cart/list
 curl -s -o /dev/null -w "%{http_code}\n" -X POST http://localhost:9082/api/cart/list
 curl -s -o /dev/null -w "%{http_code}\n" -k https://cfc.etotem.com.cn/admin/
 curl -s -o /dev/null -w "%{http_code}\n" -k https://cfc.etotem.com.cn/admin/
-grep "迁移432" /app/pro/cfc/backend/cfc-backend.log | tail -5
+grep "迁移434" /app/pro/cfc/backend/cfc-backend.log | tail -5
 curl -s -X POST http://localhost:9082/api/admin/product/linked/list -H 'Content-Type: application/json' -d '{"crossCategory":true,"page":1,"pageSize":3}'
 curl -s -X POST http://localhost:9082/api/admin/product/linked/list -H 'Content-Type: application/json' -d '{"crossCategory":true,"page":1,"pageSize":3}'
 ```
 ```
 
 
-Expected: 服务 active;端口 9082 监听;两个 URL 200;日志含「迁移432」;选择器返回 `code=200` 且含 `skus`/`hasSkus`。
+Expected: 服务 active;端口 9082 监听;两个 URL 200;日志含「迁移434」;选择器返回 `code=200` 且含 `skus`/`hasSkus`。
 
 
 - [ ] **Step 5: 失败回滚**
 - [ ] **Step 5: 失败回滚**
 
 

+ 3 - 3
docs/superpowers/specs/2026-10-31-sku-combo-design.md

@@ -26,7 +26,7 @@
 - 组合组成商品**禁止**:选中商品自身、`productType=bundle` 的商品、以及其他组合 SKU 的商品(避免递归扣减)。
 - 组合组成商品**禁止**:选中商品自身、`productType=bundle` 的商品、以及其他组合 SKU 的商品(避免递归扣减)。
 - `comboItems` 语义:**非空数组**=整表替换;**空数组**=清除组合;**字段省略(null)**=保持不变(避免部分更新误清空)。
 - `comboItems` 语义:**非空数组**=整表替换;**空数组**=清除组合;**字段省略(null)**=保持不变(避免部分更新误清空)。
 - 切换模式时显式清理旧数据(切到组合→清 `linkedProductId`;切到关联/规格→删组合明细)。
 - 切换模式时显式清理旧数据(切到组合→清 `linkedProductId`;切到关联/规格→删组合明细)。
-- 支付/退款识别组合订单:**给 `product_orders` 增加 `sku_id` 列**(迁移 432 一并做),下单时记录;比改造 `order_items` 写入路径更直接。
+- 支付/退款识别组合订单:**给 `product_orders` 增加 `sku_id` 列**(迁移 434 一并做),下单时记录;比改造 `order_items` 写入路径更直接。
 - 支付扣减只对**组合行**做逐项扣减,不改变现有非组合订单(含多行订单)的扣减行为,避免扩大回归面。
 - 支付扣减只对**组合行**做逐项扣减,不改变现有非组合订单(含多行订单)的扣减行为,避免扩大回归面。
 
 
 ## 3. 目标
 ## 3. 目标
@@ -67,7 +67,7 @@ CREATE TABLE IF NOT EXISTS product_sku_combo_items (
 ALTER TABLE product_orders ADD COLUMN sku_id BIGINT NULL COMMENT '下单SKU ID(组合SKU识别用)';
 ALTER TABLE product_orders ADD COLUMN sku_id BIGINT NULL COMMENT '下单SKU ID(组合SKU识别用)';
 ```
 ```
 
 
-- 迁移编号 **432**,放在 `DatabaseInitializer.runMigrations()` 末尾(当前最大 431)
+- 迁移编号 **434**,放在 `DatabaseInitializer.runMigrations()` 末尾(既有编号至 433)
 - 同步 `schema.sql`
 - 同步 `schema.sql`
 
 
 ### 5.3 `product_skus`
 ### 5.3 `product_skus`
@@ -155,7 +155,7 @@ ALTER TABLE product_orders ADD COLUMN sku_id BIGINT NULL COMMENT '下单SKU ID
 
 
 ## 9. 实施顺序建议
 ## 9. 实施顺序建议
 
 
-1. 迁移 432(新表 + product_orders.sku_id)+ schema.sql
+1. 迁移 434(新表 + product_orders.sku_id)+ schema.sql
 2. 实体/Mapper/DTO/Service/Controller(组合 CRUD)
 2. 实体/Mapper/DTO/Service/Controller(组合 CRUD)
 3. 消费点改造(Cart → Order 下单 → 支付扣减 → 退款回补 → 商品详情)
 3. 消费点改造(Cart → Order 下单 → 支付扣减 → 退款回补 → 商品详情)
 4. 前端 ProductPicker 多选/SKU/跨分类
 4. 前端 ProductPicker 多选/SKU/跨分类