|
@@ -0,0 +1,160 @@
|
|
|
|
|
+# Story 006.009: 父子商品名称关联查询优化(为购物车显示做准备)
|
|
|
|
|
+
|
|
|
|
|
+## Status
|
|
|
|
|
+Draft
|
|
|
|
|
+
|
|
|
|
|
+## Story
|
|
|
|
|
+**As a** 购物车用户,
|
|
|
|
|
+**I want** 父子商品名称通过关联查询准确获取,
|
|
|
|
|
+**so that** 购物车中能显示正确的父商品名称,避免数据不一致问题
|
|
|
|
|
+
|
|
|
|
|
+## Acceptance Criteria
|
|
|
|
|
+1. 商品详情API返回的 `parent` 对象包含完整的父商品基本信息(id、name等)
|
|
|
|
|
+2. 购物车页面能正确通过 `goods.parent?.name` 获取父商品名称(为故事10做准备)
|
|
|
|
|
+3. API不再返回 `spuName` 字段,前端代码直接使用 `parent.name` 获取父商品名称
|
|
|
|
|
+4. 父子商品名称显示准确,无数据不一致问题
|
|
|
|
|
+5. 所有测试通过,无回归问题
|
|
|
|
|
+6. 故事10能顺利基于故事9的实现完成购物车商品名称显示优化
|
|
|
|
|
+
|
|
|
|
|
+## Tasks / Subtasks
|
|
|
|
|
+- [ ] 任务1:更新商品Schema,完善父子商品类型定义 (AC: 1, 3)
|
|
|
|
|
+ - [ ] 创建父商品精简Schema(`ParentGoodsSchema`),包含基本字段:id、name、price、costPrice、stock、imageFileId、goodsType
|
|
|
|
|
+ - [ ] 更新`PublicGoodsSchema`中的`parent`字段类型:从`z.any()`改为`ParentGoodsSchema.nullable().optional()`
|
|
|
|
|
+ - [ ] 更新`AdminGoodsSchema`中的`parent`字段类型:从`z.any()`改为`ParentGoodsSchema.nullable().optional()`
|
|
|
|
|
+ - [ ] 更新`PublicGoodsSchema`中的`children`字段类型:从`z.array(z.any())`改为`z.array(PublicGoodsSchema).nullable().optional()`
|
|
|
|
|
+ - [ ] 从`PublicGoodsSchema`、`AdminGoodsSchema`、`UserGoodsSchema`中移除`spuName`字段(保留实体中的字段,仅从Schema中移除)
|
|
|
|
|
+ - [ ] 更新`UpdateGoodsDto`,移除`spuName`字段(API不再接受`spuName`字段更新)
|
|
|
|
|
+- [ ] 任务2:完善商品服务`GoodsServiceMt.getById`方法 (AC: 1)
|
|
|
|
|
+ - [ ] 确保`parent`对象包含完整的父商品基本信息:id、name、price、costPrice、stock、imageFileId、goodsType、spuId(0)
|
|
|
|
|
+ - [ ] 添加租户ID过滤,确保父商品与子商品在同一租户下(已实现,需要验证)
|
|
|
|
|
+ - [ ] 优化`parent`对象字段选择,确保包含所有必要字段
|
|
|
|
|
+ - [ ] 确保`children`列表返回完整的子商品信息(包含所有Schema字段)
|
|
|
|
|
+- [ ] 任务3:验证购物车页面父子商品名称获取 (AC: 2)
|
|
|
|
|
+ - [ ] 检查购物车页面(`mini/src/pages/cart/index.tsx`)当前商品名称显示逻辑
|
|
|
|
|
+ - [ ] 确认`goodsMap`中的商品数据包含`parent`对象
|
|
|
|
|
+ - [ ] 验证购物车页面能通过`latestGoods?.parent?.name`获取父商品名称
|
|
|
|
|
+ - [ ] 无需修改购物车页面代码(故事10将修改显示逻辑),仅确保数据基础可用
|
|
|
|
|
+- [ ] 任务4:编写和更新测试 (AC: 5)
|
|
|
|
|
+ - [ ] 更新商品服务单元测试,验证`getById`方法返回正确的`parent`对象
|
|
|
|
|
+ - [ ] 添加集成测试,验证商品详情API返回的`parent`对象包含完整字段
|
|
|
|
|
+ - [ ] 验证API不再返回`spuName`字段
|
|
|
|
|
+ - [ ] 测试父子商品关联查询的准确性
|
|
|
|
|
+ - [ ] 确保所有现有测试通过,无回归问题
|
|
|
|
|
+- [ ] 任务5:验证多租户兼容性和向后兼容性 (AC: 4, 5)
|
|
|
|
|
+ - [ ] 验证父子商品在同一租户下的约束
|
|
|
|
|
+ - [ ] 确保现有功能不受影响(单规格商品、无父子关系的商品)
|
|
|
|
|
+ - [ ] 验证数据库实体中的`spuName`字段仍然存在(保持向后兼容性),仅从API响应中移除
|
|
|
|
|
+
|
|
|
|
|
+## Dev Notes
|
|
|
|
|
+
|
|
|
|
|
+### 先前故事洞察
|
|
|
|
|
+- **故事8(购物车页面规格切换功能)**:已扩展`CartContext`,`CartItem`接口包含`parentGoodsId`字段,购物车页面已集成规格选择器
|
|
|
|
|
+- **故事4-6**:商品API已支持父子商品关系,商品详情API返回`children`和`parent`字段(类型为`any`)
|
|
|
|
|
+- **关键设计决策**:规格=子商品的名称,规格选择=选择子商品,购物车逻辑简化(使用子商品的`id`、`name`、`price`、`stock`)
|
|
|
|
|
+- **当前实现状态**:`GoodsServiceMt.getById`方法已返回`parent`对象,但只包含有限字段:`['id', 'name', 'price', 'costPrice', 'stock', 'imageFileId', 'goodsType']`
|
|
|
|
|
+
|
|
|
|
|
+### 数据模型
|
|
|
|
|
+- **商品实体 (`GoodsMt`)**:
|
|
|
|
|
+ - `spuId`字段:0表示父商品或单规格商品,>0表示子商品
|
|
|
|
|
+ - `spuName`字段:父商品名称(冗余字段,存在数据一致性问题)
|
|
|
|
|
+ - `tenantId`字段:租户ID,父子商品必须在同一租户下
|
|
|
|
|
+ - [Source: packages/goods-module-mt/src/entities/goods.entity.mt.ts#L76-L81]
|
|
|
|
|
+
|
|
|
|
|
+- **商品Schema**:
|
|
|
|
|
+ - `PublicGoodsSchema`: 包含`parent: z.any()`和`children: z.array(z.any())`字段,`spuName`字段存在
|
|
|
|
|
+ - `AdminGoodsSchema`: 包含`parent: z.any()`和`children: z.array(z.any())`字段,`spuName`字段存在
|
|
|
|
|
+ - `UserGoodsSchema`: 不包含`parent`和`children`字段,有`spuName`字段
|
|
|
|
|
+ - [Source: packages/goods-module-mt/src/schemas/public-goods.schema.mt.ts#L125-L127]
|
|
|
|
|
+ - [Source: packages/goods-module-mt/src/schemas/admin-goods.schema.mt.ts#L127-L129]
|
|
|
|
|
+
|
|
|
|
|
+- **父商品精简Schema需求**:
|
|
|
|
|
+ ```typescript
|
|
|
|
|
+ const ParentGoodsSchema = z.object({
|
|
|
|
|
+ id: z.number().int().positive(),
|
|
|
|
|
+ name: z.string().min(1).max(255),
|
|
|
|
|
+ price: z.coerce.number().multipleOf(0.01).min(0).default(0),
|
|
|
|
|
+ costPrice: z.coerce.number().multipleOf(0.01).min(0).default(0),
|
|
|
|
|
+ stock: z.coerce.number().int().nonnegative().default(0),
|
|
|
|
|
+ imageFileId: z.number().int().positive().nullable(),
|
|
|
|
|
+ goodsType: z.number().int().min(1).max(2).default(1),
|
|
|
|
|
+ spuId: z.number().int().nonnegative().default(0) // 父商品的spuId总是0
|
|
|
|
|
+ })
|
|
|
|
|
+ ```
|
|
|
|
|
+
|
|
|
|
|
+### API 规范
|
|
|
|
|
+- **商品详情API** (`GET /api/v1/goods/:id`):
|
|
|
|
|
+ - 父商品:返回商品详情 + `children`数组(子商品列表)
|
|
|
|
|
+ - 子商品:返回子商品详情 + `parent`对象(父商品基本信息)
|
|
|
|
|
+ - 当前`parent`对象字段:`['id', 'name', 'price', 'costPrice', 'stock', 'imageFileId', 'goodsType']`
|
|
|
|
|
+ - [Source: packages/goods-module-mt/src/services/goods.service.mt.ts#L120-L126]
|
|
|
|
|
+
|
|
|
|
|
+- **API变更策略**:
|
|
|
|
|
+ - 不再返回`spuName`字段,前端使用`parent.name`获取父商品名称
|
|
|
|
|
+ - `parent`字段类型从`any`改为具体的`ParentGoodsSchema`
|
|
|
|
|
+ - `children`字段类型从`any[]`改为`PublicGoodsSchema[]`
|
|
|
|
|
+ - 保持向后兼容性:数据库实体保留`spuName`字段,仅从API响应中移除
|
|
|
|
|
+
|
|
|
|
|
+### 组件规范
|
|
|
|
|
+- **购物车页面 (`cart/index.tsx`)**:
|
|
|
|
|
+ - 使用`goodsMap`存储从商品详情API获取的最新商品信息
|
|
|
|
|
+ - 当前商品名称显示:`goodsName = latestGoods?.name || item.name`
|
|
|
|
|
+ - 购物车项包含`parentGoodsId`字段(来自`CartItem`接口)
|
|
|
|
|
+ - 商品详情API返回的数据存储在`goodsMap`中,可通过`latestGoods?.parent?.name`获取父商品名称
|
|
|
|
|
+ - [Source: mini/src/pages/cart/index.tsx#L251-L253]
|
|
|
|
|
+
|
|
|
|
|
+- **购物车上下文 (`CartContext`)**:
|
|
|
|
|
+ - `CartItem`接口包含`parentGoodsId`字段
|
|
|
|
|
+ - `switchSpec`函数支持规格切换
|
|
|
|
|
+ - [Source: mini/src/contexts/CartContext.tsx#L4-L13]
|
|
|
|
|
+
|
|
|
|
|
+### 文件位置
|
|
|
|
|
+- **商品Schema文件**:
|
|
|
|
|
+ - `packages/goods-module-mt/src/schemas/public-goods.schema.mt.ts` - 更新`parent`和`children`字段类型,移除`spuName`
|
|
|
|
|
+ - `packages/goods-module-mt/src/schemas/admin-goods.schema.mt.ts` - 更新`parent`和`children`字段类型,移除`spuName`
|
|
|
|
|
+ - `packages/goods-module-mt/src/schemas/user-goods.schema.mt.ts` - 移除`spuName`字段
|
|
|
|
|
+ - `packages/goods-module-mt/src/schemas/goods.schema.mt.ts` - 更新`UpdateGoodsDto`,移除`spuName`字段
|
|
|
|
|
+ - 可能新建:`packages/goods-module-mt/src/schemas/parent-goods.schema.mt.ts` - 父商品精简Schema
|
|
|
|
|
+
|
|
|
|
|
+- **商品服务文件**:
|
|
|
|
|
+ - `packages/goods-module-mt/src/services/goods.service.mt.ts` - 完善`getById`方法,确保`parent`对象包含完整字段
|
|
|
|
|
+
|
|
|
|
|
+- **测试文件**:
|
|
|
|
|
+ - `packages/goods-module-mt/tests/unit/services/goods.service.mt.test.ts` - 更新单元测试
|
|
|
|
|
+ - `packages/goods-module-mt/tests/integration/public-goods-routes.integration.test.ts` - 更新集成测试
|
|
|
|
|
+
|
|
|
|
|
+- **购物车页面**:
|
|
|
|
|
+ - `mini/src/pages/cart/index.tsx` - 无需修改(故事10将修改显示逻辑),仅验证数据可用性
|
|
|
|
|
+
|
|
|
|
|
+### 技术约束
|
|
|
|
|
+- **多租户要求**:所有操作必须包含`tenantId`过滤,父子商品必须在同一租户下
|
|
|
|
|
+- **向后兼容性**:现有功能不受影响,数据库实体保留`spuName`字段,仅从API响应中移除
|
|
|
|
|
+- **性能考虑**:关联查询不应显著影响API响应时间
|
|
|
|
|
+- **数据一致性**:通过关联查询解决`spuName`字段同步问题,确保父子商品名称显示准确
|
|
|
|
|
+
|
|
|
|
|
+### 测试标准
|
|
|
|
|
+- **测试框架**:商品模块使用Vitest,mini项目使用Jest
|
|
|
|
|
+- **测试位置**:与源码并列的`tests/`目录
|
|
|
|
|
+- **单元测试位置**:`packages/goods-module-mt/tests/unit/services/goods.service.mt.test.ts`
|
|
|
|
|
+- **集成测试位置**:`packages/goods-module-mt/tests/integration/public-goods-routes.integration.test.ts`
|
|
|
|
|
+- **测试覆盖率**:核心业务逻辑 > 80%,关键函数 > 90%
|
|
|
|
|
+- **测试策略**:验证`parent`对象字段完整性、API响应不包含`spuName`字段、父子商品关联查询准确性
|
|
|
|
|
+- [Source: docs/architecture/testing-strategy.md#单元测试-unit-tests]
|
|
|
|
|
+
|
|
|
|
|
+## Change Log
|
|
|
|
|
+| Date | Version | Description | Author |
|
|
|
|
|
+|------|---------|-------------|--------|
|
|
|
|
|
+| 2025-12-14 | 1.0 | 初始故事创建 | Bob (Scrum Master) |
|
|
|
|
|
+
|
|
|
|
|
+## Dev Agent Record
|
|
|
|
|
+*此部分由开发代理在实施过程中填写*
|
|
|
|
|
+
|
|
|
|
|
+### Agent Model Used
|
|
|
|
|
+
|
|
|
|
|
+### Debug Log References
|
|
|
|
|
+
|
|
|
|
|
+### Completion Notes List
|
|
|
|
|
+
|
|
|
|
|
+### File List
|
|
|
|
|
+
|
|
|
|
|
+## QA Results
|
|
|
|
|
+*此部分由QA代理在审查完成后填写*
|