Pārlūkot izejas kodu

✨ feat: 实现订单商品关联关系架构重构

- 在订单实体中添加与订单商品的一对多关联关系
- 更新用户订单路由配置包含orderGoods关联查询
- 统一订单商品图片URL字段为fullUrl,使用z.url()验证器
- 修复测试工厂中订单商品创建方法,确保提供有效的goods_id字段
- 添加订单商品关联验证测试
- 更新故事文档记录已完成工作

🤖 Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
yourname 1 mēnesi atpakaļ
vecāks
revīzija
bfdc84d983

+ 195 - 0
docs/stories/003.002.order-goods-display-fix.story.md

@@ -0,0 +1,195 @@
+# Story 003.002: 修复订单列表和详情页商品显示问题
+
+## Status
+Draft
+
+## Story
+**As a** 小程序用户,
+**I want** 在订单列表页和详情页正确显示商品信息,
+**so that** 能够清楚了解订单中的商品详情
+
+## Acceptance Criteria
+1. 在`OrderCard`组件中修复`parseGoodsDetail`函数,确保能正确解析JSON格式的商品详情 (`mini/src/components/order/OrderCard/index.tsx:18-24`)
+2. 在`OrderDetailPage`中修复`parseGoodsDetail`函数,确保能正确解析JSON格式的商品详情 (`mini/src/pages/order-detail/index.tsx:134-140`)
+3. 验证订单创建时`goodsDetail`字段是否正确保存商品信息(包括商品图片、名称、价格、规格等)
+4. 确保订单列表页和详情页中商品图片、名称、规格、价格等信息的完整显示 (`mini/src/pages/order-list/index.tsx`, `mini/src/pages/order-detail/index.tsx:242-263`)
+5. 增强JSON解析的错误处理,当解析失败时显示默认商品信息
+
+## Tasks / Subtasks
+- [x] **修复订单实体关联关系** (AC: 3)
+  - [x] 在订单实体中添加与订单商品的一对多关联关系 `packages/orders-module-mt/src/entities/order.mt.entity.ts`
+  - [x] 更新订单路由配置,包含订单商品关联查询 `packages/orders-module-mt/src/routes/user/orders.mt.ts`
+  - [x] 更新订单Schema,添加订单商品关联信息 `packages/orders-module-mt/src/schemas/order.mt.schema.ts`
+  - [x] 验证关联查询正确返回订单商品信息
+  - [x] 修复测试工厂中订单商品创建方法,确保提供有效的goods_id字段
+  - [x] 添加订单商品关联验证测试 `packages/orders-module-mt/tests/integration/user-orders-routes.integration.test.ts`
+
+- [ ] **修复OrderCard组件中的商品详情解析** (AC: 1, 5)
+  - [ ] 更新组件使用新的orderGoods关联而不是解析goodsDetail字段 `mini/src/components/order/OrderCard/index.tsx:18-24`
+  - [ ] 增强错误处理,当关联数据为空时显示默认商品信息 `mini/src/components/order/OrderCard/index.tsx:18-24`
+  - [ ] 验证商品图片、名称、规格、价格等信息的正确显示 `mini/src/components/order/OrderCard/index.tsx`
+  - [ ] 添加单元测试验证解析功能 `mini/tests/unit/components/order/OrderCard.test.tsx`
+
+- [ ] **修复OrderDetailPage组件中的商品详情解析** (AC: 2, 5)
+  - [ ] 更新组件使用新的orderGoods关联而不是解析goodsDetail字段 `mini/src/pages/order-detail/index.tsx:134-140`
+  - [ ] 增强错误处理,当关联数据为空时显示默认商品信息 `mini/src/pages/order-detail/index.tsx:134-140`
+  - [ ] 验证商品图片、名称、规格、价格等信息的正确显示 `mini/src/pages/order-detail/index.tsx:242-263`
+  - [ ] 添加单元测试验证解析功能 `mini/tests/unit/pages/order-detail/order-detail.test.tsx`
+
+- [ ] **验证商品详情数据完整性** (AC: 3)
+  - [ ] 验证订单创建时订单商品关联关系是否正确建立 `packages/orders-module-mt/src/services/order.mt.service.ts`
+  - [ ] 检查商品图片、名称、价格、规格等字段的完整性 `packages/orders-module-mt/src/entities/order.mt.entity.ts`
+  - [ ] 添加数据验证测试 `packages/orders-module-mt/tests/unit/services/order.mt.service.test.ts`
+
+- [ ] **优化订单列表页商品显示** (AC: 4)
+  - [ ] 确保订单列表页中商品图片、名称、规格、价格等信息的完整显示 `mini/src/pages/order-list/index.tsx`
+  - [ ] 验证商品信息布局和样式正确 `mini/src/pages/order-list/index.tsx`
+  - [ ] 添加集成测试验证页面显示 `mini/tests/unit/pages/order-list/order-list.test.tsx`
+
+- [ ] **优化订单详情页商品显示** (AC: 4)
+  - [ ] 确保订单详情页中商品图片、名称、规格、价格等信息的完整显示 `mini/src/pages/order-detail/index.tsx:242-263`
+  - [ ] 验证商品信息布局和样式正确 `mini/src/pages/order-detail/index.tsx:242-263`
+  - [ ] 添加集成测试验证页面显示 `mini/tests/unit/pages/order-detail/order-detail.test.tsx`
+
+## Dev Notes
+
+### 技术栈信息 [Source: architecture/tech-stack.md#现有技术栈维护]
+- **前端框架**: React 19.1.0 + TypeScript
+- **小程序框架**: Taro
+- **状态管理**: @tanstack/react-query (服务端状态管理)
+- **HTTP客户端**: 基于Hono Client的封装
+- **API调用**: RPC类型安全
+
+### 项目结构信息 [Source: architecture/source-tree.md#实际项目结构]
+- **小程序项目**: `mini/src/`
+- **订单组件**: `mini/src/components/order/`
+  - `OrderCard/index.tsx` - 订单卡片组件
+  - `OrderButtonBar/index.tsx` - 订单操作按钮栏
+- **订单页面**:
+  - `mini/src/pages/order-list/index.tsx` - 订单列表页
+  - `mini/src/pages/order-detail/index.tsx` - 订单详情页
+- **API客户端**: `mini/src/api.ts` - RPC客户端配置
+
+### 多租户订单模块信息 [Source: packages/orders-module-mt/src/entities/order.mt.entity.ts]
+- **订单实体字段**:
+  - `goodsDetail`: string - JSON格式存储的商品详情信息
+  - `state`: number - 订单状态
+  - `payState`: number - 支付状态
+- **商品详情JSON结构**:
+  ```typescript
+  {
+    goodsId: number,      // 商品ID
+    goodsName: string,    // 商品名称
+    goodsImg: string,     // 商品图片
+    price: number,        // 商品价格
+    num: number,          // 商品数量
+    spec: string          // 商品规格
+  }
+  ```
+
+### 订单状态定义 [Source: packages/orders-module-mt/src/entities/order.mt.entity.ts]
+- **支付状态 (payState)**:
+  - `0`: 未支付
+  - `2`: 支付成功
+  - `5`: 订单关闭
+- **订单状态 (state)**:
+  - `0`: 未发货
+  - `1`: 已发货
+  - `2`: 收货成功
+  - `3`: 已退货
+
+### 编码标准 [Source: architecture/coding-standards.md#关键集成规则]
+- **RPC客户端架构**: 直接使用导出的客户端实例
+- **类型安全**: 使用Hono的InferRequestType和InferResponseType
+- **组件调用规范**: 直接调用 `api.$method` 方法
+- **错误处理**: 完善的错误处理和用户反馈机制
+
+### 测试要求 [Source: docs/architecture/testing-strategy.md#测试金字塔策略]
+- **单元测试**: 验证组件逻辑和函数功能
+- **集成测试**: 验证页面组件集成
+- **测试框架**: Jest (小程序) + Testing Library
+- **测试位置**: `mini/tests/unit/` 目录
+- **覆盖率要求**: 核心业务逻辑 > 80%
+
+## Testing
+
+### 小程序前端测试
+#### 测试标准 [Source: docs/architecture/testing-strategy.md#测试金字塔策略]
+- **测试框架**: Jest + Testing Library
+- **测试类型**: 组件单元测试 + 集成测试
+- **测试位置**: `mini/tests/unit/` 目录
+- **覆盖率要求**: 核心业务逻辑 > 80%
+
+#### 具体测试要求
+- **OrderCard组件测试** `mini/tests/unit/components/order/OrderCard.test.tsx`
+  - 验证`parseGoodsDetail`函数正确解析JSON格式商品详情
+  - 验证JSON解析失败时的错误处理
+  - 验证商品图片、名称、规格、价格等信息的正确显示
+  - 验证商品数量计算逻辑
+
+- **OrderDetailPage组件测试** `mini/tests/unit/pages/order-detail/order-detail.test.tsx`
+  - 验证`parseGoodsDetail`函数正确解析JSON格式商品详情
+  - 验证JSON解析失败时的错误处理
+  - 验证商品图片、名称、规格、价格等信息的正确显示
+  - 验证页面状态正确更新
+
+- **OrderListPage组件测试** `mini/tests/unit/pages/order-list/order-list.test.tsx`
+  - 验证订单列表页中商品信息的完整显示
+  - 验证商品信息布局和样式正确
+  - 验证页面集成功能
+
+### 后端模块测试
+#### 测试标准 [Source: packages/orders-module-mt/package.json#测试配置]
+- **测试框架**: Vitest
+- **测试类型**: 单元测试 + 集成测试
+- **测试位置**: `packages/orders-module-mt/tests/unit/` 目录
+- **覆盖率要求**: 核心业务逻辑 > 80%
+
+#### 具体测试要求
+- **订单服务测试** `packages/orders-module-mt/tests/unit/services/order.mt.service.test.ts`
+  - 验证订单创建时`goodsDetail`字段正确保存商品信息
+  - 验证商品图片、名称、价格、规格等字段的完整性
+  - 验证数据验证逻辑
+
+## Change Log
+| Date | Version | Description | Author |
+|------|---------|-------------|---------|
+| 2025-11-23 | 1.0 | 初始故事创建 | Bob |
+
+## Dev Agent Record
+
+### Agent Model Used
+- Claude Code (d8d-model)
+
+### Debug Log References
+- 修复测试工厂中订单商品创建方法,确保提供有效的goods_id字段
+- 修复价格类型不匹配问题(期望字符串"100.00"但实际返回数字100)
+- 验证订单商品关联关系在订单查询中正常工作
+
+### Completion Notes List
+- ✅ **后端架构重构完成**:从goodsDetail字段解析改为使用一对多关联关系
+- ✅ **订单实体更新**:在OrderMt实体中添加了与OrderGoodsMt的一对多关联关系
+- ✅ **路由配置更新**:用户订单路由现在包含orderGoods关联查询
+- ✅ **Schema更新**:OrderSchema中添加了完整的订单商品信息结构
+- ✅ **测试验证**:添加了订单商品关联验证测试,确保订单详情和列表查询包含订单商品信息
+- ✅ **测试修复**:修复了测试工厂中订单商品创建方法,确保提供有效的goods_id字段
+- ✅ **完整测试通过**:所有订单模块测试(14个测试)全部通过
+- ✅ **图片URL Schema修复**:统一订单商品图片URL字段为`fullUrl`,使用`z.url()`验证器,与文件模块保持一致
+
+### File List
+- `packages/orders-module-mt/src/entities/order.mt.entity.ts` - 添加订单商品一对多关联关系
+- `packages/orders-module-mt/src/routes/user/orders.mt.ts` - 更新relations配置包含orderGoods关联
+- `packages/orders-module-mt/src/schemas/order.mt.schema.ts` - 添加订单商品关联Schema定义,修复图片URL字段
+- `packages/orders-module-mt/src/schemas/order-goods.schema.ts` - 统一图片URL字段为`fullUrl`
+- `packages/orders-module-mt/tests/integration/user-orders-routes.integration.test.ts` - 添加订单商品关联验证测试
+- `packages/orders-module-mt/tests/factories/orders-test-factory.ts` - 修复订单商品创建方法
+
+### 架构决策
+- **采用一对多关联关系**:替代原有的goodsDetail字段解析,保持架构一致性
+- **遵循商品模块模式**:使用与商品轮播图相同的关联关系模式
+- **通用CRUD路由配置**:通过relations配置实现关联查询,无需修改路由逻辑
+- **类型安全**:通过TypeORM关联关系和Zod Schema确保类型安全
+- **统一文件URL格式**:使用`fullUrl`字段和`z.url()`验证器,与文件模块保持一致,确保完整的URL访问地址
+
+## QA Results
+*Results from QA Agent QA review of the completed story implementation*

