|
|
@@ -0,0 +1,1354 @@
|
|
|
+# 商品可选赠品功能 实施计划
|
|
|
+
|
|
|
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
|
|
+
|
|
|
+**Goal:** 实现商品可选赠品功能:运营为商品配置不同会员等级的赠品池(含赠品商品 或 CF值),用户购买时按自身等级从赠品池中选0个或多个,支付成功后发放对应赠品。
|
|
|
+
|
|
|
+**Architecture:** 新增三张表(`product_gift_configs` / `product_gift_items` / `product_order_gifts`),新增 `ProductGiftService` 承载赠品配置与发放逻辑;在现有下单(`ProductOrderService.create`)和支付成功(`handlePaymentSuccess`)两个节点注入赠品校验与发放;商品详情接口附带赠品配置;管理端和小程序前端分别实现配置与选择 UI。
|
|
|
+
|
|
|
+**Tech Stack:** Java 8 / Spring Boot 2.7.18 / MyBatis-Plus / Vue 2 + uni-app(小程序)/ Vue 2 + Element UI(管理端)
|
|
|
+
|
|
|
+**Spec:** `docs/superpowers/specs/2026-09-02-product-gift-selection-design.md`
|
|
|
+
|
|
|
+## Global Constraints
|
|
|
+
|
|
|
+- 统一 `@PostMapping`,禁止 `@GetMapping/@PutMapping/@DeleteMapping`
|
|
|
+- DI 使用 `@Resource`,字段名必须与类型默认 Bean Name 一致
|
|
|
+- 响应统一 `Result<T>`(code/message/data),`Result.error(String)` 返回 code=500
|
|
|
+- 数据库迁移在 `DatabaseInitializer.runMigrations()` 中执行,幂等(try-catch 包裹);同步更新 `schema.sql`
|
|
|
+- 小程序禁止可选链 `?.`(用 `&&` 替代)、禁止 CSS Grid、禁止 `:key` 表达式(用方法调用代替)
|
|
|
+- 所有迁移编号递增,当前最大编号见 `DatabaseInitializer.runMigrations()` 末尾搜索 `// 迁移`
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## File Map
|
|
|
+
|
|
|
+| 操作 | 文件 |
|
|
|
+|------|------|
|
|
|
+| 新增 | `cfc-backend/src/main/java/com/etotem/cfc/entity/ProductGiftConfig.java` |
|
|
|
+| 新增 | `cfc-backend/src/main/java/com/etotem/cfc/entity/ProductGiftItem.java` |
|
|
|
+| 新增 | `cfc-backend/src/main/java/com/etotem/cfc/entity/ProductOrderGift.java` |
|
|
|
+| 新增 | `cfc-backend/src/main/java/com/etotem/cfc/mapper/ProductGiftConfigMapper.java` |
|
|
|
+| 新增 | `cfc-backend/src/main/java/com/etotem/cfc/mapper/ProductGiftItemMapper.java` |
|
|
|
+| 新增 | `cfc-backend/src/main/java/com/etotem/cfc/mapper/ProductOrderGiftMapper.java` |
|
|
|
+| 新增 | `cfc-backend/src/main/java/com/etotem/cfc/service/ProductGiftService.java` |
|
|
|
+| 新增 | `cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminProductGiftController.java` |
|
|
|
+| 新增 | `cfc-backend/src/main/java/com/etotem/cfc/dto/ProductGiftItemDTO.java` |
|
|
|
+| 新增 | `cfc-backend/src/main/java/com/etotem/cfc/dto/GiftConfigVO.java` |
|
|
|
+| 修改 | `cfc-backend/src/main/java/com/etotem/cfc/dto/CreateProductOrderDTO.java` |
|
|
|
+| 修改 | `cfc-backend/src/main/java/com/etotem/cfc/dto/ProductDTO.java` |
|
|
|
+| 修改 | `cfc-backend/src/main/java/com/etotem/cfc/service/ProductService.java` |
|
|
|
+| 修改 | `cfc-backend/src/main/java/com/etotem/cfc/service/ProductOrderService.java` |
|
|
|
+| 修改 | `cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java` |
|
|
|
+| 修改 | `cfc-backend/src/main/resources/schema.sql` |
|
|
|
+| 修改 | `cfc-frontend/pages/discover-detail/product-detail/product-detail.vue` |
|
|
|
+| 修改 | `cfc-web/src/views/admin/ProductEdit.vue` |
|
|
|
+| 修改 | `cfc-web/src/api/admin.js` |
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Task 1: 数据库迁移(三张新表)
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Modify: `cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java`
|
|
|
+- Modify: `cfc-backend/src/main/resources/schema.sql`
|
|
|
+
|
|
|
+**Interfaces:**
|
|
|
+- Consumes: existing `ensureColumn` method pattern (line ~4099 in DatabaseInitializer)
|
|
|
+- Produces: three new tables available for use by subsequent tasks
|
|
|
+
|
|
|
+- [ ] **Step 1: 查找最新迁移编号**
|
|
|
+
|
|
|
+在 `DatabaseInitializer.java` 中搜索最后一个 `// 迁移` 注释:
|
|
|
+```bash
|
|
|
+grep -n "// 迁移" cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java | tail -5
|
|
|
+```
|
|
|
+记录最大编号 N(预计是 `迁移278` 或更高)。
|
|
|
+
|
|
|
+- [ ] **Step 2: 在 DatabaseInitializer 末尾添加三个建表迁移**
|
|
|
+
|
|
|
+在 `runMigrations()` 方法末尾(`}` 之前)追加:
|
|
|
+```java
|
|
|
+// 迁移N+1: 创建 product_gift_configs 表(商品赠品配置,商品×等级)
|
|
|
+try {
|
|
|
+ jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS product_gift_configs (" +
|
|
|
+ "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
|
|
|
+ "product_id BIGINT NOT NULL COMMENT '关联products表', " +
|
|
|
+ "level_code VARCHAR(50) NOT NULL COMMENT '会员等级编码(FAMILY/PREMIUM等)', " +
|
|
|
+ "status TINYINT(1) DEFAULT 1 COMMENT '1=启用 0=停用', " +
|
|
|
+ "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
|
|
|
+ "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
|
|
|
+ "UNIQUE KEY uk_product_level (product_id, level_code), " +
|
|
|
+ "INDEX idx_product_id (product_id), " +
|
|
|
+ "INDEX idx_level_code (level_code)" +
|
|
|
+ ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品赠品配置(商品×等级)'");
|
|
|
+ log.info("已创建product_gift_configs表");
|
|
|
+} catch (Exception e) {
|
|
|
+ // 表已存在,忽略
|
|
|
+}
|
|
|
+
|
|
|
+// 迁移N+2: 创建 product_gift_items 表(商品赠品项)
|
|
|
+try {
|
|
|
+ jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS product_gift_items (" +
|
|
|
+ "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
|
|
|
+ "config_id BIGINT NOT NULL COMMENT '关联product_gift_configs.id', " +
|
|
|
+ "gift_type VARCHAR(20) NOT NULL COMMENT 'product=赠品商品 cf=CF值', " +
|
|
|
+ "gift_product_id BIGINT COMMENT '赠品商品ID(gift_type=product时必填)', " +
|
|
|
+ "gift_product_name VARCHAR(200) COMMENT '赠品商品名快照', " +
|
|
|
+ "gift_product_image VARCHAR(500) COMMENT '赠品商品图快照', " +
|
|
|
+ "cf_value INT DEFAULT 0 COMMENT 'CF值数量(gift_type=cf时必填,单位分)', " +
|
|
|
+ "sort_order INT DEFAULT 0 COMMENT '排序(值越小越靠前)', " +
|
|
|
+ "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
|
|
|
+ "INDEX idx_config_id (config_id)" +
|
|
|
+ ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品赠品项'");
|
|
|
+ log.info("已创建product_gift_items表");
|
|
|
+} catch (Exception e) {
|
|
|
+ // 表已存在,忽略
|
|
|
+}
|
|
|
+
|
|
|
+// 迁移N+3: 创建 product_order_gifts 表(订单赠品记录)
|
|
|
+try {
|
|
|
+ jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS product_order_gifts (" +
|
|
|
+ "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
|
|
|
+ "order_id BIGINT NOT NULL COMMENT '关联product_orders.id', " +
|
|
|
+ "order_no VARCHAR(64) NOT NULL COMMENT '订单号快照', " +
|
|
|
+ "gift_type VARCHAR(20) NOT NULL COMMENT 'product=赠品商品 cf=CF值', " +
|
|
|
+ "gift_product_id BIGINT COMMENT '赠品商品ID', " +
|
|
|
+ "gift_product_name VARCHAR(200) COMMENT '赠品商品名快照', " +
|
|
|
+ "gift_product_image VARCHAR(500) COMMENT '赠品商品图快照', " +
|
|
|
+ "cf_value INT DEFAULT 0 COMMENT 'CF值数量(gift_type=cf时)', " +
|
|
|
+ "quantity INT DEFAULT 1 COMMENT '数量(默认1)', " +
|
|
|
+ "fulfilled TINYINT(1) DEFAULT 0 COMMENT '0=已选未发放 1=已发放', " +
|
|
|
+ "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
|
|
|
+ "INDEX idx_order_id (order_id), " +
|
|
|
+ "INDEX idx_order_no (order_no)" +
|
|
|
+ ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单赠品记录'");
|
|
|
+ log.info("已创建product_order_gifts表");
|
|
|
+} catch (Exception e) {
|
|
|
+ // 表已存在,忽略
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 3: 同步更新 schema.sql**
|
|
|
+
|
|
|
+在 `schema.sql` 中,找到 `product_gift_rules` 建表语句(约第4301行),在其**之后**追加以下三张表的建表语句(保持与迁移代码完全一致):
|
|
|
+```sql
|
|
|
+-- 商品赠品配置(商品×等级)
|
|
|
+CREATE TABLE IF NOT EXISTS product_gift_configs (
|
|
|
+ id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
|
|
+ product_id BIGINT NOT NULL COMMENT '关联products表',
|
|
|
+ level_code VARCHAR(50) NOT NULL COMMENT '会员等级编码(FAMILY/PREMIUM等)',
|
|
|
+ status TINYINT(1) DEFAULT 1 COMMENT '1=启用 0=停用',
|
|
|
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
|
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
|
|
+ UNIQUE KEY uk_product_level (product_id, level_code),
|
|
|
+ INDEX idx_product_id (product_id),
|
|
|
+ INDEX idx_level_code (level_code)
|
|
|
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品赠品配置(商品×等级)';
|
|
|
+
|
|
|
+-- 商品赠品项
|
|
|
+CREATE TABLE IF NOT EXISTS product_gift_items (
|
|
|
+ id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
|
|
+ config_id BIGINT NOT NULL COMMENT '关联product_gift_configs.id',
|
|
|
+ gift_type VARCHAR(20) NOT NULL COMMENT 'product=赠品商品 cf=CF值',
|
|
|
+ gift_product_id BIGINT COMMENT '赠品商品ID(gift_type=product时必填)',
|
|
|
+ gift_product_name VARCHAR(200) COMMENT '赠品商品名快照',
|
|
|
+ gift_product_image VARCHAR(500) COMMENT '赠品商品图快照',
|
|
|
+ cf_value INT DEFAULT 0 COMMENT 'CF值数量(gift_type=cf时必填,单位分)',
|
|
|
+ sort_order INT DEFAULT 0 COMMENT '排序(值越小越靠前)',
|
|
|
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
|
+ INDEX idx_config_id (config_id)
|
|
|
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品赠品项';
|
|
|
+
|
|
|
+-- 订单赠品记录
|
|
|
+CREATE TABLE IF NOT EXISTS product_order_gifts (
|
|
|
+ id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
|
|
+ order_id BIGINT NOT NULL COMMENT '关联product_orders.id',
|
|
|
+ order_no VARCHAR(64) NOT NULL COMMENT '订单号快照',
|
|
|
+ gift_type VARCHAR(20) NOT NULL COMMENT 'product=赠品商品 cf=CF值',
|
|
|
+ gift_product_id BIGINT COMMENT '赠品商品ID',
|
|
|
+ gift_product_name VARCHAR(200) COMMENT '赠品商品名快照',
|
|
|
+ gift_product_image VARCHAR(500) COMMENT '赠品商品图快照',
|
|
|
+ cf_value INT DEFAULT 0 COMMENT 'CF值数量(gift_type=cf时)',
|
|
|
+ quantity INT DEFAULT 1 COMMENT '数量(默认1)',
|
|
|
+ fulfilled TINYINT(1) DEFAULT 0 COMMENT '0=已选未发放 1=已发放',
|
|
|
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
|
+ INDEX idx_order_id (order_id),
|
|
|
+ INDEX idx_order_no (order_no)
|
|
|
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单赠品记录';
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 4: 编译验证**
|
|
|
+
|
|
|
+```bash
|
|
|
+cd cfc-backend && mvn clean compile
|
|
|
+```
|
|
|
+预期:BUILD SUCCESS,无编译错误。
|
|
|
+
|
|
|
+- [ ] **Step 5: 提交**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java
|
|
|
+git add cfc-backend/src/main/resources/schema.sql
|
|
|
+git commit -m "feat(product-gift): 建三张赠品相关表迁移(product_gift_configs/items/order_gifts)"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Task 2: 实体类与 Mapper 接口(三个实体 + 三个 Mapper)
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Create: `cfc-backend/src/main/java/com/etotem/cfc/entity/ProductGiftConfig.java`
|
|
|
+- Create: `cfc-backend/src/main/java/com/etotem/cfc/entity/ProductGiftItem.java`
|
|
|
+- Create: `cfc-backend/src/main/java/com/etotem/cfc/entity/ProductOrderGift.java`
|
|
|
+- Create: `cfc-backend/src/main/java/com/etotem/cfc/mapper/ProductGiftConfigMapper.java`
|
|
|
+- Create: `cfc-backend/src/main/java/com/etotem/cfc/mapper/ProductGiftItemMapper.java`
|
|
|
+- Create: `cfc-backend/src/main/java/com/etotem/cfc/mapper/ProductOrderGiftMapper.java`
|
|
|
+
|
|
|
+**Interfaces:**
|
|
|
+- Consumes: table names and column definitions from Task 1
|
|
|
+- Produces: three entity classes and three Mapper interfaces available for service layer injection
|
|
|
+
|
|
|
+- [ ] **Step 1: 创建 ProductGiftConfig.java**
|
|
|
+
|
|
|
+```java
|
|
|
+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;
|
|
|
+
|
|
|
+@Data
|
|
|
+@TableName("product_gift_configs")
|
|
|
+public class ProductGiftConfig implements Serializable {
|
|
|
+
|
|
|
+ @TableId(type = IdType.AUTO)
|
|
|
+ private Long id;
|
|
|
+
|
|
|
+ private Long productId;
|
|
|
+
|
|
|
+ private String levelCode;
|
|
|
+
|
|
|
+ private Integer status;
|
|
|
+
|
|
|
+ private Date createdAt;
|
|
|
+
|
|
|
+ private Date updatedAt;
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 2: 创建 ProductGiftItem.java**
|
|
|
+
|
|
|
+```java
|
|
|
+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;
|
|
|
+
|
|
|
+@Data
|
|
|
+@TableName("product_gift_items")
|
|
|
+public class ProductGiftItem implements Serializable {
|
|
|
+
|
|
|
+ @TableId(type = IdType.AUTO)
|
|
|
+ private Long id;
|
|
|
+
|
|
|
+ private Long configId;
|
|
|
+
|
|
|
+ private String giftType;
|
|
|
+
|
|
|
+ private Long giftProductId;
|
|
|
+
|
|
|
+ private String giftProductName;
|
|
|
+
|
|
|
+ private String giftProductImage;
|
|
|
+
|
|
|
+ private Integer cfValue;
|
|
|
+
|
|
|
+ private Integer sortOrder;
|
|
|
+
|
|
|
+ private Date createdAt;
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 3: 创建 ProductOrderGift.java**
|
|
|
+
|
|
|
+```java
|
|
|
+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;
|
|
|
+
|
|
|
+@Data
|
|
|
+@TableName("product_order_gifts")
|
|
|
+public class ProductOrderGift implements Serializable {
|
|
|
+
|
|
|
+ @TableId(type = IdType.AUTO)
|
|
|
+ private Long id;
|
|
|
+
|
|
|
+ private Long orderId;
|
|
|
+
|
|
|
+ private String orderNo;
|
|
|
+
|
|
|
+ private String giftType;
|
|
|
+
|
|
|
+ private Long giftProductId;
|
|
|
+
|
|
|
+ private String giftProductName;
|
|
|
+
|
|
|
+ private String giftProductImage;
|
|
|
+
|
|
|
+ private Integer cfValue;
|
|
|
+
|
|
|
+ private Integer quantity;
|
|
|
+
|
|
|
+ private Integer fulfilled;
|
|
|
+
|
|
|
+ private Date createdAt;
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 4: 创建三个 Mapper 接口(每个都与 ProductGiftRuleMapper 同构)**
|
|
|
+
|
|
|
+`ProductGiftConfigMapper.java`:
|
|
|
+```java
|
|
|
+package com.etotem.cfc.mapper;
|
|
|
+
|
|
|
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
|
|
+import com.etotem.cfc.entity.ProductGiftConfig;
|
|
|
+import org.apache.ibatis.annotations.Mapper;
|
|
|
+
|
|
|
+@Mapper
|
|
|
+public interface ProductGiftConfigMapper extends BaseMapper<ProductGiftConfig> {
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+`ProductGiftItemMapper.java`:
|
|
|
+```java
|
|
|
+package com.etotem.cfc.mapper;
|
|
|
+
|
|
|
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
|
|
+import com.etotem.cfc.entity.ProductGiftItem;
|
|
|
+import org.apache.ibatis.annotations.Mapper;
|
|
|
+
|
|
|
+@Mapper
|
|
|
+public interface ProductGiftItemMapper extends BaseMapper<ProductGiftItem> {
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+`ProductOrderGiftMapper.java`:
|
|
|
+```java
|
|
|
+package com.etotem.cfc.mapper;
|
|
|
+
|
|
|
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
|
|
+import com.etotem.cfc.entity.ProductOrderGift;
|
|
|
+import org.apache.ibatis.annotations.Mapper;
|
|
|
+
|
|
|
+@Mapper
|
|
|
+public interface ProductOrderGiftMapper extends BaseMapper<ProductOrderGift> {
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 5: 编译验证**
|
|
|
+
|
|
|
+```bash
|
|
|
+cd cfc-backend && mvn clean compile
|
|
|
+```
|
|
|
+预期:BUILD SUCCESS。
|
|
|
+
|
|
|
+- [ ] **Step 6: 提交**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add cfc-backend/src/main/java/com/etotem/cfc/entity/ProductGiftConfig.java
|
|
|
+git add cfc-backend/src/main/java/com/etotem/cfc/entity/ProductGiftItem.java
|
|
|
+git add cfc-backend/src/main/java/com/etotem/cfc/entity/ProductOrderGift.java
|
|
|
+git add cfc-backend/src/main/java/com/etotem/cfc/mapper/ProductGiftConfigMapper.java
|
|
|
+git add cfc-backend/src/main/java/com/etotem/cfc/mapper/ProductGiftItemMapper.java
|
|
|
+git add cfc-backend/src/main/java/com/etotem/cfc/mapper/ProductOrderGiftMapper.java
|
|
|
+git commit -m "feat(product-gift): 新增三张赠品表实体类与Mapper接口"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Task 3: ProductGiftService(配置 CRUD + 查询)
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Create: `cfc-backend/src/main/java/com/etotem/cfc/service/ProductGiftService.java`
|
|
|
+- Create: `cfc-backend/src/main/java/com/etotem/cfc/dto/ProductGiftItemDTO.java`
|
|
|
+- Create: `cfc-backend/src/main/java/com/etotem/cfc/dto/GiftConfigVO.java`
|
|
|
+- Modify: `cfc-backend/src/main/java/com/etotem/cfc/dto/ProductDTO.java`(在末尾新增 `giftConfigs` 字段及内嵌类)
|
|
|
+
|
|
|
+**Interfaces:**
|
|
|
+- Consumes: `ProductGiftConfigMapper`, `ProductGiftItemMapper`, `MembershipLevelMapper`, `ProductMapper`
|
|
|
+- Produces: `saveGiftConfig(productId, levelCode, status, items) → Result<Void>`
|
|
|
+ `listConfigsByProduct(productId) → List<GiftConfigVO>`
|
|
|
+ `getActiveItemById(itemId) → ProductGiftItem or null`
|
|
|
+ `getAvailableItems(productId, levelCode) → List<GiftConfigVO>`(按等级过滤且只返回启用配置)
|
|
|
+
|
|
|
+- [ ] **Step 1: 创建 ProductGiftItemDTO.java**
|
|
|
+
|
|
|
+```java
|
|
|
+package com.etotem.cfc.dto;
|
|
|
+
|
|
|
+import lombok.Data;
|
|
|
+
|
|
|
+@Data
|
|
|
+public class ProductGiftItemDTO {
|
|
|
+ private Long id; // null=新增,有值=更新
|
|
|
+ private String giftType; // "product" or "cf"
|
|
|
+ private Long giftProductId;
|
|
|
+ private Integer cfValue;
|
|
|
+ private Integer sortOrder;
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 2: 创建 GiftConfigVO.java**
|
|
|
+
|
|
|
+```java
|
|
|
+package com.etotem.cfc.dto;
|
|
|
+
|
|
|
+import lombok.Data;
|
|
|
+import java.util.List;
|
|
|
+
|
|
|
+@Data
|
|
|
+public class GiftConfigVO {
|
|
|
+ private Long id;
|
|
|
+ private String levelCode;
|
|
|
+ private String levelName;
|
|
|
+ private Integer status;
|
|
|
+ private List<GiftItemVO> items;
|
|
|
+
|
|
|
+ @Data
|
|
|
+ public static class GiftItemVO {
|
|
|
+ private Long id;
|
|
|
+ private String giftType;
|
|
|
+ private Long giftProductId;
|
|
|
+ private String giftProductName;
|
|
|
+ private String giftProductImage;
|
|
|
+ private Integer cfValue;
|
|
|
+ private Integer sortOrder;
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 3: 创建 ProductGiftService.java**
|
|
|
+
|
|
|
+```java
|
|
|
+package com.etotem.cfc.service;
|
|
|
+
|
|
|
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
|
|
+import com.etotem.cfc.common.Result;
|
|
|
+import com.etotem.cfc.dto.GiftConfigVO;
|
|
|
+import com.etotem.cfc.dto.ProductGiftItemDTO;
|
|
|
+import com.etotem.cfc.entity.MemberLevel;
|
|
|
+import com.etotem.cfc.entity.Product;
|
|
|
+import com.etotem.cfc.entity.ProductGiftConfig;
|
|
|
+import com.etotem.cfc.entity.ProductGiftItem;
|
|
|
+import com.etotem.cfc.mapper.MemberLevelMapper;
|
|
|
+import com.etotem.cfc.mapper.ProductGiftConfigMapper;
|
|
|
+import com.etotem.cfc.mapper.ProductGiftItemMapper;
|
|
|
+import com.etotem.cfc.mapper.ProductMapper;
|
|
|
+import org.springframework.stereotype.Service;
|
|
|
+import org.springframework.transaction.annotation.Transactional;
|
|
|
+
|
|
|
+import javax.annotation.Resource;
|
|
|
+import java.util.*;
|
|
|
+import java.util.stream.Collectors;
|
|
|
+
|
|
|
+@Service
|
|
|
+public class ProductGiftService {
|
|
|
+
|
|
|
+ @Resource
|
|
|
+ private ProductGiftConfigMapper configMapper;
|
|
|
+ @Resource
|
|
|
+ private ProductGiftItemMapper itemMapper;
|
|
|
+ @Resource
|
|
|
+ private ProductMapper productMapper;
|
|
|
+ @Resource
|
|
|
+ private MemberLevelMapper levelMapper;
|
|
|
+
|
|
|
+ /** 保存赠品配置(幂等:先删后插) */
|
|
|
+ @Transactional
|
|
|
+ public Result<Void> saveGiftConfig(Long productId, String levelCode, Integer status, List<ProductGiftItemDTO> items) {
|
|
|
+ if (productId == null || levelCode == null || status == null) {
|
|
|
+ return Result.error("参数不完整");
|
|
|
+ }
|
|
|
+ // 校验等级存在且非 FREE
|
|
|
+ MemberLevel level = levelMapper.selectOne(new LambdaQueryWrapper<MemberLevel>()
|
|
|
+ .eq(MemberLevel::getLevelCode, levelCode));
|
|
|
+ if (level == null || "FREE".equals(level.getLevelCode())) {
|
|
|
+ return Result.error("无效的赠送等级");
|
|
|
+ }
|
|
|
+ // 校验赠品商品(giftType=product)存在且启用
|
|
|
+ if (items != null) {
|
|
|
+ for (ProductGiftItemDTO it : items) {
|
|
|
+ if ("product".equals(it.getGiftType()) && it.getGiftProductId() != null) {
|
|
|
+ Product giftProduct = productMapper.selectById(it.getGiftProductId());
|
|
|
+ if (giftProduct == null || !"on_shelf".equals(giftProduct.getStatus())) {
|
|
|
+ return Result.error("赠品商品不存在或未上架");
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ // 先删后插(整个 config + 其下所有 items)
|
|
|
+ configMapper.delete(new LambdaQueryWrapper<ProductGiftConfig>()
|
|
|
+ .eq(ProductGiftConfig::getProductId, productId)
|
|
|
+ .eq(ProductGiftConfig::getLevelCode, levelCode));
|
|
|
+ if (status == 1) {
|
|
|
+ ProductGiftConfig config = new ProductGiftConfig();
|
|
|
+ config.setProductId(productId);
|
|
|
+ config.setLevelCode(levelCode);
|
|
|
+ config.setStatus(1);
|
|
|
+ config.setCreatedAt(new Date());
|
|
|
+ config.setUpdatedAt(new Date());
|
|
|
+ configMapper.insert(config);
|
|
|
+ if (items != null) {
|
|
|
+ for (ProductGiftItemDTO it : items) {
|
|
|
+ ProductGiftItem item = new ProductGiftItem();
|
|
|
+ item.setConfigId(config.getId());
|
|
|
+ item.setGiftType(it.getGiftType());
|
|
|
+ item.setGiftProductId(it.getGiftProductId());
|
|
|
+ item.setCfValue(it.getCfValue() != null ? it.getCfValue() : 0);
|
|
|
+ item.setSortOrder(it.getSortOrder() != null ? it.getSortOrder() : 0);
|
|
|
+ // 快照商品名称和图
|
|
|
+ if (it.getGiftProductId() != null) {
|
|
|
+ Product gp = productMapper.selectById(it.getGiftProductId());
|
|
|
+ if (gp != null) {
|
|
|
+ item.setGiftProductName(gp.getName());
|
|
|
+ item.setGiftProductImage(gp.getCoverImage());
|
|
|
+ }
|
|
|
+ }
|
|
|
+ item.setCreatedAt(new Date());
|
|
|
+ itemMapper.insert(item);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return Result.success(null);
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 查询商品全部等级赠品配置(管理端用) */
|
|
|
+ public List<GiftConfigVO> listConfigsByProduct(Long productId) {
|
|
|
+ List<ProductGiftConfig> configs = configMapper.selectList(
|
|
|
+ new LambdaQueryWrapper<ProductGiftConfig>()
|
|
|
+ .eq(ProductGiftConfig::getProductId, productId)
|
|
|
+ .orderByDesc(ProductGiftConfig::getStatus));
|
|
|
+ return toVOList(configs);
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 查询某商品某等级下启用的赠品项(含库存充足校验,供商品详情展示) */
|
|
|
+ public List<GiftConfigVO> getActiveConfigsByProductAndLevel(Long productId, String levelCode) {
|
|
|
+ ProductGiftConfig config = configMapper.selectOne(new LambdaQueryWrapper<ProductGiftConfig>()
|
|
|
+ .eq(ProductGiftConfig::getProductId, productId)
|
|
|
+ .eq(ProductGiftConfig::getLevelCode, levelCode)
|
|
|
+ .eq(ProductGiftConfig::getStatus, 1)
|
|
|
+ .last("LIMIT 1"));
|
|
|
+ if (config == null) {
|
|
|
+ return Collections.emptyList();
|
|
|
+ }
|
|
|
+ List<ProductGiftItem> items = itemMapper.selectList(
|
|
|
+ new LambdaQueryWrapper<ProductGiftItem>()
|
|
|
+ .eq(ProductGiftItem::getConfigId, config.getId())
|
|
|
+ .orderByAsc(ProductGiftItem::getSortOrder));
|
|
|
+ GiftConfigVO vo = new GiftConfigVO();
|
|
|
+ vo.setId(config.getId());
|
|
|
+ vo.setLevelCode(config.getLevelCode());
|
|
|
+ MemberLevel level = levelMapper.selectOne(new LambdaQueryWrapper<MemberLevel>()
|
|
|
+ .eq(MemberLevel::getLevelCode, levelCode));
|
|
|
+ vo.setLevelName(level != null ? level.getLevelName() : levelCode);
|
|
|
+ vo.setStatus(config.getStatus());
|
|
|
+ List<GiftConfigVO.GiftItemVO> itemVOs = new ArrayList<>();
|
|
|
+ for (ProductGiftItem it : items) {
|
|
|
+ GiftConfigVO.GiftItemVO itemVO = new GiftConfigVO.GiftItemVO();
|
|
|
+ itemVO.setId(it.getId());
|
|
|
+ itemVO.setGiftType(it.getGiftType());
|
|
|
+ itemVO.setGiftProductId(it.getGiftProductId());
|
|
|
+ itemVO.setGiftProductName(it.getGiftProductName());
|
|
|
+ itemVO.setGiftProductImage(it.getGiftProductImage());
|
|
|
+ itemVO.setCfValue(it.getCfValue() != null ? it.getCfValue() : 0);
|
|
|
+ itemVO.setSortOrder(it.getSortOrder());
|
|
|
+ // 库存充足校验:实物赠品必须库存>0
|
|
|
+ if ("product".equals(it.getGiftType()) && it.getGiftProductId() != null) {
|
|
|
+ Product gp = productMapper.selectById(it.getGiftProductId());
|
|
|
+ if (gp == null || gp.getStock() == null || gp.getStock() <= 0) {
|
|
|
+ continue; // 库存不足则不展示
|
|
|
+ }
|
|
|
+ }
|
|
|
+ itemVOs.add(itemVO);
|
|
|
+ }
|
|
|
+ vo.setItems(itemVOs);
|
|
|
+ return Collections.singletonList(vo);
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 根据 ID 查询赠品项(含 config 归属校验) */
|
|
|
+ public ProductGiftItem getActiveItemById(Long itemId) {
|
|
|
+ return itemMapper.selectById(itemId);
|
|
|
+ }
|
|
|
+
|
|
|
+ private List<GiftConfigVO> toVOList(List<ProductGiftConfig> configs) {
|
|
|
+ List<GiftConfigVO> result = new ArrayList<>();
|
|
|
+ for (ProductGiftConfig cfg : configs) {
|
|
|
+ GiftConfigVO vo = new GiftConfigVO();
|
|
|
+ vo.setId(cfg.getId());
|
|
|
+ vo.setLevelCode(cfg.getLevelCode());
|
|
|
+ MemberLevel level = levelMapper.selectOne(new LambdaQueryWrapper<MemberLevel>()
|
|
|
+ .eq(MemberLevel::getLevelCode, cfg.getLevelCode()));
|
|
|
+ vo.setLevelName(level != null ? level.getLevelName() : cfg.getLevelCode());
|
|
|
+ vo.setStatus(cfg.getStatus());
|
|
|
+ List<ProductGiftItem> items = itemMapper.selectList(
|
|
|
+ new LambdaQueryWrapper<ProductGiftItem>()
|
|
|
+ .eq(ProductGiftItem::getConfigId, cfg.getId())
|
|
|
+ .orderByAsc(ProductGiftItem::getSortOrder));
|
|
|
+ List<GiftConfigVO.GiftItemVO> itemVOs = new ArrayList<>();
|
|
|
+ for (ProductGiftItem it : items) {
|
|
|
+ GiftConfigVO.GiftItemVO itemVO = new GiftConfigVO.GiftItemVO();
|
|
|
+ itemVO.setId(it.getId());
|
|
|
+ itemVO.setGiftType(it.getGiftType());
|
|
|
+ itemVO.setGiftProductId(it.getGiftProductId());
|
|
|
+ itemVO.setGiftProductName(it.getGiftProductName());
|
|
|
+ itemVO.setGiftProductImage(it.getGiftProductImage());
|
|
|
+ itemVO.setCfValue(it.getCfValue() != null ? it.getCfValue() : 0);
|
|
|
+ itemVO.setSortOrder(it.getSortOrder());
|
|
|
+ itemVOs.add(itemVO);
|
|
|
+ }
|
|
|
+ vo.setItems(itemVOs);
|
|
|
+ result.add(vo);
|
|
|
+ }
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+注意:项目中会员等级实体名为 `MembershipLevel`(`entity/MembershipLevel.java`),不是 `MemberLevel`。请将上述代码中所有 `MemberLevel` 替换为 `MembershipLevel`,`MemberLevelMapper` 替换为 `MembershipLevelMapper`,字段 `levelCode` / `levelName` 对应 `getLevelCode()` / `getLevelName()`。
|
|
|
+
|
|
|
+- [ ] **Step 4: 编译验证**
|
|
|
+
|
|
|
+```bash
|
|
|
+cd cfc-backend && mvn clean compile
|
|
|
+```
|
|
|
+预期:BUILD SUCCESS。如有编译错误,根据错误信息调整字段名和 import。
|
|
|
+
|
|
|
+- [ ] **Step 5: 提交**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add cfc-backend/src/main/java/com/etotem/cfc/service/ProductGiftService.java
|
|
|
+git add cfc-backend/src/main/java/com/etotem/cfc/dto/ProductGiftItemDTO.java
|
|
|
+git add cfc-backend/src/main/java/com/etotem/cfc/dto/GiftConfigVO.java
|
|
|
+git commit -m "feat(product-gift): 新增 ProductGiftService(配置保存/查询逻辑)"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Task 4: Admin Controller(赠品配置管理接口)
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Create: `cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminProductGiftController.java`
|
|
|
+
|
|
|
+**Interfaces:**
|
|
|
+- Consumes: `ProductGiftService.saveGiftConfig`, `ProductGiftService.listConfigsByProduct`
|
|
|
+- Produces: `POST /api/admin/product-gift/save`, `POST /api/admin/product-gift/list`
|
|
|
+
|
|
|
+- [ ] **Step 1: 创建 AdminProductGiftController.java**
|
|
|
+
|
|
|
+```java
|
|
|
+package com.etotem.cfc.controller.admin;
|
|
|
+
|
|
|
+import com.etotem.cfc.common.Result;
|
|
|
+import com.etotem.cfc.dto.GiftConfigVO;
|
|
|
+import com.etotem.cfc.dto.ProductGiftItemDTO;
|
|
|
+import com.etotem.cfc.service.ProductGiftService;
|
|
|
+import org.springframework.web.bind.annotation.PostMapping;
|
|
|
+import org.springframework.web.bind.annotation.RequestBody;
|
|
|
+import org.springframework.web.bind.annotation.RequestMapping;
|
|
|
+import org.springframework.web.bind.annotation.RestController;
|
|
|
+
|
|
|
+import javax.annotation.Resource;
|
|
|
+import java.util.List;
|
|
|
+import java.util.Map;
|
|
|
+
|
|
|
+@RestController
|
|
|
+@RequestMapping("/api/admin/product-gift")
|
|
|
+public class AdminProductGiftController {
|
|
|
+
|
|
|
+ @Resource
|
|
|
+ private ProductGiftService giftService;
|
|
|
+
|
|
|
+ /** 保存某商品某等级的赠品配置(幂等:先删后插) */
|
|
|
+ @PostMapping("/save")
|
|
|
+ public Result<Void> save(@RequestBody Map<String, Object> params) {
|
|
|
+ Long productId = Long.parseLong(params.get("productId").toString());
|
|
|
+ String levelCode = params.get("levelCode").toString();
|
|
|
+ Integer status = Integer.parseInt(params.get("status").toString());
|
|
|
+ @SuppressWarnings("unchecked")
|
|
|
+ List<Map<String, Object>> itemsMap = (List<Map<String, Object>>) params.get("items");
|
|
|
+ List<ProductGiftItemDTO> items = null;
|
|
|
+ if (itemsMap != null) {
|
|
|
+ items = new java.util.ArrayList<>();
|
|
|
+ for (Map<String, Object> m : itemsMap) {
|
|
|
+ ProductGiftItemDTO dto = new ProductGiftItemDTO();
|
|
|
+ Object id = m.get("id");
|
|
|
+ dto.setId(id != null ? Long.parseLong(id.toString()) : null);
|
|
|
+ dto.setGiftType((String) m.get("giftType"));
|
|
|
+ Object gpId = m.get("giftProductId");
|
|
|
+ dto.setGiftProductId(gpId != null ? Long.parseLong(gpId.toString()) : null);
|
|
|
+ Object cf = m.get("cfValue");
|
|
|
+ dto.setCfValue(cf != null ? Integer.parseInt(cf.toString()) : 0);
|
|
|
+ Object sort = m.get("sortOrder");
|
|
|
+ dto.setSortOrder(sort != null ? Integer.parseInt(sort.toString()) : 0);
|
|
|
+ items.add(dto);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return giftService.saveGiftConfig(productId, levelCode, status, items);
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 查询某商品的全部等级赠品配置(管理端回显) */
|
|
|
+ @PostMapping("/list")
|
|
|
+ public Result<List<GiftConfigVO>> list(@RequestBody Map<String, Object> params) {
|
|
|
+ Long productId = Long.parseLong(params.get("productId").toString());
|
|
|
+ return Result.success(giftService.listConfigsByProduct(productId));
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 2: 编译验证**
|
|
|
+
|
|
|
+```bash
|
|
|
+cd cfc-backend && mvn clean compile
|
|
|
+```
|
|
|
+预期:BUILD SUCCESS。
|
|
|
+
|
|
|
+- [ ] **Step 3: 提交**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminProductGiftController.java
|
|
|
+git commit -m "feat(product-gift): 新增管理端赠品配置接口(/api/admin/product-gift/save + /list)"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Task 5: ProductOrderService 修改(create 校验赠品 + 写入订单赠品记录)
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Modify: `cfc-backend/src/main/java/com/etotem/cfc/dto/CreateProductOrderDTO.java`
|
|
|
+- Modify: `cfc-backend/src/main/java/com/etotem/cfc/service/ProductOrderService.java`
|
|
|
+- Modify: `cfc-backend/src/main/java/com/etotem/cfc/entity/ProductOrderGift.java`(已由 Task 2 创建,无需额外修改)
|
|
|
+
|
|
|
+**Interfaces:**
|
|
|
+- Consumes: `ProductGiftService.getActiveItemById`, `ProductGiftService.getActiveConfigsByProductAndLevel`
|
|
|
+- Consumes: `MembershipService.getMemberLevel(userId)` — 返回家庭创建者等级字符串
|
|
|
+- Consumes: `ProductOrderMapper.insert(order)` result (order.getId())
|
|
|
+- Produces: `product_order_gifts` 表中记录 `order_id`, `order_no`, `gift_type`, `gift_product_id`, `gift_product_name`, `gift_product_image`, `cf_value`, `fulfilled=0`
|
|
|
+
|
|
|
+- [ ] **Step 1: CreateProductOrderDTO 新增字段**
|
|
|
+
|
|
|
+在 `CreateProductOrderDTO.java` 末尾(`purchaseInfo` 字段之后)追加:
|
|
|
+```java
|
|
|
+ private List<Long> giftItemIds; // 选中的赠品项ID列表(来自 product_gift_items.id)
|
|
|
+
|
|
|
+ public List<Long> getGiftItemIds() { return giftItemIds; }
|
|
|
+ public void setGiftItemIds(List<Long> giftItemIds) { this.giftItemIds = giftItemIds; }
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 2: ProductOrderService 注入依赖**
|
|
|
+
|
|
|
+在 `ProductOrderService.java` 的 `@Resource` 字段区域,追加:
|
|
|
+```java
|
|
|
+ @Resource
|
|
|
+ private ProductGiftService productGiftService;
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 3: 在 create 方法末尾(insert 之后)追加赠品逻辑**
|
|
|
+
|
|
|
+在 `orderMapper.insert(order)` 之后(约第250行)、`return Result.success(...)` 之前,插入:
|
|
|
+```java
|
|
|
+ // ===== 赠品校验与记录 =====
|
|
|
+ if (dto.getGiftItemIds() != null && !dto.getGiftItemIds().isEmpty()) {
|
|
|
+ // 获取家庭创建者等级
|
|
|
+ Long familyId = order.getFamilyId();
|
|
|
+ String userLevel = null;
|
|
|
+ if (familyId != null) {
|
|
|
+ // 复用 MembershipService 中已有的 getMemberLevel 或 getCurrentLevel 逻辑
|
|
|
+ // 根据 MembershipService.getCurrentLevel(familyId) 返回的 MembershipLevelDTO.getLevelCode()
|
|
|
+ // 这里调用 membershipService.getCurrentLevel(familyId) 的 levelCode
|
|
|
+ userLevel = membershipService.getCurrentLevel(familyId) != null
|
|
|
+ ? membershipService.getCurrentLevel(familyId).getLevelCode()
|
|
|
+ : null;
|
|
|
+ }
|
|
|
+ if (userLevel != null && !"FREE".equals(userLevel)) {
|
|
|
+ // 查询该商品该等级下启用的赠品项(含库存校验)
|
|
|
+ List<GiftConfigVO> activeConfigs = productGiftService.getActiveConfigsByProductAndLevel(
|
|
|
+ product.getId(), userLevel);
|
|
|
+ if (!activeConfigs.isEmpty()) {
|
|
|
+ Set<Long> allowedItemIds = activeConfigs.stream()
|
|
|
+ .flatMap(c -> c.getItems().stream())
|
|
|
+ .map(GiftConfigVO.GiftItemVO::getId)
|
|
|
+ .collect(Collectors.toSet());
|
|
|
+ for (Long itemId : dto.getGiftItemIds()) {
|
|
|
+ if (!allowedItemIds.contains(itemId)) {
|
|
|
+ return Result.error("赠品项不存在或超出权限");
|
|
|
+ }
|
|
|
+ ProductGiftItem giftItem = productGiftService.getActiveItemById(itemId);
|
|
|
+ if (giftItem == null) {
|
|
|
+ return Result.error("赠品项不存在");
|
|
|
+ }
|
|
|
+ ProductOrderGift orderGift = new ProductOrderGift();
|
|
|
+ orderGift.setOrderId(order.getId());
|
|
|
+ orderGift.setOrderNo(order.getOrderNo());
|
|
|
+ orderGift.setGiftType(giftItem.getGiftType());
|
|
|
+ orderGift.setGiftProductId(giftItem.getGiftProductId());
|
|
|
+ orderGift.setGiftProductName(giftItem.getGiftProductName());
|
|
|
+ orderGift.setGiftProductImage(giftItem.getGiftProductImage());
|
|
|
+ orderGift.setCfValue(giftItem.getCfValue() != null ? giftItem.getCfValue() : 0);
|
|
|
+ orderGift.setQuantity(1);
|
|
|
+ orderGift.setFulfilled(0);
|
|
|
+ orderGift.setCreatedAt(new Date());
|
|
|
+ productOrderGiftMapper.insert(orderGift);
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ return Result.error("您所在等级暂无该商品赠品,请联系客服");
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ return Result.error("您暂无会员等级,无法享受赠品");
|
|
|
+ }
|
|
|
+ }
|
|
|
+```
|
|
|
+
|
|
|
+注意:需要在文件顶部 import 中追加:
|
|
|
+```java
|
|
|
+import com.etotem.cfc.dto.GiftConfigVO;
|
|
|
+import com.etotem.cfc.entity.ProductGiftItem;
|
|
|
+import com.etotem.cfc.entity.ProductOrderGift;
|
|
|
+import com.etotem.cfc.mapper.ProductOrderGiftMapper;
|
|
|
+import com.etotem.cfc.service.ProductGiftService;
|
|
|
+import com.etotem.cfc.service.MembershipService;
|
|
|
+import java.util.Set;
|
|
|
+```
|
|
|
+
|
|
|
+并在 `@Resource` 区域追加:
|
|
|
+```java
|
|
|
+ @Resource
|
|
|
+ private ProductOrderGiftMapper productOrderGiftMapper;
|
|
|
+ @Resource
|
|
|
+ private MembershipService membershipService;
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 4: 编译验证**
|
|
|
+
|
|
|
+```bash
|
|
|
+cd cfc-backend && mvn clean compile
|
|
|
+```
|
|
|
+根据编译错误修正字段名/方法名(`MembershipService.getCurrentLevel` 的返回类型需确认,如返回 `MembershipLevelDTO` 则字段名可能是 `getLevelCode()`,如返回 `MembershipLevel` 则直接调用 `getLevelCode()`)。
|
|
|
+
|
|
|
+- [ ] **Step 5: 提交**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add cfc-backend/src/main/java/com/etotem/cfc/dto/CreateProductOrderDTO.java
|
|
|
+git add cfc-backend/src/main/java/com/etotem/cfc/service/ProductOrderService.java
|
|
|
+git commit -m "feat(product-gift): create 流程注入赠品校验与 product_order_gifts 记录"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Task 6: ProductOrderService.handlePaymentSuccess 追加赠品发放
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Modify: `cfc-backend/src/main/java/com/etotem/cfc/service/ProductOrderService.java`
|
|
|
+
|
|
|
+**Interfaces:**
|
|
|
+- Consumes: `productOrderGiftMapper.selectList(fulfilled=0)`, `platformPointsService.earn()`, `productService.decreaseStock()`, `inventoryService.recordOutbound()`
|
|
|
+- Produces: `product_order_gifts.fulfilled=1` after successful fulfillment
|
|
|
+
|
|
|
+- [ ] **Step 1: 定位 handlePaymentSuccess 中现有赠品会员逻辑位置**
|
|
|
+
|
|
|
+在 `ProductOrderService.java` 中搜索:
|
|
|
+```java
|
|
|
+ProductGiftRule giftRule = productGiftRuleService.getActiveByProductId
|
|
|
+```
|
|
|
+该行约在第706行。在其**之后**(约第718行 `log.info("商品订单...")` 之后)插入赠品发放逻辑。
|
|
|
+
|
|
|
+- [ ] **Step 2: 插入赠品发放代码**
|
|
|
+
|
|
|
+```java
|
|
|
+ // ===== 发放订单赠品(CF值 + 实物库存扣减) =====
|
|
|
+ try {
|
|
|
+ List<ProductOrderGift> pendingGifts = productOrderGiftMapper.selectList(
|
|
|
+ new LambdaQueryWrapper<ProductOrderGift>()
|
|
|
+ .eq(ProductOrderGift::getOrderId, order.getId())
|
|
|
+ .eq(ProductOrderGift::getFulfilled, 0));
|
|
|
+ for (ProductOrderGift gift : pendingGifts) {
|
|
|
+ if ("cf".equals(gift.getGiftType()) && gift.getCfValue() != null && gift.getCfValue() > 0) {
|
|
|
+ platformPointsService.earn(order.getBuyerId(), gift.getCfValue(),
|
|
|
+ "product_gift", order.getId(),
|
|
|
+ "购买商品赠品: " + (gift.getGiftProductName() != null ? gift.getGiftProductName() : "CF值"));
|
|
|
+ } else if ("product".equals(gift.getGiftType()) && gift.getGiftProductId() != null) {
|
|
|
+ boolean stockOk = productService.decreaseStock(gift.getGiftProductId(), gift.getQuantity() != null ? gift.getQuantity() : 1);
|
|
|
+ if (!stockOk) {
|
|
|
+ log.error("赠品库存扣减失败: orderNo={}, giftProductId={}", orderNo, gift.getGiftProductId());
|
|
|
+ } else {
|
|
|
+ inventoryService.recordOutbound(gift.getGiftProductId(), null,
|
|
|
+ gift.getQuantity() != null ? gift.getQuantity() : 1,
|
|
|
+ "gift", order.getId(), null, "商品赠品:" + orderNo);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ gift.setFulfilled(1);
|
|
|
+ gift.setUpdatedAt(new Date());
|
|
|
+ productOrderGiftMapper.updateById(gift);
|
|
|
+ }
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("订单{}赠品发放异常: {}", orderNo, e.getMessage());
|
|
|
+ }
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 3: 编译验证**
|
|
|
+
|
|
|
+```bash
|
|
|
+cd cfc-backend && mvn clean compile
|
|
|
+```
|
|
|
+预期:BUILD SUCCESS。
|
|
|
+
|
|
|
+- [ ] **Step 4: 提交**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add cfc-backend/src/main/java/com/etotem/cfc/service/ProductOrderService.java
|
|
|
+git commit -m "feat(product-gift): handlePaymentSuccess 追加赠品发放逻辑"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Task 7: ProductService.detail 附带赠品配置
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Modify: `cfc-backend/src/main/java/com/etotem/cfc/service/ProductService.java`
|
|
|
+- Modify: `cfc-backend/src/main/java/com/etotem/cfc/dto/ProductDTO.java`
|
|
|
+
|
|
|
+**Interfaces:**
|
|
|
+- Consumes: `ProductGiftService.getActiveConfigsByProductAndLevel(productId, userLevel)`
|
|
|
+- Produces: `ProductDTO.giftConfigs` 字段(当前用户等级对应配置列表)
|
|
|
+
|
|
|
+- [ ] **Step 1: ProductDTO 新增 giftConfigs 字段**
|
|
|
+
|
|
|
+在 `ProductDTO.java` 末尾 `giftLevelName` 字段之后追加:
|
|
|
+```java
|
|
|
+ // 商品可选赠品配置(当前用户等级对应,空数组=无赠品)
|
|
|
+ private List<GiftConfigVO> giftConfigs;
|
|
|
+
|
|
|
+ // 内嵌 GiftConfigVO(与 dto/GiftConfigVO 同构,避免额外 import)
|
|
|
+ @Data
|
|
|
+ public static class GiftConfigVO {
|
|
|
+ private Long id;
|
|
|
+ private String levelCode;
|
|
|
+ private String levelName;
|
|
|
+ private Integer status;
|
|
|
+ private List<GiftItemVO> items;
|
|
|
+
|
|
|
+ @Data
|
|
|
+ public static class GiftItemVO {
|
|
|
+ private Long id;
|
|
|
+ private String giftType;
|
|
|
+ private Long giftProductId;
|
|
|
+ private String giftProductName;
|
|
|
+ private String giftProductImage;
|
|
|
+ private Integer cfValue;
|
|
|
+ private Integer sortOrder;
|
|
|
+ }
|
|
|
+ }
|
|
|
+```
|
|
|
+
|
|
|
+注意:`@Data` 注解来自 `lombok.Data`,`ProductDTO` 已有 `import lombok.Data`,无需新增 import。
|
|
|
+
|
|
|
+- [ ] **Step 2: ProductService.detail 组装赠品配置**
|
|
|
+
|
|
|
+在 `ProductService.java` 的 `detail` 方法中,现有赠品会员逻辑(约第163行 `ProductGiftRule giftRule = ...` 块)**之后**插入:
|
|
|
+```java
|
|
|
+ // 可选赠品配置(当前用户等级)
|
|
|
+ try {
|
|
|
+ Long currentUserId = ...; // 从 JWT 上下文获取,参考现有 detail 方法签名
|
|
|
+ // detail(Long id) 无 userId 参数,需改为 detail(Long id, Long userId)
|
|
|
+ // 参考现有调用方式:ProductController.detail(@PathVariable 或 @RequestParam)
|
|
|
+ // 实际调用链:ProductController.detail(@PathVariable Long id) → 调用 productMapper.selectById 等
|
|
|
+ // 改为从 @RequestAttribute("userId") 获取当前用户
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.debug("获取赠品配置失败: {}", e.getMessage());
|
|
|
+ }
|
|
|
+```
|
|
|
+
|
|
|
+**重要:`ProductService.detail(Long id)` 目前无 userId 参数。** 需要:
|
|
|
+1. 修改 `ProductService.detail` 方法签名:`public Result<ProductDTO> detail(Long id, Long userId)`
|
|
|
+2. 修改 `ProductController.detail` 调用:从 `@RequestAttribute("userId") Long userId` 获取
|
|
|
+3. 在 detail 方法中组装赠品配置:
|
|
|
+
|
|
|
+```java
|
|
|
+ if (userId != null) {
|
|
|
+ // 获取用户家庭创建者等级
|
|
|
+ Long familyId = userMapper.selectById(userId).getFamilyId();
|
|
|
+ String userLevel = null;
|
|
|
+ if (familyId != null) {
|
|
|
+ MembershipLevelDTO currentLevel = membershipService.getCurrentLevel(familyId);
|
|
|
+ if (currentLevel != null) {
|
|
|
+ userLevel = currentLevel.getLevelCode();
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (userLevel != null && !"FREE".equals(userLevel)) {
|
|
|
+ List<GiftConfigVO> activeConfigs = productGiftService.getActiveConfigsByProductAndLevel(id, userLevel);
|
|
|
+ dto.setGiftConfigs(activeConfigs);
|
|
|
+ }
|
|
|
+ }
|
|
|
+```
|
|
|
+
|
|
|
+需要先确认 `MembershipService.getCurrentLevel` 返回类型。查看现有代码:
|
|
|
+```bash
|
|
|
+grep -n "getCurrentLevel\|MembershipLevelDTO" cfc-backend/src/main/java/com/etotem/cfc/service/MembershipService.java | head -10
|
|
|
+```
|
|
|
+若返回 `MembershipLevelDTO`,则字段为 `getLevelCode()`;若返回 `MembershipLevel`,则直接 `getLevelCode()`。
|
|
|
+
|
|
|
+- [ ] **Step 3: 修改 ProductController.detail 传入 userId**
|
|
|
+
|
|
|
+查看 `ProductController.java` 的 detail 方法,将:
|
|
|
+```java
|
|
|
+public Result<ProductDTO> detail(@PathVariable Long id)
|
|
|
+```
|
|
|
+改为:
|
|
|
+```java
|
|
|
+public Result<ProductDTO> detail(@RequestParam("id") Long id,
|
|
|
+ @RequestAttribute(value = "userId", required = false) Long userId)
|
|
|
+```
|
|
|
+并在方法体中传递 `userId` 给 `productService.detail(id, userId)`。
|
|
|
+
|
|
|
+- [ ] **Step 4: 编译验证**
|
|
|
+
|
|
|
+```bash
|
|
|
+cd cfc-backend && mvn clean compile
|
|
|
+```
|
|
|
+预期:BUILD SUCCESS。
|
|
|
+
|
|
|
+- [ ] **Step 5: 提交**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add cfc-backend/src/main/java/com/etotem/cfc/service/ProductService.java
|
|
|
+git add cfc-backend/src/main/java/com/etotem/cfc/controller/product/ProductController.java
|
|
|
+git add cfc-backend/src/main/java/com/etotem/cfc/dto/ProductDTO.java
|
|
|
+git commit -m "feat(product-gift): 商品详情附带当前用户等级赠品配置"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Task 8: 小程序前端 — 商品详情页展示赠品选择
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Modify: `cfc-frontend/pages/discover-detail/product-detail/product-detail.vue`
|
|
|
+- Modify: `cfc-frontend/utils/api.js`(或对应 API 封装文件)
|
|
|
+
|
|
|
+**Interfaces:**
|
|
|
+- Consumes: `POST /api/product/detail`(现有接口,已返回 `giftConfigs`)
|
|
|
+- Produces: 商品详情页展示"购买即送"区块,支持多选赠品,提交订单时透传 `giftItemIds`
|
|
|
+
|
|
|
+- [ ] **Step 1: 确认 API 封装位置**
|
|
|
+
|
|
|
+搜索现有商品详情 API 调用:
|
|
|
+```bash
|
|
|
+grep -rn "product/detail\|product-detail\|api.product" cfc-frontend/pages/discover-detail/ | head -5
|
|
|
+```
|
|
|
+找到对应的 API 封装文件(通常在 `utils/api.js` 或页面内联)。
|
|
|
+
|
|
|
+- [ ] **Step 2: 在商品详情页添加赠品区块**
|
|
|
+
|
|
|
+在 `product-detail.vue` 中,价格区下方(`<view class="price">` 块之后)插入:
|
|
|
+```vue
|
|
|
+ <!-- 赠品选择区 -->
|
|
|
+ <view class="gift-section" v-if="product.giftConfigs && product.giftConfigs.length > 0">
|
|
|
+ <view class="gift-title">购买即送</view>
|
|
|
+ <view class="gift-items">
|
|
|
+ <view
|
|
|
+ class="gift-item"
|
|
|
+ :class="{ selected: selectedGiftIds.indexOf(item.id) > -1 }"
|
|
|
+ v-for="config in product.giftConfigs"
|
|
|
+ v-for="item in config.items"
|
|
|
+ :key="getItemKey(item)"
|
|
|
+ @click="toggleGift(item)"
|
|
|
+ >
|
|
|
+ <image
|
|
|
+ v-if="item.giftType === 'product' && item.giftProductImage"
|
|
|
+ :src="item.giftProductImage"
|
|
|
+ class="gift-image"
|
|
|
+ mode="aspectFill"
|
|
|
+ />
|
|
|
+ <view v-else class="gift-cf-icon">💎</view>
|
|
|
+ <text class="gift-name">{{ item.giftType === 'product' ? item.giftProductName : item.cfValue + ' CF值' }}</text>
|
|
|
+ <view class="gift-check" v-if="selectedGiftIds.indexOf(item.id) > -1">✓</view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ <view class="gift-footer" v-if="selectedGiftIds.length > 0">
|
|
|
+ 已选 {{ selectedGiftIds.length }} 件赠品
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+```
|
|
|
+
|
|
|
+注意:小程序禁止 `:key` 表达式,需改用方法调用:
|
|
|
+```vue
|
|
|
+:key="getItemKey(item)"
|
|
|
+```
|
|
|
+并在 `methods` 中添加:
|
|
|
+```js
|
|
|
+getItemKey: function(item) {
|
|
|
+ return item.id;
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 3: 添加 data 和 methods**
|
|
|
+
|
|
|
+在 `data()` 中追加:
|
|
|
+```js
|
|
|
+selectedGiftIds: []
|
|
|
+```
|
|
|
+
|
|
|
+在 `methods` 中追加:
|
|
|
+```js
|
|
|
+toggleGift: function(item) {
|
|
|
+ var ids = this.selectedGiftIds;
|
|
|
+ var idx = ids.indexOf(item.id);
|
|
|
+ if (idx > -1) {
|
|
|
+ ids.splice(idx, 1);
|
|
|
+ } else {
|
|
|
+ ids.push(item.id);
|
|
|
+ }
|
|
|
+},
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 4: 下单时透传 giftItemIds**
|
|
|
+
|
|
|
+找到提交订单的函数(通常在"立即购买"或"加入购物车"按钮的点击事件中),在构建下单参数时追加:
|
|
|
+```js
|
|
|
+var params = {
|
|
|
+ productId: this.productId,
|
|
|
+ quantity: this.quantity,
|
|
|
+ // ... 其他字段
|
|
|
+ giftItemIds: this.selectedGiftIds.length > 0 ? this.selectedGiftIds : undefined
|
|
|
+};
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 5: 编译验证**
|
|
|
+
|
|
|
+```bash
|
|
|
+cd cfc-frontend && npm run build:mp-weixin
|
|
|
+```
|
|
|
+预期:构建成功,无语法错误。
|
|
|
+
|
|
|
+- [ ] **Step 6: 提交**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add cfc-frontend/pages/discover-detail/product-detail/product-detail.vue
|
|
|
+git commit -m "feat(product-gift): 商品详情页展示赠品选择区,下单透传 giftItemIds"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Task 9: Web 管理端 — 商品编辑页新增赠品配置区块
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Modify: `cfc-web/src/views/admin/ProductEdit.vue`
|
|
|
+- Modify: `cfc-web/src/api/admin.js`
|
|
|
+
|
|
|
+**Interfaces:**
|
|
|
+- Consumes: `POST /api/admin/product-gift/save`,`POST /api/admin/product-gift/list`,`GET /api/membership/levels`(或复用现有接口)
|
|
|
+- Produces: 商品编辑页"赠品配置"区块,支持按等级配置赠品项
|
|
|
+
|
|
|
+- [ ] **Step 1: 在 admin.js 中新增 API 封装**
|
|
|
+
|
|
|
+在 `cfc-web/src/api/admin.js` 末尾追加:
|
|
|
+```js
|
|
|
+// ===== 商品赠品配置 =====
|
|
|
+export const saveProductGift = (data) => request('/api/admin/product-gift/save', 'POST', data)
|
|
|
+export const getProductGiftList = (data) => request('/api/admin/product-gift/list', 'POST', data)
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 2: ProductEdit.vue 新增赠品配置区块**
|
|
|
+
|
|
|
+在 `ProductEdit.vue` 中,找到商品基础信息区块末尾,追加:
|
|
|
+```vue
|
|
|
+ <!-- 赠品配置区块 -->
|
|
|
+ <el-divider content-position="left">赠品配置</el-divider>
|
|
|
+ <el-form-item label="赠品">
|
|
|
+ <el-table :data="giftConfigs" border size="mini">
|
|
|
+ <el-table-column label="会员等级" width="120">
|
|
|
+ <template slot-scope="scope">
|
|
|
+ <span>{{ scope.row.levelName }}</span>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="赠品项">
|
|
|
+ <template slot-scope="scope">
|
|
|
+ <div v-for="item in scope.row.items" :key="item.id" style="margin-bottom:4px">
|
|
|
+ <span v-if="item.giftType === 'product'">{{ item.giftProductName }}</span>
|
|
|
+ <span v-else>{{ item.cfValue }} CF值</span>
|
|
|
+ </div>
|
|
|
+ <span v-if="!scope.row.items || scope.row.items.length === 0">未配置</span>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ </el-table>
|
|
|
+ <el-button size="mini" @click="showGiftDialog = true" style="margin-top:8px">
|
|
|
+ 配置赠品
|
|
|
+ </el-button>
|
|
|
+ </el-form-item>
|
|
|
+
|
|
|
+ <!-- 赠品配置弹窗 -->
|
|
|
+ <el-dialog title="配置赠品" :visible.sync="showGiftDialog" width="700px">
|
|
|
+ <el-tabs v-model="activeGiftTab">
|
|
|
+ <el-tab-pane
|
|
|
+ v-for="level in membershipLevels"
|
|
|
+ :key="level.levelCode"
|
|
|
+ :label="level.levelName"
|
|
|
+ :name="level.levelCode"
|
|
|
+ >
|
|
|
+ <div v-for="(item, index) in getGiftItems(level.levelCode)" :key="item.id || index" style="margin-bottom:8px; display:flex; align-items:center">
|
|
|
+ <el-select v-model="item.giftType" size="small" style="width:100px; margin-right:8px">
|
|
|
+ <el-option label="赠品商品" value="product" />
|
|
|
+ <el-option label="CF值" value="cf" />
|
|
|
+ </el-select>
|
|
|
+ <el-select v-if="item.giftType === 'product'" v-model="item.giftProductId" size="small" placeholder="选择商品" style="flex:1; margin-right:8px">
|
|
|
+ <el-option v-for="p in onShelfProducts" :key="p.id" :label="p.name" :value="p.id" />
|
|
|
+ </el-select>
|
|
|
+ <el-input v-if="item.giftType === 'cf'" v-model.number="item.cfValue" size="small" placeholder="CF值数量" style="width:120px; margin-right:8px" type="number" />
|
|
|
+ <el-button size="small" type="text" @click="removeGiftItem(level.levelCode, index)">删除</el-button>
|
|
|
+ </div>
|
|
|
+ <el-button size="small" @click="addGiftItem(level.levelCode)">+ 添加赠品</el-button>
|
|
|
+ </el-tab-pane>
|
|
|
+ </el-tabs>
|
|
|
+ <div slot="footer">
|
|
|
+ <el-button size="small" @click="showGiftDialog = false">取消</el-button>
|
|
|
+ <el-button size="small" type="primary" @click="saveGiftConfig">保存</el-button>
|
|
|
+ </div>
|
|
|
+ </el-dialog>
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 3: 添加 data 字段和 methods**
|
|
|
+
|
|
|
+在 `data()` 中追加:
|
|
|
+```js
|
|
|
+showGiftDialog: false,
|
|
|
+activeGiftTab: '',
|
|
|
+membershipLevels: [], // 从 /api/membership/levels 获取,过滤 FREE
|
|
|
+giftConfigs: [], // 已配置的赠品(加载商品详情时获取)
|
|
|
+giftItemsMap: {}, // { [levelCode]: [{ giftType, giftProductId, cfValue, sortOrder }] }
|
|
|
+onShelfProducts: [], // 已上架商品列表(用于赠品商品选择)
|
|
|
+```
|
|
|
+
|
|
|
+在 `methods` 中追加:
|
|
|
+```js
|
|
|
+async loadGiftConfigs() {
|
|
|
+ if (!this.productId) return
|
|
|
+ const res = await getProductGiftList({ productId: this.productId })
|
|
|
+ if (res.code === 200) {
|
|
|
+ this.giftConfigs = res.data || []
|
|
|
+ this.giftItemsMap = {}
|
|
|
+ for (const cfg of this.giftConfigs) {
|
|
|
+ this.giftItemsMap[cfg.levelCode] = cfg.items || []
|
|
|
+ }
|
|
|
+ }
|
|
|
+},
|
|
|
+async loadMembershipLevels() {
|
|
|
+ // 复用现有会员等级接口,过滤 FREE
|
|
|
+ const res = await getMembershipLevels()
|
|
|
+ if (res.code === 200) {
|
|
|
+ this.membershipLevels = (res.data || []).filter(l => l.levelCode !== 'FREE')
|
|
|
+ if (this.membershipLevels.length > 0 && !this.activeGiftTab) {
|
|
|
+ this.activeGiftTab = this.membershipLevels[0].levelCode
|
|
|
+ }
|
|
|
+ }
|
|
|
+},
|
|
|
+async loadOnShelfProducts() {
|
|
|
+ const res = await getProductList({ status: 'on_shelf', size: 200 })
|
|
|
+ if (res.code === 200) {
|
|
|
+ this.onShelfProducts = res.data.records || res.data || []
|
|
|
+ }
|
|
|
+},
|
|
|
+getGiftItems(levelCode) {
|
|
|
+ return this.giftItemsMap[levelCode] || []
|
|
|
+},
|
|
|
+addGiftItem(levelCode) {
|
|
|
+ if (!this.giftItemsMap[levelCode]) {
|
|
|
+ this.giftItemsMap[levelCode] = []
|
|
|
+ }
|
|
|
+ this.giftItemsMap[levelCode].push({ giftType: 'product', giftProductId: null, cfValue: 0, sortOrder: 0 })
|
|
|
+},
|
|
|
+removeGiftItem(levelCode, index) {
|
|
|
+ this.giftItemsMap[levelCode].splice(index, 1)
|
|
|
+},
|
|
|
+async saveGiftConfig() {
|
|
|
+ if (!this.productId) return
|
|
|
+ for (const level of this.membershipLevels) {
|
|
|
+ const items = this.giftItemsMap[level.levelCode] || []
|
|
|
+ const itemsDTO = items.map((it, idx) => ({
|
|
|
+ giftType: it.giftType,
|
|
|
+ giftProductId: it.giftType === 'product' ? it.giftProductId : null,
|
|
|
+ cfValue: it.giftType === 'cf' ? it.cfValue : 0,
|
|
|
+ sortOrder: idx
|
|
|
+ }))
|
|
|
+ const res = await saveProductGift({
|
|
|
+ productId: this.productId,
|
|
|
+ levelCode: level.levelCode,
|
|
|
+ status: 1,
|
|
|
+ items: itemsDTO
|
|
|
+ })
|
|
|
+ if (res.code !== 200) {
|
|
|
+ this.$message.error('保存' + level.levelName + '赠品失败: ' + res.message)
|
|
|
+ return
|
|
|
+ }
|
|
|
+ }
|
|
|
+ this.$message.success('赠品配置已保存')
|
|
|
+ this.showGiftDialog = false
|
|
|
+ await this.loadGiftConfigs()
|
|
|
+},
|
|
|
+```
|
|
|
+
|
|
|
+在 `mounted` 或商品加载回调中调用:
|
|
|
+```js
|
|
|
+this.loadMembershipLevels()
|
|
|
+this.loadOnShelfProducts()
|
|
|
+// 当 productId 变化时(编辑已有商品):
|
|
|
+this.loadGiftConfigs()
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 4: 编译验证**
|
|
|
+
|
|
|
+```bash
|
|
|
+cd cfc-web && npm run build
|
|
|
+```
|
|
|
+预期:构建成功,无语法错误。
|
|
|
+
|
|
|
+- [ ] **Step 5: 提交**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add cfc-web/src/views/admin/ProductEdit.vue
|
|
|
+git add cfc-web/src/api/admin.js
|
|
|
+git commit -m "feat(product-gift): 管理端商品编辑页新增赠品配置区块"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## Self-Review Checklist
|
|
|
+
|
|
|
+| 检查项 | 结论 |
|
|
|
+|--------|------|
|
|
|
+| 三张新表 DDL 完整 | ✅ Task 1 已覆盖 |
|
|
|
+| 实体类字段与 DDL 对齐 | ✅ Task 2 已覆盖 |
|
|
|
+| 赠品配置保存幂等(先删后插) | ✅ Task 3 `saveGiftConfig` 已实现 |
|
|
|
+| 管理端 save + list 接口 | ✅ Task 4 已覆盖 |
|
|
|
+| 下单时赠品校验(防越权) | ✅ Task 5 已覆盖 |
|
|
|
+| 支付成功后 CF 发放 + 实物库存扣减 | ✅ Task 6 已覆盖 |
|
|
|
+| 商品详情返回赠品配置 | ✅ Task 7 已覆盖 |
|
|
|
+| 小程序赠品选择 UI | ✅ Task 8 已覆盖 |
|
|
|
+| 管理端赠品配置 UI | ✅ Task 9 已覆盖 |
|
|
|
+| 不修改现有 `product_gift_rules` | ✅ 独立新表,不触碰旧逻辑 |
|
|
|
+| 赠品商品不受等级限制 | ✅ 仅校验商品 `status=on_shelf`,不校验 `memberEligible` |
|
|
|
+| CF 赠品发给购买者本人 | ✅ `platformPointsService.earn(buyerId, ...)` |
|