# 商品可选赠品功能 实施计划 > **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`(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 { } ``` `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 { } ``` `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 { } ``` - [ ] **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` `listConfigsByProduct(productId) → List` `getActiveItemById(itemId) → ProductGiftItem or null` `getAvailableItems(productId, levelCode) → List`(按等级过滤且只返回启用配置) - [ ] **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 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 saveGiftConfig(Long productId, String levelCode, Integer status, List items) { if (productId == null || levelCode == null || status == null) { return Result.error("参数不完整"); } // 校验等级存在且非 FREE MemberLevel level = levelMapper.selectOne(new LambdaQueryWrapper() .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() .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 listConfigsByProduct(Long productId) { List configs = configMapper.selectList( new LambdaQueryWrapper() .eq(ProductGiftConfig::getProductId, productId) .orderByDesc(ProductGiftConfig::getStatus)); return toVOList(configs); } /** 查询某商品某等级下启用的赠品项(含库存充足校验,供商品详情展示) */ public List getActiveConfigsByProductAndLevel(Long productId, String levelCode) { ProductGiftConfig config = configMapper.selectOne(new LambdaQueryWrapper() .eq(ProductGiftConfig::getProductId, productId) .eq(ProductGiftConfig::getLevelCode, levelCode) .eq(ProductGiftConfig::getStatus, 1) .last("LIMIT 1")); if (config == null) { return Collections.emptyList(); } List items = itemMapper.selectList( new LambdaQueryWrapper() .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() .eq(MemberLevel::getLevelCode, levelCode)); vo.setLevelName(level != null ? level.getLevelName() : levelCode); vo.setStatus(config.getStatus()); List 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 toVOList(List configs) { List 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() .eq(MemberLevel::getLevelCode, cfg.getLevelCode())); vo.setLevelName(level != null ? level.getLevelName() : cfg.getLevelCode()); vo.setStatus(cfg.getStatus()); List items = itemMapper.selectList( new LambdaQueryWrapper() .eq(ProductGiftItem::getConfigId, cfg.getId()) .orderByAsc(ProductGiftItem::getSortOrder)); List 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 save(@RequestBody Map 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> itemsMap = (List>) params.get("items"); List items = null; if (itemsMap != null) { items = new java.util.ArrayList<>(); for (Map 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(@RequestBody Map 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 giftItemIds; // 选中的赠品项ID列表(来自 product_gift_items.id) public List getGiftItemIds() { return giftItemIds; } public void setGiftItemIds(List 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 activeConfigs = productGiftService.getActiveConfigsByProductAndLevel( product.getId(), userLevel); if (!activeConfigs.isEmpty()) { Set 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 pendingGifts = productOrderGiftMapper.selectList( new LambdaQueryWrapper() .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 giftConfigs; // 内嵌 GiftConfigVO(与 dto/GiftConfigVO 同构,避免额外 import) @Data public static class GiftConfigVO { private Long id; private String levelCode; private String levelName; private Integer status; private List 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 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 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 detail(@PathVariable Long id) ``` 改为: ```java public Result 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` 中,价格区下方(`` 块之后)插入: ```vue 购买即送 💎 {{ item.giftType === 'product' ? item.giftProductName : item.cfValue + ' CF值' }} ✓ 已选 {{ selectedGiftIds.length }} 件赠品 ``` 注意:小程序禁止 `: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 赠品配置 配置赠品
删除
+ 添加赠品
取消 保存
``` - [ ] **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, ...)` |