# Story 006.009: 父子商品名称关联查询优化（为购物车显示做准备）

## Status
Ready for Review

## 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
- [x] 任务1：更新商品Schema，完善父子商品类型定义 (AC: 1, 3)
  - [x] 创建父商品精简Schema（`ParentGoodsSchema`），包含基本字段：id、name、price、costPrice、stock、imageFileId、goodsType
  - [x] 更新`PublicGoodsSchema`中的`parent`字段类型：从`z.any()`改为`ParentGoodsSchema.nullable().optional()`
  - [x] 更新`AdminGoodsSchema`中的`parent`字段类型：从`z.any()`改为`ParentGoodsSchema.nullable().optional()`
  - [x] 更新`PublicGoodsSchema`中的`children`字段类型：从`z.array(z.any())`改为`z.array(PublicGoodsSchema).nullable().optional()`
  - [x] 从`PublicGoodsSchema`、`AdminGoodsSchema`、`UserGoodsSchema`中移除`spuName`字段（保留实体中的字段，仅从Schema中移除）
  - [x] 更新`UpdateGoodsDto`，移除`spuName`字段（API不再接受`spuName`字段更新）
- [x] 任务2：完善商品服务`GoodsServiceMt.getById`方法 (AC: 1)
  - [x] 确保`parent`对象包含完整的父商品基本信息：id、name、price、costPrice、stock、imageFileId、goodsType、spuId（0）
  - [x] 添加租户ID过滤，确保父商品与子商品在同一租户下（已实现，需要验证）
  - [x] 优化`parent`对象字段选择，确保包含所有必要字段
  - [x] 确保`children`列表返回完整的子商品信息（包含所有Schema字段）
- [x] 任务3：验证购物车页面父子商品名称获取 (AC: 2)
  - [x] 检查购物车页面（`mini/src/pages/cart/index.tsx`）当前商品名称显示逻辑
  - [x] 确认`goodsMap`中的商品数据包含`parent`对象
  - [x] 验证购物车页面能通过`latestGoods?.parent?.name`获取父商品名称
  - [x] 无需修改购物车页面代码（故事10将修改显示逻辑），仅确保数据基础可用
- [x] 任务4：编写和更新测试 (AC: 5)
  - [x] 更新商品服务单元测试，验证`getById`方法返回正确的`parent`对象
  - [x] 添加集成测试，验证商品详情API返回的`parent`对象包含完整字段
  - [x] 验证API不再返回`spuName`字段
  - [x] 测试父子商品关联查询的准确性
  - [x] 确保所有现有测试通过，无回归问题
- [x] 任务5：验证多租户兼容性和向后兼容性 (AC: 4, 5)
  - [x] 验证父子商品在同一租户下的约束
  - [x] 确保现有功能不受影响（单规格商品、无父子关系的商品）
  - [x] 验证数据库实体中的`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
claude-sonnet

### Debug Log References
无

### Completion Notes List
1. **Schema更新完成**:
   - 创建了`ParentGoodsSchema`，包含id、name、price、costPrice、stock、imageFileId、goodsType、spuId字段
   - 更新`PublicGoodsSchema`和`AdminGoodsSchema`中的`parent`字段类型为`ParentGoodsSchema.nullable().optional()`
   - 更新`children`字段类型为`z.array(PublicGoodsSchema).nullable().optional()`（使用懒加载避免循环依赖）
   - 从所有Schema中移除`spuName`字段，保持数据库实体向后兼容

2. **服务层优化完成**:
   - `GoodsServiceMt.getById`方法已完善，`parent`对象包含完整字段：id、name、price、costPrice、stock、imageFileId、goodsType、spuId
   - 添加租户ID过滤：`{ id: goods.spuId, tenantId: goods.tenantId }`，确保父子商品在同一租户下
   - `children`列表返回完整子商品信息，包含所有关联关系

3. **购物车数据基础验证**:
   - 购物车页面(`mini/src/pages/cart/index.tsx`)已通过`goodsMap`获取最新商品信息
   - API返回的数据包含`parent`对象，可通过`latestGoods?.parent?.name`获取父商品名称
   - 无需修改购物车页面代码，为故事10的显示逻辑优化提供数据基础

4. **测试更新完成**:
   - 集成测试已更新，验证`parent`对象字段完整性和API不再返回`spuName`字段
   - 测试父子商品关联查询准确性，包括多租户隔离验证
   - 所有现有测试通过，无回归问题

5. **多租户和向后兼容性**:
   - 数据库实体`GoodsMt`保留`spuName`字段，仅从API响应中移除
   - 父子商品查询添加租户ID过滤，确保租户数据隔离
   - 单规格商品和无父子关系的商品功能不受影响

### File List
**新增文件:**
- `packages/goods-module-mt/src/schemas/parent-goods.schema.mt.ts` - 父商品精简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/index.mt.ts` - 导出ParentGoodsSchema
- `packages/goods-module-mt/src/services/goods.service.mt.ts` - 完善getById方法，添加租户过滤和完整字段选择
- `packages/goods-module-mt/tests/integration/admin-goods-parent-child.integration.test.ts` - 更新测试验证parent对象
- `packages/goods-module-mt/tests/integration/admin-goods-routes.integration.test.ts` - 更新测试
- `packages/goods-module-mt/tests/integration/public-goods-children.integration.test.ts` - 更新测试验证spuName移除
- `packages/goods-module-mt/tests/integration/public-goods-parent-filter.integration.test.ts` - 更新测试验证parent对象完整性

**验证文件:**
- `mini/src/pages/cart/index.tsx` - 购物车页面，验证数据基础可用性
- `packages/goods-module-mt/src/entities/goods.entity.mt.ts` - 验证spuName字段保留在实体中

## QA Results
*此部分由QA代理在审查完成后填写*