Explorar el Código

📝 创建故事006.014并更新相关状态

- 创建故事006.014:订单提交快照商品名称优化
- 更新PRD史诗006状态:故事11标记为已完成
- 更新故事006.011文档:添加API模拟规范更新
- 更新集成测试:根据API模拟规范优化mock配置

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
yourname hace 1 mes
padre
commit
7716c668d9

+ 5 - 5
docs/prd/epic-006-parent-child-goods-multi-spec-support.md

@@ -1,8 +1,8 @@
 # 史诗006:父子商品多规格支持 - 棕地增强
 
 ## 史诗状态
-**进度**: 11/14 故事完成 (78.6%)
-**最近更新**: 2025-12-15 (故事12:商品详情页规格选择流程优化完成)
+**进度**: 12/14 故事完成 (85.7%)
+**最近更新**: 2025-12-15 (故事11:子商品删除功能实现完成)
 **当前状态**: 故事1-12已完成,故事13-14待开始
 
 ### 完成概览
@@ -16,7 +16,7 @@
 - ✅ **故事8**: 购物车页面规格切换功能 (已完成)
 - ✅ **故事9**: 父子商品名称关联查询优化(为购物车显示做准备) (已完成)
 - ✅ **故事10**: 购物车商品名称显示优化 (已完成)
-- ⏳ **故事11**: 子商品删除功能实现 (待开始)
+- ✅ **故事11**: 子商品删除功能实现 (已完成)
 - ✅ **故事12**: 商品详情页规格选择流程优化 (已完成)
 - ⏳ **故事13**: 父子商品列表缓存自动刷新优化 (待开始)
 - ⏳ **故事14**: 订单提交快照商品名称优化 (待开始)
@@ -63,7 +63,7 @@
   7. ✅ 用户能在购物车页面切换规格(故事8已实现)
   8. ✅ 父子商品名称通过关联查询获取,为购物车显示提供准确父商品名称(故事9已实现)
   9. ✅ 购物车中父子商品显示完整的组合名称(父商品名称 + 子商品规格名称)(故事10已实现)
-  10. ⏳ 管理员能删除不需要的子商品规格(故事11待实现)
+  10. ✅ 管理员能删除不需要的子商品规格(故事11已实现)
   11. ✅ 用户在商品详情页能一键完成规格选择和购物车/购买操作(故事12已实现)
   12. ⏳ 订单提交快照商品名称包含完整的商品和规格信息(故事14待实现)
 
@@ -388,7 +388,7 @@
        - 更新测试中的点击事件,使用正确的DOM元素(div.goods-specs)
        - 更新测试断言,使用精确文本匹配和正则表达式
 
-11. **故事11:子商品删除功能实现** ⏳ **待开始**
+11. **故事11:子商品删除功能实现** ✅ **已完成**
    - **问题背景**:当前在管理后台商品管理对话框的父子商品管理面板中,子商品列表(`ChildGoodsList`组件)提供了删除按钮,但该按钮没有实际作用。点击删除按钮时,`handleDelete`函数仅检查`onDeleteChild`回调是否存在,而父组件`GoodsParentChildPanel`并未传递此回调,导致删除操作无效。管理员无法在管理界面中直接删除子商品规格。
    - **解决方案**:实现子商品删除功能,在父子商品管理面板中为子商品列表添加有效的删除操作,允许管理员删除不需要的子商品规格。
    - **功能需求**:

+ 25 - 0
docs/stories/006.011.child-goods-deletion.story.md

@@ -193,6 +193,25 @@ Ready for Review
    - 分离新功能测试与现有测试问题,确保新功能验证完整
    - 在集成测试中使用真实组件,仅模拟API层
 
+### API模拟规范更新
+根据架构文档`docs/architecture/testing-strategy.md`中的API模拟规范,管理后台UI包的测试应统一模拟`@d8d/shared-ui-components/utils/hc`中的`rpcClient`函数,而非分别模拟各个客户端管理器。规范要点:
+
+1. **统一模拟点**:集中模拟`rpcClient`函数,统一拦截所有API调用
+2. **跨包优势**:天然支持多个UI包组件的集成测试,无需分别模拟客户端管理器
+3. **简化配置**:所有API调用都经过同一个模拟点,配置更简单
+4. **维护性**:API响应配置集中管理,易于更新
+
+**当前测试状态检查**:
+- **集成测试**:`goods-management.integration.test.tsx`已部分更新,包含统一的`mockRpcClient`函数模拟,但仍保留对`goodsClientManager`的模拟以保持向后兼容性
+- **模拟模式**:目前采用混合模式,既模拟`rpcClient`函数,又模拟`goodsClientManager`,测试用例仍通过`goodsClientManager.get()`访问模拟客户端
+- **符合性评估**:基本符合API模拟规范,但未完全采用直接通过`mockRpcClient`创建的客户端实例
+- **改进建议**:可进一步优化为完全使用统一模拟模式,直接通过`mockRpcClient('/')`获取模拟客户端实例
+
+**商品删除API模拟验证**:
+- `:id.$delete`方法已正确模拟,返回`createMockResponse(204)`符合实际API响应
+- 模拟响应格式与实际API一致,支持子商品删除功能的测试
+- 测试用例中正确配置了商品删除API的模拟响应
+
 ### Completion Notes List
 1. 在GoodsParentChildPanel组件中添加了onDeleteChild回调函数,实现子商品删除逻辑
 2. 添加了删除确认对话框,使用现有Dialog组件防止误操作
