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: 将 SKU 从"规格变体"改为"关联商品选择器"——商品 A 的 SKU 可指向另一个独立商品 B,用户在 A 页面选择规格后查看 B 的信息并购买 B。
Architecture: product_skus 表新增 linked_product_id 和 label 两列。新模式 SKU(linked_product_id IS NOT NULL)的价格/库存/图片从 linked_product 读取,下单时直接购买 linked_product。旧模式 SKU(linked_product_id IS NULL)保持原规格变体行为。管理端新增商品选择器组件,SKU 编辑弹窗按模式切换表单。
Tech Stack: Java 8 + Spring Boot 2.7.18 + MyBatis-Plus / uni-app Vue 2 小程序 / Vue 2 + Element UI Web管理端
@PostMapping,禁止 @GetMapping/@PutMapping/@DeleteMappingResult<T>(code/message/data)@RequestAttribute("userId") 获取用户 ID@Resource,字段名匹配 Bean NameDatabaseInitializer.runMigrations()ensureColumn 或异常忽略)schema.sql 必须同步更新(ADD COLUMN 后同步到 CREATE TABLE 语句)?.(用 &&)、禁止 CSS Grid(用 flexbox)、禁止 :key 表达式(用方法调用)、禁止 new Date(string)(用 parseDate())linked_product_id = NULL,保持原行为,不迁移Files:
cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java (runMigrations() 末尾,迁移247)cfc-backend/src/main/java/com/etotem/cfc/entity/ProductSku.java (新增 linkedProductId、label)cfc-backend/src/main/resources/schema.sql (同步 product_skus CREATE TABLE 新增两列)Interfaces:
Produces: ProductSku.linkedProductId (Long, nullable), ProductSku.label (String, nullable)
[ ] Step 1: 修改实体 ProductSku.java
在 private Integer enabled; 之后、private Date createTime; 之前插入:
private Long linkedProductId; // 关联商品ID,null=Legacy SKU
private String label; // 自定义显示标签,null=使用linked_product.name
搜索当前最大迁移编号(在文件末尾搜索 // 迁移),在最新迁移块之后、方法结束 } 之前插入:
// 迁移247: product_skus 添加 linked_product_id 和 label 列(SKU关联商品选择器)
ensureColumn("product_skus", "linked_product_id", "BIGINT COMMENT '关联商品ID,null=Legacy SKU'");
ensureColumn("product_skus", "label", "VARCHAR(100) COMMENT '自定义显示标签,null=使用linked_product.name'");
搜索 CREATE TABLE IF NOT EXISTS product_skus,在 enabled 列定义之后、create_time 之前插入:
linked_product_id BIGINT COMMENT '关联商品ID,null=Legacy SKU',
label VARCHAR(100) COMMENT '自定义显示标签,null=使用linked_product.name',
[ ] Step 4: 编译验证
cd cfc-backend && mvn clean compile -q
Expected: BUILD SUCCESS
[ ] Step 5: 提交
git add cfc-backend/src/main/java/com/etotem/cfc/entity/ProductSku.java cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java cfc-backend/src/main/resources/schema.sql
git commit -m "feat(backend): ProductSku 新增 linkedProductId/label 字段,迁移247"
Files:
cfc-backend/src/main/java/com/etotem/cfc/service/ProductSkuService.java (create/update 方法)Interfaces:
ProductSku.linkedProductId, ProductSku.labelProduces: 兼容创建/更新 linked SKU(不校验 specs/price/stock 等旧字段)
[ ] Step 1: 修改 create 方法
当前 create() 方法中,if (sku.getSpecs() == null || sku.getSpecs().trim().isEmpty()) { sku.setSpecs("[]"); } 改为:
sku.getLinkedProductId() != null,跳过 specs 默认值设置(linked SKU 不需要 specs)如果 sku.getSkuCode() == null || sku.getSkuCode().isEmpty(),仍然生成 SKU 码
@Transactional
public ProductSku create(ProductSku sku) {
log.info("[SKU-CREATE] productId={}, linkedProductId={}", sku.getProductId(), sku.getLinkedProductId());
if (sku.getLinkedProductId() == null) {
// Legacy SKU: 需要 specs
if (sku.getSpecs() == null || sku.getSpecs().trim().isEmpty()) {
sku.setSpecs("[]");
}
}
sku.setCreateTime(new Date());
sku.setUpdateTime(new Date());
if (sku.getEnabled() == null) {
sku.setEnabled(1);
}
if (sku.getSkuCode() == null || sku.getSkuCode().isEmpty()) {
sku.setSkuCode("SKU-" + sku.getProductId() + "-" + System.currentTimeMillis());
}
skuMapper.insert(sku);
return sku;
}
[ ] Step 2: 修改 update 方法
同理,update() 中 if (sku.getSpecs() == null || sku.getSpecs().trim().isEmpty()) 改为只在 linkedProductId 为空时设置默认 specs:
@Transactional
public void update(ProductSku sku) {
if (sku.getLinkedProductId() == null) {
if (sku.getSpecs() == null || sku.getSpecs().trim().isEmpty()) {
sku.setSpecs("[]");
}
}
sku.setUpdateTime(new Date());
skuMapper.updateById(sku);
}
[ ] Step 3: 编译验证
cd cfc-backend && mvn clean compile -q
Expected: BUILD SUCCESS
[ ] Step 4: 提交
git add cfc-backend/src/main/java/com/etotem/cfc/service/ProductSkuService.java
git commit -m "feat(backend): ProductSkuService 创建/更新支持 linked SKU,跳过 specs 校验"
Files:
cfc-backend/src/main/java/com/etotem/cfc/controller/product/ProductController.java (specMap 方法)cfc-backend/src/main/java/com/etotem/cfc/service/ProductSkuService.java (新增 listLinked 方法)Interfaces:
ProductSku.linkedProductIdProduces: specMap 返回的 option 中增加 linkedProductId、linked(name/price/stock/image/brief)、isLinked
[ ] Step 1: 在 ProductSkuService 新增查询 linked 商品摘要的方法
/**
* 查询 linked 商品摘要(用于 specMap 扩展)
*/
public Map<String, Object> getLinkedProductSummary(Long linkedProductId) {
if (linkedProductId == null) return null;
Product product = productMapper.selectById(linkedProductId);
if (product == null) return null;
if (product.getEnabled() == null || product.getEnabled() == 0) return null;
if (product.getDeleted() != null && product.getDeleted() == 1) return null;
if (product.getStock() != null && product.getStock() == 0) return null;
Map<String, Object> summary = new HashMap<>();
summary.put("id", product.getId());
summary.put("name", product.getName());
summary.put("price", product.getPrice());
summary.put("stock", product.getStock());
summary.put("image", product.getCoverImage());
summary.put("brief", product.getIntro());
return summary;
}
需要在 ProductSkuService 注入 ProductMapper:
@Resource
private ProductMapper productMapper;
在循环构建 option 对象时,对 linked SKU 扩展返回字段。在 for (ProductSku sku : skus) 循环内,构建 option 前增加:
boolean isLinked = sku.getLinkedProductId() != null;
// Linked SKU: 跳过库存为0/已删除的关联商品
if (isLinked) {
Map<String, Object> linkedSummary = productSkuService.getLinkedProductSummary(sku.getLinkedProductId());
if (linkedSummary == null) continue; // 库存为0或已下架,不显示
}
在 option 构建时增加:
if (isLinked) {
option.put("linkedProductId", sku.getLinkedProductId());
option.put("isLinked", true);
Map<String, Object> linkedSummary = productSkuService.getLinkedProductSummary(sku.getLinkedProductId());
option.put("linked", linkedSummary);
// Linked SKU 的 option name 用 label 或 linked product name
String label = sku.getLabel();
if (label == null || label.trim().isEmpty()) {
label = (String) linkedSummary.get("name");
}
option.put("name", label);
} else {
option.put("isLinked", false);
}
注意:option.put("name", value) 当前直接取 specEntry 的 value。对于 linked SKU 需要覆盖 name 为 label/linked.name。
[ ] Step 3: 编译验证
cd cfc-backend && mvn clean compile -q
Expected: BUILD SUCCESS
[ ] Step 4: 提交
git add cfc-backend/src/main/java/com/etotem/cfc/controller/product/ProductController.java cfc-backend/src/main/java/com/etotem/cfc/service/ProductSkuService.java
git commit -m "feat(backend): specMap 扩展返回 linked 商品摘要,库存为0/已下架不显示"
Files:
cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminProductPickerController.javacfc-backend/src/main/java/com/etotem/cfc/service/ProductService.java (新增 listForPicker 方法)Interfaces:
Produces: POST /api/admin/product/linked/list — 分页 + 分类筛选 + 关键词搜索 + 排除当前商品 + 排除已关联商品
[ ] Step 1: 在 ProductService 新增 listForPicker 方法
/**
* 管理端关联商品选择器列表
* @param excludeProductId 排除当前商品ID
* @param categoryId 分类筛选,null=全部
* @param keyword 关键词搜索,null=不搜索
* @param page 页码
* @param pageSize 每页大小
* @return Map with list/total/page/pageSize
*/
public Map<String, Object> listForPicker(Long excludeProductId, Long categoryId, String keyword,
Integer page, Integer pageSize) {
if (page == null || page < 1) page = 1;
if (pageSize == null || pageSize < 1) pageSize = 20;
int offset = (page - 1) * pageSize;
List<Product> list = productMapper.selectForPicker(excludeProductId, categoryId, keyword, offset, pageSize);
Long total = productMapper.countForPicker(excludeProductId, categoryId, keyword);
Map<String, Object> result = new HashMap<>();
result.put("list", list);
result.put("total", total);
result.put("page", page);
result.put("pageSize", pageSize);
return result;
}
[ ] Step 2: 在 ProductMapper 新增查询方法
<!-- ProductMapper.xml -->
<select id="selectForPicker" resultType="com.etotem.cfc.entity.Product">
SELECT id, name, price, stock, cover_image AS coverImage, category_id AS categoryId
FROM products
WHERE deleted = 0 AND enabled = 1
<if test="excludeProductId != null">
AND id != #{excludeProductId}
</if>
<if test="categoryId != null">
AND category_id = #{categoryId}
</if>
<if test="keyword != null and keyword != ''">
AND name LIKE CONCAT('%', #{keyword}, '%')
</if>
ORDER BY sort_order, id DESC
LIMIT #{pageSize} OFFSET #{offset}
</select>
<select id="countForPicker" resultType="long">
SELECT COUNT(*) FROM products
WHERE deleted = 0 AND enabled = 1
<if test="excludeProductId != null">
AND id != #{excludeProductId}
</if>
<if test="categoryId != null">
AND category_id = #{categoryId}
</if>
<if test="keyword != null and keyword != ''">
AND name LIKE CONCAT('%', #{keyword}, '%')
</if>
</select>
[ ] Step 3: 创建 AdminProductPickerController
package com.etotem.cfc.controller.admin;
import com.etotem.cfc.common.Result;
import com.etotem.cfc.service.ProductService;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import java.util.Map;
@RestController
@RequestMapping("/api/admin/product")
public class AdminProductPickerController {
@Resource
private ProductService productService;
@PostMapping("/linked/list")
public Result<Map<String, Object>> linkedList(@RequestBody Map<String, Object> params) {
Long productId = params.get("productId") != null ? Long.valueOf(params.get("productId").toString()) : null;
Long categoryId = params.get("categoryId") != null ? Long.valueOf(params.get("categoryId").toString()) : null;
String keyword = (String) params.get("keyword");
Integer page = params.get("page") != null ? Integer.valueOf(params.get("page").toString()) : 1;
Integer pageSize = params.get("pageSize") != null ? Integer.valueOf(params.get("pageSize").toString()) : 20;
// 同分类限制:只返回同一分类的商品
if (categoryId == null && productId != null) {
// 如果未传 categoryId,从当前商品获取
com.etotem.cfc.entity.Product product = productService.getById(productId);
if (product != null) {
categoryId = product.getCategoryId();
}
}
Map<String, Object> result = productService.listForPicker(productId, categoryId, keyword, page, pageSize);
return Result.success(result);
}
}
[ ] Step 4: 编译验证
cd cfc-backend && mvn clean compile -q
Expected: BUILD SUCCESS
[ ] Step 5: 提交
git add cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminProductPickerController.java cfc-backend/src/main/java/com/etotem/cfc/service/ProductService.java
git commit -m "feat(backend): 管理端商品选择器 API /api/admin/product/linked/list"
Files:
cfc-backend/src/main/java/com/etotem/cfc/service/CartService.java (buildCartItemDTO linked 处理)cfc-backend/src/main/java/com/etotem/cfc/service/ProductOrderService.java (createMultiItem linked 处理)Interfaces:
ProductSku.linkedProductIdProduces: 用户选 linked SKU 时,加入购物车/下单的 productId = linked_product_id
[ ] Step 1: 修改 CartService.buildCartItemDTO
buildCartItemDTO 方法在已有 sku 查询块内,追加 linked 转发逻辑:
if (cart.getSkuId() != null) {
ProductSku sku = productSkuService.getById(cart.getSkuId());
if (sku != null) {
// Linked SKU: 转发到 linked 商品
if (sku.getLinkedProductId() != null) {
Product linkedProduct = productService.getById(sku.getLinkedProductId());
if (linkedProduct != null) {
dto.setProductId(linkedProduct.getId());
dto.setProductName(linkedProduct.getName());
dto.setUnitPrice(linkedProduct.getPrice() != null ? linkedProduct.getPrice() : 0);
// 不覆盖 specs 解析(linked SKU 无 specs)
}
} else {
// Legacy SKU 逻辑保持原样
if (sku.getPrice() != null) {
dto.setUnitPrice(sku.getPrice());
}
if (sku.getSpecs() != null) {
// ... 现有 specs 解析逻辑 ...
}
}
}
}
注意:当前 CartService.java 的 buildCartItemDTO 方法在之前的 Task 3 中已修改为 if (sku.getPrice() != null) { dto.setUnitPrice(sku.getPrice()); }。需要将这段改为 linked-aware 的版本。
在 createMultiItem 方法中,解析 productData 时,如果传了 skuId,查询 SKU 的 linkedProductId:
// 如果传了 skuId,检查是否为 linked SKU
if (skuId != null) {
ProductSku sku = productSkuService.getById(skuId);
if (sku != null && sku.getLinkedProductId() != null) {
Product linkedProduct = productService.getById(sku.getLinkedProductId());
if (linkedProduct != null) {
productId = linkedProduct.getId();
productName = linkedProduct.getName();
unitPrice = linkedProduct.getPrice() != null ? linkedProduct.getPrice() : 0;
coverImage = linkedProduct.getCoverImage();
}
}
}
[ ] Step 3: 编译验证
cd cfc-backend && mvn clean compile -q
Expected: BUILD SUCCESS
[ ] Step 4: 提交
git add cfc-backend/src/main/java/com/etotem/cfc/service/CartService.java cfc-backend/src/main/java/com/etotem/cfc/service/ProductOrderService.java
git commit -m "feat(backend): 购物车/下单 linked SKU 转发到关联商品"
Files:
cfc-web/src/views/admin/components/ProductPicker.vuecfc-web/src/views/admin/components/ProductSkuEditor.vuecfc-web/src/api/admin.js (新增 linkedProductList API)Interfaces:
POST /api/admin/product/linked/listProduces: 管理端可创建/编辑 linked SKU(选择商品 + 自定义标签)
[ ] Step 1: 在 admin.js 新增 linkedProductList API
export function linkedProductList(params) {
return request({
url: '/api/admin/product/linked/list',
method: 'post',
data: params
})
}
[ ] Step 2: 创建 ProductPicker.vue 商品选择器组件
<template>
<el-dialog :visible.sync="visible" title="选择关联商品" width="600px" @close="handleClose">
<el-form :inline="true" size="small">
<el-form-item label="分类">
<el-select v-model="query.categoryId" placeholder="全部" clearable @change="search">
<el-option v-for="cat in categories" :key="cat.id" :label="cat.name" :value="cat.id" />
</el-select>
</el-form-item>
<el-form-item label="搜索">
<el-input v-model="query.keyword" placeholder="商品名称" clearable @keyup.enter.native="search" />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="search">搜索</el-button>
</el-form-item>
</el-form>
<el-table :data="list" highlight-current-row @current-change="onSelect">
<el-table-column width="50">
<template slot-scope="{ row }">
<el-radio :value="selectedId" :label="row.id" @change="onSelect(row)"> </el-radio>
</template>
</el-table-column>
<el-table-column label="商品" width="380">
<template slot-scope="{ row }">
<div style="display:flex;align-items:center;gap:10px;">
<img v-if="row.coverImage" :src="row.coverImage" style="width:50px;height:50px;object-fit:cover;border-radius:4px;" />
<span v-else style="width:50px;height:50px;background:#f0f0f0;display:inline-block;border-radius:4px;"></span>
<div>
<div>{{ row.name }}</div>
<div style="font-size:12px;color:#999;">¥{{ row.price ? (row.price/100).toFixed(2) : '0.00' }}</div>
</div>
</div>
</template>
</el-table-column>
<el-table-column prop="stock" label="库存" width="80" />
</el-table>
<el-pagination
v-if="total > pageSize"
:current-page="query.page"
:page-size="pageSize"
:total="total"
layout="prev, pager, next"
@current-change="onPageChange"
/>
<span slot="footer">
<el-button @click="visible = false">取消</el-button>
<el-button type="primary" :disabled="!selectedProduct" @click="confirm">确认选择</el-button>
</span>
</el-dialog>
</template>
<script>
import { linkedProductList } from '@/api/admin'
export default {
name: 'ProductPicker',
props: {
productId: { type: Number, default: null },
categoryId: { type: Number, default: null }
},
data() {
return {
visible: false,
list: [],
total: 0,
pageSize: 20,
query: { keyword: '', categoryId: null, page: 1 },
selectedProduct: null,
categories: []
}
},
computed: {
selectedId() { return this.selectedProduct ? this.selectedProduct.id : null }
},
methods: {
open() {
this.visible = true
this.query.categoryId = this.categoryId
this.search()
},
async search() {
this.query.page = 1
await this.load()
},
async load() {
try {
const res = await linkedProductList({
productId: this.productId,
categoryId: this.query.categoryId,
keyword: this.query.keyword,
page: this.query.page,
pageSize: this.pageSize
})
if (res.code === 200) {
this.list = res.data.list || []
this.total = res.data.total || 0
}
} catch (e) {
this.$message.error('加载商品列表失败')
}
},
onSelect(row) {
this.selectedProduct = row
},
confirm() {
this.$emit('selected', this.selectedProduct)
this.visible = false
},
onPageChange(page) {
this.query.page = page
this.load()
},
handleClose() {
this.selectedProduct = null
}
}
}
</script>
[ ] Step 3: 改造 ProductSkuEditor.vue
在 ProductSkuEditor.vue 中:
a) 引入 ProductPicker 组件:
import ProductPicker from './ProductPicker.vue'
export default {
components: { ProductPicker },
// ...
}
b) data 中新增 linked SKU 相关字段:
data() {
return {
// ... 现有字段
skuForm: {
// ... 现有字段
label: '',
linkedProductId: null,
linkedProductName: '',
linkedProductPrice: 0,
linkedProductImage: '',
linkedProductStock: 0
},
isLinkedMode: false
}
}
c) 弹窗中新增模式切换(在顶部添加):
<el-radio-group v-model="isLinkedMode" size="small" style="margin-bottom:12px;">
<el-radio-button :label="false">规格变体</el-radio-button>
<el-radio-button :label="true">关联商品</el-radio-button>
</el-radio-group>
d) 关联商品模式表单(isLinkedMode=true 时显示):
<template v-if="isLinkedMode">
<el-form-item label="显示标签">
<el-input v-model="skuForm.label" placeholder="默认使用关联商品名称" />
</el-form-item>
<el-form-item label="关联商品">
<el-button @click="openProductPicker">选择商品</el-button>
<div v-if="skuForm.linkedProductName" style="margin-top:8px;display:flex;align-items:center;gap:10px;padding:8px;background:#f5f7fa;border-radius:4px;">
<img v-if="skuForm.linkedProductImage" :src="skuForm.linkedProductImage" style="width:50px;height:50px;object-fit:cover;border-radius:4px;" />
<div>
<div>{{ skuForm.linkedProductName }}</div>
<div style="font-size:12px;color:#999;">¥{{ skuForm.linkedProductPrice ? (skuForm.linkedProductPrice/100).toFixed(2) : '0.00' }} | 库存 {{ skuForm.linkedProductStock }}</div>
</div>
<el-button size="mini" type="text" @click="clearLinkedProduct">更换</el-button>
</div>
</el-form-item>
</template>
e) 规格变体模式保持原表单(isLinkedMode=false 时显示,隐藏 price/stock/ specs 等字段)
f) handleSave 方法中,根据 isLinkedMode 构建 payload:
if (this.isLinkedMode) {
payload.linkedProductId = this.skuForm.linkedProductId
payload.label = this.skuForm.label || null
} else {
payload.specs = this.normalizeSpecsToJson(this.skuForm.specs)
payload.price = Math.round(parseFloat(this.skuForm.price) * 100)
// ... 其他旧字段
}
g) 新增 openProductPicker 方法:
openProductPicker() {
this.$refs.productPicker.open()
},
onProductSelected(product) {
this.skuForm.linkedProductId = product.id
this.skuForm.linkedProductName = product.name
this.skuForm.linkedProductPrice = product.price
this.skuForm.linkedProductImage = product.image || product.coverImage
this.skuForm.linkedProductStock = product.stock
}
[ ] Step 4: 提交
git add cfc-web/src/views/admin/components/ProductPicker.vue cfc-web/src/views/admin/components/ProductSkuEditor.vue cfc-web/src/api/admin.js
git commit -m "feat(admin): ProductSkuEditor 双模式(规格变体/关联商品)+ ProductPicker 组件"
Files:
cfc-frontend/pages/discover-detail/product-detail/product-detail.vueInterfaces:
specMap 返回的 linked 字段(name/price/stock/image/brief)Produces: 用户选中 linked SKU 时展示简介卡片,下单时 productId = linked.id
[ ] Step 1: data 中新增 linked 相关数据
data() {
return {
// ... 现有字段
linkedProductMap: {}, // { skuId: { name, price, stock, image, brief } }
selectedLinkedProduct: null // 当前选中的 linked 商品
}
}
[ ] Step 2: 在收到 specMap 数据后构建 linkedProductMap
在 loadProduct 或 loadSpec 方法中,处理 specMap 返回后:
// 构建 linked 商品映射
var linkedMap = {}
if (specGroups && specGroups.length > 0) {
for (var i = 0; i < specGroups.length; i++) {
var options = specGroups[i].options || []
for (var j = 0; j < options.length; j++) {
var opt = options[j]
if (opt.isLinked && opt.linked) {
linkedMap[opt.skuId] = opt.linked
}
}
}
}
this.linkedProductMap = linkedMap
在 selectSpec 方法中,匹配到 SKU 后:
// 检查是否为 linked SKU
var sku = this.matchSku() // 或从 selectedSpecs 推导
var linked = this.linkedProductMap[matchedSkuId]
if (linked) {
this.selectedLinkedProduct = linked
this.actualPrice = linked.price
// 显示 linked 商品简介
} else {
this.selectedLinkedProduct = null
// 恢复为父商品价格
}
在规格选择区下方、购买按钮上方插入:
<!-- Linked 商品简介卡片 -->
<view class="linked-product-card" v-if="selectedLinkedProduct">
<view class="linked-card-inner" @click="goLinkedProduct(selectedLinkedProduct.id)">
<image class="linked-image" :src="selectedLinkedProduct.image" mode="aspectFill" />
<view class="linked-info">
<text class="linked-name">{{ selectedLinkedProduct.name }}</text>
<text class="linked-price">¥{{ formatPrice(selectedLinkedProduct.price) }}</text>
<text class="linked-stock">库存 {{ selectedLinkedProduct.stock }}</text>
<text class="linked-brief" v-if="selectedLinkedProduct.brief">{{ selectedLinkedProduct.brief }}</text>
</view>
<view class="linked-arrow">›</view>
</view>
</view>
[ ] Step 5: 修改 onBuy 和 onAddToCart——选择 linked SKU 时买 linked 商品
onBuy: function() {
var userId = uni.getStorageSync('userId')
if (!userId) {
uni.navigateTo({ url: '/pages/login/login' })
return
}
var productId = this.product.id
var productName = this.product.name
var price = this.actualPrice
var coverImage = this.product.coverImage
var skuId = this.selectedSku ? this.selectedSku.id : null
// Linked SKU: 转发到 linked 商品
if (this.selectedLinkedProduct) {
productId = this.selectedLinkedProduct.id
productName = this.selectedLinkedProduct.name
price = this.selectedLinkedProduct.price
coverImage = this.selectedLinkedProduct.image
}
var productData = {
productId: productId,
productName: productName,
coverImage: coverImage,
// ...
}
// 如果有关联 skuId,传递
if (skuId) {
productData.skuId = skuId
}
// ...
}
[ ] Step 6: 添加 goLinkedProduct 方法
goLinkedProduct: function(productId) {
uni.navigateTo({
url: '/pages/discover-detail/product-detail/product-detail?id=' + productId
})
}
[ ] Step 7: 验证小程序约束
确保:
?. 可选链:key 表达式(用方法调用)无 new Date(string)(用 parseDate())
[ ] Step 8: 提交
git add cfc-frontend/pages/discover-detail/product-detail/product-detail.vue
git commit -m "feat(frontend): 详情页 linked SKU 简介卡片展示 + 购买转发到关联商品"
Files:
验证项:
[ ] 后端编译
cd cfc-backend && mvn clean compile -q
Expected: BUILD SUCCESS
[ ] 迁移幂等验证
检查 DatabaseInitializer.java 中迁移247 使用 ensureColumn 方法,自动处理重复列异常。
[ ] schema.sql 同步检查
确认 product_skus 的 CREATE TABLE 中包含 linked_product_id 和 label 列。
[ ] 管理端路由检查
检查新路由 /api/admin/product/linked/list 是否与其他 Controller 重复:
grep -rn '@Mapping' cfc-backend/src/main/java/com/etotem/cfc/controller/ | grep -oP '@\w+Mapping\("\K[^"]*' | sort -u | grep linked
Expected: 只有一条匹配
[ ] Bean 名冲突检查
确认 AdminProductPickerController 类名不与其他 Controller 重名。
[ ] 提交
git add -A
git commit -m "chore: 端到端验证 — SKU 关联商品选择器全功能"