+ 6 - 1
packages/orders-module-mt/src/entities/order.mt.entity.ts

@@ -1,8 +1,9 @@
-import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, ManyToOne, JoinColumn, Index } from 'typeorm';
+import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, ManyToOne, JoinColumn, Index, OneToMany } from 'typeorm';
 import { UserEntityMt } from '@d8d/user-module-mt';
 import { MerchantMt } from '@d8d/merchant-module-mt';
 import { SupplierMt } from '@d8d/supplier-module-mt';
 import { DeliveryAddressMt } from '@d8d/delivery-address-module-mt';
+import { OrderGoodsMt } from './order-goods.mt.entity';
 
 @Entity('orders_mt')
 @Index(['tenantId'])
@@ -151,4 +152,8 @@ export class OrderMt {
   @ManyToOne(() => DeliveryAddressMt)
   @JoinColumn({ name: 'address_id', referencedColumnName: 'id' })
   deliveryAddress!: DeliveryAddressMt;
+
+  // 订单商品关联关系
+  @OneToMany(() => OrderGoodsMt, orderGoods => orderGoods.order)
+  orderGoods!: OrderGoodsMt[];
 }

+ 1 - 1
packages/orders-module-mt/src/routes/user/orders.mt.ts

@@ -15,7 +15,7 @@ const userOrderCrudRoutes = createCrudRoutes({
   getSchema: OrderSchema,
   listSchema: OrderSchema,
   searchFields: ['orderNo', 'userPhone', 'recevierName'],
-  relations: ['user', 'merchant', 'supplier', 'deliveryAddress'],
+  relations: ['user', 'merchant', 'supplier', 'deliveryAddress', 'orderGoods'],
   middleware: [authMiddleware],
   readOnly: true,
   userTracking: {

+ 2 - 2
packages/orders-module-mt/src/schemas/order-goods.schema.ts

@@ -97,7 +97,7 @@ export const OrderGoodsSchema = z.object({
   imageFile: z.object({
     id: z.number().int().positive().openapi({ description: '文件ID' }),
     name: z.string().max(255).openapi({ description: '文件名', example: 'goods.jpg' }),
-    fullUrl: z.string().openapi({ description: '文件完整URL', example: 'https://example.com/goods.jpg' }),
+    fullUrl: z.url().openapi({ description: '完整文件访问URL', example: 'https://minio.example.com/d8dai/uploads/goods/2024/product-image.jpg' }),
     type: z.string().nullable().openapi({ description: '文件类型', example: 'image/jpeg' }),
     size: z.number().nullable().openapi({ description: '文件大小(字节)', example: 102400 })
   }).nullable().optional().openapi({
@@ -117,7 +117,7 @@ export const OrderGoodsSchema = z.object({
     imageFile: z.object({
       id: z.number().int().positive().openapi({ description: '文件ID' }),
       name: z.string().max(255).openapi({ description: '文件名', example: 'goods.jpg' }),
-      fullUrl: z.string().openapi({ description: '文件完整URL', example: 'https://example.com/goods.jpg' }),
+      fullUrl: z.url().openapi({ description: '完整文件访问URL', example: 'https://minio.example.com/d8dai/uploads/goods/2024/product-image.jpg' }),
       type: z.string().nullable().openapi({ description: '文件类型', example: 'image/jpeg' }),
       size: z.number().nullable().openapi({ description: '文件大小(字节)', example: 102400 })
     }).nullable().optional().openapi({

+ 34 - 0
packages/orders-module-mt/src/schemas/order.mt.schema.ts

@@ -215,6 +215,23 @@ export const OrderSchema = z.object({
     address: z.string().openapi({ description: '详细地址', example: '北京市朝阳区xxx路xxx号' })
   }).nullable().optional().openapi({
     description: '收货地址信息'
+  }),
+  // 订单商品信息
+  orderGoods: z.array(z.object({
+    id: z.number().int().positive().openapi({ description: '订单商品ID' }),
+    goodsId: z.number().int().positive().openapi({ description: '商品ID' }),
+    goodsName: z.string().openapi({ description: '商品名称', example: '商品A' }),
+    price: z.coerce.number<number>().openapi({ description: '商品价格', example: 99.99 }),
+    num: z.number().int().positive().openapi({ description: '商品数量', example: 1 }),
+    imageFileId: z.number().int().positive().nullable().openapi({ description: '商品图片文件ID', example: 1 }),
+    imageFile: z.object({
+      id: z.number().int().positive().openapi({ description: '文件ID' }),
+      fullUrl: z.url().openapi({ description: '完整文件访问URL', example: 'https://minio.example.com/d8dai/uploads/goods/2024/product-image.jpg' })
+    }).nullable().optional().openapi({
+      description: '商品图片信息'
+    })
+  })).optional().openapi({
+    description: '订单商品列表'
   })
 });
 
@@ -573,5 +590,22 @@ export const OrderListSchema = z.object({
     address: z.string().openapi({ description: '详细地址', example: '北京市朝阳区xxx路xxx号' })
   }).nullable().optional().openapi({
     description: '收货地址信息'
+  }),
+  // 订单商品信息
+  orderGoods: z.array(z.object({
+    id: z.number().int().positive().openapi({ description: '订单商品ID' }),
+    goodsId: z.number().int().positive().openapi({ description: '商品ID' }),
+    goodsName: z.string().openapi({ description: '商品名称', example: '商品A' }),
+    price: z.coerce.number<number>().openapi({ description: '商品价格', example: 99.99 }),
+    num: z.number().int().positive().openapi({ description: '商品数量', example: 1 }),
+    imageFileId: z.number().int().positive().nullable().openapi({ description: '商品图片文件ID', example: 1 }),
+    imageFile: z.object({
+      id: z.number().int().positive().openapi({ description: '文件ID' }),
+      fullUrl: z.url().openapi({ description: '完整文件访问URL', example: 'https://minio.example.com/d8dai/uploads/goods/2024/product-image.jpg' })
+    }).nullable().optional().openapi({
+      description: '商品图片信息'
+    })
+  })).optional().openapi({
+    description: '订单商品列表'
   })
 });

+ 68 - 0
packages/orders-module-mt/tests/integration/user-orders-routes.integration.test.ts

@@ -159,6 +159,74 @@ describe('多租户用户订单管理API集成测试', () => {
     });
   });
 
+  describe('订单商品关联验证', () => {
+    it('应该返回包含订单商品信息的订单详情', async () => {
+      // 创建测试订单和订单商品
+      const order = await testFactory.createTestOrder(testUser.id, { tenantId: 1 });
+      const testGoods = await testFactory.createTestGoods(testUser.id, { tenantId: 1 });
+      const orderGoods = await testFactory.createTestOrderGoods(order.id, testGoods.id, { tenantId: 1 });
+
+      // 查询订单详情
+      const response = await client[':id'].$get({
+        param: { id: order.id }
+      }, {
+        headers: {
+          'Authorization': `Bearer ${userToken}`
+        }
+      });
+
+      expect(response.status).toBe(200);
+      if (response.status === 200) {
+        const orderDetail = await response.json();
+
+        // 验证订单详情包含订单商品信息
+        expect(orderDetail.orderGoods).toBeDefined();
+        expect(Array.isArray(orderDetail.orderGoods)).toBe(true);
+        expect(orderDetail.orderGoods).toHaveLength(1);
+
+        const goods = orderDetail.orderGoods[0];
+        expect(goods.id).toBe(orderGoods.id);
+        expect(goods.goodsId).toBe(orderGoods.goodsId);
+        expect(goods.goodsName).toBe(orderGoods.goodsName);
+        expect(goods.price).toBe(Number(orderGoods.price));
+        expect(goods.num).toBe(orderGoods.num);
+      }
+    });
+
+    it('应该返回包含订单商品信息的订单列表', async () => {
+      // 创建测试订单和订单商品
+      const order = await testFactory.createTestOrder(testUser.id, { tenantId: 1 });
+      const testGoods = await testFactory.createTestGoods(testUser.id, { tenantId: 1 });
+      await testFactory.createTestOrderGoods(order.id, testGoods.id, { tenantId: 1 });
+
+      // 查询订单列表
+      const response = await client.index.$get({
+        query: {}
+      }, {
+        headers: {
+          'Authorization': `Bearer ${userToken}`
+        }
+      });
+
+      expect(response.status).toBe(200);
+      if (response.status === 200) {
+        const data = await response.json();
+
+        // 验证订单列表包含订单商品信息
+        expect(data.data).toHaveLength(1);
+        expect(data.data[0].orderGoods).toBeDefined();
+        expect(Array.isArray(data.data[0].orderGoods)).toBe(true);
+        expect(data.data[0].orderGoods).toHaveLength(1);
+
+        const goods = data.data[0].orderGoods[0];
+        expect(goods.goodsId).toBeGreaterThan(0);
+        expect(goods.goodsName).toBeDefined();
+        expect(goods.price).toBeGreaterThan(0);
+        expect(goods.num).toBeGreaterThan(0);
+      }
+    });
+  });
+
   describe('订单创建验证', () => {
     it('应该自动设置租户ID', async () => {
       // 创建必要的关联实体