@@ -204,6 +223,7 @@ Ready for Review
 8. 验证了多租户兼容性(租户ID验证)和向后兼容性
 9. 移除了生产代码和测试代码中的调试信息(console.debug),减少上下文干扰
 10. 更新测试策略文档,强调集成测试应使用真实UI组件,仅模拟API层
+11. 根据API模拟规范检查并验证集成测试的API模拟配置,集成测试已包含统一的`rpcClient`函数模拟,商品删除API`:id.$delete`方法正确模拟,返回`204 No Content`响应
 
 ### File List
 **已修改文件:**
@@ -228,6 +248,11 @@ Ready for Review
    - 更新回调函数测试,验证onDeleteChild调用
    - 添加删除期间加载状态测试
 
+5. `packages/goods-management-ui-mt/tests/integration/goods-management.integration.test.tsx`
+   - 根据API模拟规范更新mock,使用统一的`rpcClient`模拟
+   - 添加`$get`方法模拟,支持子商品删除前的验证
+   - 确保模拟响应格式与实际API响应一致
+
 **已更新文件:**
 1. `docs/stories/006.011.child-goods-deletion.story.md`
    - 更新Status从Draft到Approved

+ 166 - 0
docs/stories/006.014.order-submit-goods-name-optimization.story.md

@@ -0,0 +1,166 @@
+# Story 006.014: 订单提交快照商品名称优化
+
+## Status
+Draft
+
+## Story
+**As a** 用户,
+**I want** 订单快照中的商品名称包含完整的父商品名称和规格信息,
+**so that** 订单页面能正确显示商品信息,与购物车显示逻辑保持一致
+
+## Acceptance Criteria
+1. 提交订单时,子商品的快照商品名称包含完整的父商品名称和规格信息(例如:"连衣裙 红色 大码")
+2. 单规格商品的快照商品名称保持不变
+3. 订单详情页面正确显示完整的商品信息
+4. 现有功能不受影响,无回归问题
+
+## Tasks / Subtasks
+- [ ] 任务1:分析当前订单商品快照逻辑 (AC: 1, 2, 3, 4)
+  - [ ] 检查 `packages/orders-module-mt/src/services/order.mt.service.ts` 中的 `createOrder` 方法
+  - [ ] 分析当前 `goodsName` 字段的赋值逻辑(第139行:`goodsName: info.goods.name`)
+  - [ ] 理解商品实体结构,确认 `spuId` 字段的使用方式
+  - [ ] 检查购物车商品名称显示逻辑,确保与订单快照逻辑保持一致
+- [ ] 任务2:实现子商品快照名称优化逻辑 (AC: 1, 2, 4)
+  - [ ] 在 `createOrder` 方法的商品循环中,判断商品是否为子商品(通过 `spuId` 字段)
+  - [ ] 如果是子商品(`spuId > 0`),通过 `spuId` 查询父商品实体,获取父商品名称
+  - [ ] 将父商品名称和子商品名称拼接后赋值给 `goodsName` 字段(例如:`goodsName = `${parentGoods.name} ${goods.name}``)
+  - [ ] 确保多租户过滤:父子商品在同一租户下
+  - [ ] 确保单规格商品(`spuId = 0`)保持现有逻辑不变
+- [ ] 任务3:添加单元测试和集成测试 (AC: 1, 2, 3, 4)
+  - [ ] 为 `createOrder` 方法中的商品名称拼接逻辑添加单元测试
+  - [ ] 在现有集成测试文件 `packages/orders-module-mt/tests/integration/user-orders-routes.integration.test.ts` 中新增针对父子商品的订单创建测试用例
+  - [ ] 在集成测试中验证子商品订单快照的商品名称格式
+  - [ ] 验证单规格商品的现有行为保持不变
+- [ ] 任务4:验证功能完整性和性能 (AC: 3, 4)
+  - [ ] 验证订单详情页面正确显示完整的商品信息
+  - [ ] 测试父子商品在不同租户下的隔离性
+  - [ ] 确保没有额外的数据库查询影响性能
+  - [ ] 运行现有测试套件,确保无回归问题
+
+## Dev Notes
+
+### 问题分析
+- **当前问题**:在 `packages/orders-module-mt/src/services/order.mt.service.ts` 的 `createOrder` 方法中,`goodsName` 字段直接使用商品实体的 `name` 字段(第139行)。对于子商品,这会导致订单快照中存储的是子商品的规格名称,而不是父商品名称+规格名称的组合。
+- **影响**:订单页面显示的商品名称与购物车显示逻辑不一致。购物车中商品名称显示父商品名称,规格名称显示子商品规格名称,但订单快照只存储子商品的名称。
+- **技术背景**:
+  - 商品实体(`GoodsMt`)包含 `spuId` 字段,用于标识父子商品关系(`spuId = 0` 表示父商品,`spuId > 0` 表示子商品)
+  - 购物车显示逻辑已优化:显示父商品名称 + 子商品规格名称
+  - 需要保持订单快照与购物车显示逻辑的一致性
+
+### 技术实现细节
+- **文件位置**:
+  - 主要修改文件:`packages/orders-module-mt/src/services/order.mt.service.ts` [Source: architecture/source-tree.md#实际项目结构]
+  - 测试文件:在现有集成测试文件 `packages/orders-module-mt/tests/integration/user-orders-routes.integration.test.ts` 中新增测试用例
+- **商品实体结构**:
+  - `GoodsMt` 实体包含 `spuId: number` 字段(父商品ID)[Source: packages/goods-module-mt/src/entities/goods.entity.mt.ts:77-78]
+  - `spuId = 0`:父商品或无父子关系的商品
+  - `spuId > 0`:子商品,指向父商品的ID
+  - 商品名称字段:`name: string` [Source: packages/goods-module-mt/src/entities/goods.entity.mt.ts:17-18]
+- **当前实现分析**:
+  - `createOrder` 方法在第134-152行创建订单商品明细
+  - 第139行:`goodsName: info.goods.name` 直接使用商品名称
+  - 需要修改:对于子商品,使用父商品名称 + 子商品名称的组合
+- **多租户要求**:
+  - 所有查询必须包含 `tenantId` 过滤条件 [Source: architecture/coding-standards.md#架构原则]
+  - 父子商品必须在同一租户下
+- **性能考虑**:
+  - 避免N+1查询问题:批量查询父商品信息
+  - 考虑在事务中缓存父商品查询结果
+
+### 解决方案设计
+1. **修改 `createOrder` 方法**:
+   ```typescript
+   // 在商品循环中收集需要查询的父商品ID
+   const parentGoodsIds = new Set<number>();
+   for (const item of products) {
+     const goods = await this.goodsRepository.findOne({
+       where: { id: item.id, tenantId }
+     });
+     // ... 现有验证逻辑
+
+     if (goods.spuId > 0) {
+       parentGoodsIds.add(goods.spuId);
+     }
+   }
+
+   // 批量查询父商品信息
+   const parentGoodsMap = new Map<number, GoodsMt>();
+   if (parentGoodsIds.size > 0) {
+     const parentGoods = await this.goodsRepository.find({
+       where: { id: In([...parentGoodsIds]), tenantId }
+     });
+     parentGoods.forEach(g => parentGoodsMap.set(g.id, g));
+   }
+
+   // 创建订单商品明细时使用正确的商品名称
+   const orderGoodsList = goodsInfo.map(info => {
+     let goodsName = info.goods.name;
+     if (info.goods.spuId > 0) {
+       const parentGoods = parentGoodsMap.get(info.goods.spuId);
+       if (parentGoods) {
+         goodsName = `${parentGoods.name} ${info.goods.name}`;
+       }
+     }
+
+     return {
+       // ... 其他字段
+       goodsName,
+       // ... 其他字段
+     };
+   });
+   ```
+
+2. **测试策略**:
+   - 单元测试:验证商品名称拼接逻辑
+   - 集成测试:验证完整订单创建流程
+   - 测试父子商品、单规格商品的不同场景
+
+### 文件位置
+- **主要修改文件**:
+  - `packages/orders-module-mt/src/services/order.mt.service.ts` - 修改 `createOrder` 方法中的商品名称拼接逻辑
+- **测试文件**:
+  - `packages/orders-module-mt/tests/integration/user-orders-routes.integration.test.ts` - 在现有集成测试中新增针对父子商品的订单创建测试用例,验证订单快照商品名称格式
+
+### 技术约束
+- **多租户隔离**:所有数据库查询必须包含 `tenantId` 条件 [Source: architecture/coding-standards.md#架构原则]
+- **TypeORM使用**:使用 `In` 操作符进行批量查询,避免N+1问题
+- **事务安全**:修改在现有事务中,确保数据一致性
+- **向后兼容性**:单规格商品(`spuId = 0`)的行为保持不变
+
+### Testing
+- **测试框架**:Vitest [Source: architecture/tech-stack.md#新技术添加]
+- **集成测试框架**:hono/testing [Source: architecture/tech-stack.md#新技术添加]
+- **测试位置**:
+  - 单元测试:`packages/orders-module-mt/tests/unit/**/*.test.ts` [Source: architecture/testing-strategy.md#单元测试]
+  - 集成测试:`packages/orders-module-mt/tests/integration/**/*.test.ts` [Source: architecture/testing-strategy.md#集成测试]
+- **测试标准**:
+  - 单元测试覆盖率目标:≥ 80% [Source: architecture/testing-strategy.md#单元测试]
+  - 集成测试覆盖率目标:≥ 60% [Source: architecture/testing-strategy.md#集成测试]
+- **具体测试要求**:
+  - 验证子商品订单快照的商品名称格式
+  - 验证单规格商品的现有行为不变
+  - 验证多租户数据隔离
+  - 验证事务回滚场景
+
+## Change Log
+| Date | Version | Description | Author |
+|------|---------|-------------|--------|
+| 2025-12-15 | 1.0 | 初始故事创建 | Bob (Scrum Master) |
+
+## Dev Agent Record
+*此部分由开发代理在实施过程中填写*
+
+### Agent Model Used
+-
+
+### Debug Log References
+-
+
+### Completion Notes List
+-
+
+### File List
+-
+
+## QA Results
+*此部分由QA代理在审查完成后填写*

+ 25 - 6
packages/goods-management-ui-mt/tests/integration/goods-management.integration.test.tsx

@@ -2,6 +2,7 @@ import React from 'react';
 import { describe, it, expect, vi, beforeEach } from 'vitest';
 import { render, screen, fireEvent, waitFor, within } from '@testing-library/react';
 import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
+import type { Hono } from 'hono';
 import { GoodsManagement } from '../../src/components/GoodsManagement';
 import { goodsClient, goodsClientManager } from '../../src/api/goodsClient';
 
@@ -24,16 +25,20 @@ const createMockResponse = (status: number, data?: any) => ({
   clone: function() { return this; }
 });
 
-// Mock API client
-vi.mock('../../src/api/goodsClient', () => {
-  const mockGoodsClient = {
+// 创建模拟的rpcClient函数(根据API模拟规范)
+// 符合测试策略文档的API模拟规范:统一模拟@d8d/shared-ui-components/utils/hc中的rpcClient函数
+const mockRpcClient = vi.hoisted(() => vi.fn((aptBaseUrl: string) => {
+  // 根据页面组件实际调用的RPC路径定义模拟端点
+  // 符合规范:支持Hono风格的$get、$post、$put、$delete方法
+  return {
     index: {
       $get: vi.fn(() => Promise.resolve(createMockResponse(200))),
       $post: vi.fn(() => Promise.resolve(createMockResponse(201))),
     },
     ':id': {
+      $get: vi.fn(() => Promise.resolve(createMockResponse(200, { id: 1, spuId: 0, tenantId: 1 }))),
       $put: vi.fn(() => Promise.resolve(createMockResponse(200))),
-      $delete: vi.fn(() => Promise.resolve(createMockResponse(204))),
+      $delete: vi.fn(() => Promise.resolve(createMockResponse(204))), // 商品删除API,支持子商品删除
       // 故事006.002新增的父子商品管理API
       children: {
         $get: vi.fn(() => Promise.resolve(createMockResponse(200, { data: [], total: 0 }))),
@@ -50,14 +55,28 @@ vi.mock('../../src/api/goodsClient', () => {
       $post: vi.fn(() => Promise.resolve(createMockResponse(200))),
     },
   };
+}));
+
+// 模拟共享UI组件包中的rpcClient函数(统一模拟点)
+// 核心API模拟规范:统一拦截所有API调用,支持跨UI包集成测试
+vi.mock('@d8d/shared-ui-components/utils/hc', () => ({
+  rpcClient: mockRpcClient
+}));
+
+// Mock API client(保持向后兼容性,但实际使用上面的统一模拟)
+// 符合API模拟规范:统一模拟rpcClient函数,客户端管理器使用模拟的rpcClient
+vi.mock('../../src/api/goodsClient', () => {
+  // 获取模拟的客户端实例(通过mockRpcClient创建)
+  // 符合测试策略文档的规范:直接通过模拟的rpcClient函数创建客户端
+  const mockClient = mockRpcClient('/');
 
   const mockGoodsClientManager = {
-    get: vi.fn(() => mockGoodsClient),
+    get: vi.fn(() => mockClient),
   };
 
   return {
     goodsClientManager: mockGoodsClientManager,
-    goodsClient: mockGoodsClient,
+    goodsClient: mockClient,
   };
 });