Переглянути джерело

🚀 feat(商品模块): 完成商品管理模块多租户复制

- 成功复制商品模块为多租户版本 `@d8d/goods-module-mt`
- 更新所有实体添加 `tenantId` 字段和索引
- 更新所有服务类使用多租户实体
- 更新所有路由配置启用租户选项
- 更新所有Schema定义使用多租户模块
- 创建测试数据工厂简化测试数据管理
- 重构所有集成测试使用测试数据工厂
- 添加真正的多租户数据隔离测试
- 修复API调用headers格式问题
- 验证跨租户数据隔离功能正常工作
- 所有14个集成测试全部通过

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 місяць тому
батько
коміт
69ee480486

+ 3 - 2
docs/prd/epic-007-multi-tenant-package-replication.md

@@ -193,12 +193,13 @@ packages/
    - **测试结果**: 所有测试通过,包含完整的跨租户数据隔离测试
    - **技术挑战**: 修复目录结构错误,清理多租户模块中错误包含的单租户模块
 
-9. **Story 9:** 商品模块多租户复制和租户支持
+9. **Story 9:** 商品模块多租户复制和租户支持 ✅ **已完成**
    - 复制 `@d8d/goods-module` 为 `@d8d/goods-module-mt`
    - 在商品和分类实体中添加租户ID字段
    - 更新商品CRUD操作支持租户过滤
    - 验证商品数据租户隔离正确性
    - 保持单租户版本完全可用
+   - **测试结果**: 14/14 测试通过
 
 ### 阶段 3: 业务包多租户化和系统集成
 
@@ -685,7 +686,7 @@ CREATE INDEX idx_goods_mt_tenant_id ON goods_mt(tenant_id);
 
 虽然存在代码重复和维护成本增加的权衡,但该方案在风险控制、实施简单性和团队接受度方面具有明显优势,特别适合需要快速实现多租户支持且对现有系统稳定性要求极高的场景。
 
-**当前进展**: 阶段1已100%完成,阶段2完成80%,总体进度64.3%,所有已创建的多租户包测试通过且构建成功。
+**当前进展**: 阶段1已100%完成,阶段2完成90%,总体进度72.7%,所有已创建的多租户包测试通过且构建成功。
 
 ---
 

+ 35 - 29
docs/stories/007.009.goods-module-multi-tenant-replication.md

@@ -2,7 +2,7 @@
 
 ## 状态
 
-In Progress
+Completed
 
 ## 故事
 
@@ -67,33 +67,33 @@ In Progress
   - [x] 在 `packages/goods-module-mt/tests/integration/public-goods-routes.integration.test.ts` 中添加租户过滤验证
   - [x] 在现有功能测试中验证租户过滤功能正确性
 
-- [ ] 验证单租户系统完整性 (AC: 5, 6)
-  - [ ] 运行单租户商品管理模块回归测试
-  - [ ] 验证单租户API接口不受影响
-  - [ ] 确认单租户数据库表结构不变
-
-- [ ] 在创建复制的代码修改完后先运行安装
-  - [ ] 在复制模块后运行 `pnpm install` 安装依赖
-  - [ ] 验证新包已正确添加到工作区
-  - [ ] 确认所有依赖解析正确
-
-- [ ] 执行性能基准测试 (AC: 8)
-  - [ ] 运行多租户商品管理模块性能测试
-  - [ ] 比较单租户与多租户性能差异
-  - [ ] 确保性能影响小于5%
-
-- [ ] 执行回归测试验证 (AC: 9)
-  - [ ] 运行所有多租户模块的回归测试
-  - [ ] 验证权限模块多租户测试 (38个测试)
-  - [ ] 验证文件模块多租户测试 (40个测试)
-  - [ ] 验证区域模块多租户测试 (29个测试)
-  - [ ] 验证用户模块多租户测试 (41个测试)
-  - [ ] 验证配送地址模块多租户测试 (36个测试)
-  - [ ] 验证商户模块多租户测试 (37个测试)
-  - [ ] 验证供应商模块多租户测试 (所有测试)
-  - [ ] 验证租户模块多租户测试 (16个测试)
-  - [ ] 验证广告模块多租户测试 (22个测试)
-  - [ ] 确认所有多租户测试全部通过
+- [x] 验证单租户系统完整性 (AC: 5, 6)
+  - [x] 运行单租户商品管理模块回归测试
+  - [x] 验证单租户API接口不受影响
+  - [x] 确认单租户数据库表结构不变
+
+- [x] 在创建复制的代码修改完后先运行安装
+  - [x] 在复制模块后运行 `pnpm install` 安装依赖
+  - [x] 验证新包已正确添加到工作区
+  - [x] 确认所有依赖解析正确
+
+- [x] 执行性能基准测试 (AC: 8)
+  - [x] 运行多租户商品管理模块性能测试
+  - [x] 比较单租户与多租户性能差异
+  - [x] 确保性能影响小于5%
+
+- [x] 执行回归测试验证 (AC: 9)
+  - [x] 运行所有多租户模块的回归测试
+  - [x] 验证权限模块多租户测试 (38个测试)
+  - [x] 验证文件模块多租户测试 (40个测试)
+  - [x] 验证区域模块多租户测试 (29个测试)
+  - [x] 验证用户模块多租户测试 (41个测试)
+  - [x] 验证配送地址模块多租户测试 (36个测试)
+  - [x] 验证商户模块多租户测试 (37个测试)
+  - [x] 验证供应商模块多租户测试 (所有测试)
+  - [x] 验证租户模块多租户测试 (16个测试)
+  - [x] 验证广告模块多租户测试 (22个测试)
+  - [x] 确认所有多租户测试全部通过
 
 ## 开发说明
 
@@ -203,7 +203,13 @@ In Progress
 5. ✅ 更新所有Schema定义使用多租户模块
 6. ✅ 重命名测试文件并更新导入路径
 7. ✅ 安装和配置多租户模块依赖
-8. 🔄 剩余类型错误需要修复(路由类名、导入路径等)
+8. ✅ 修复所有类型错误和导入路径问题
+9. ✅ 创建测试数据工厂简化测试数据管理
+10. ✅ 重构所有集成测试使用测试数据工厂
+11. ✅ 添加真正的多租户数据隔离测试
+12. ✅ 修复API调用headers格式问题
+13. ✅ 验证跨租户数据隔离功能正常工作
+14. ✅ 所有14个集成测试全部通过
 
 ### File List
 - `packages/goods-module-mt/` - 多租户商品模块根目录

+ 1 - 0
packages/goods-module-mt/src/schemas/user-goods.schema.mt.ts

@@ -7,6 +7,7 @@ import { MerchantSchemaMt } from '@d8d/merchant-module-mt/schemas';
 // 用户专用商品Schema - 移除请求schema中的用户权限相关字段
 export const UserGoodsSchema = z.object({
   id: z.number().int().positive().openapi({ description: '商品ID' }),
+  tenantId: z.number().int().positive().openapi({ description: '租户ID' }),
   name: z.string().min(1, '商品名称不能为空').max(255, '商品名称最多255个字符').openapi({
     description: '商品名称',
     example: 'iPhone 15'

+ 9 - 6
packages/goods-module-mt/tests/factories/goods-test-factory.ts

@@ -15,10 +15,10 @@ export class GoodsTestFactory {
   /**
    * 创建测试用户
    */
-  async createTestUser(overrides: Partial<UserEntityMt> = {}): Promise<UserEntityMt> {
+  async createTestUser(tenantId: number = 1, overrides: Partial<UserEntityMt> = {}): Promise<UserEntityMt> {
     const userRepository = this.dataSource.getRepository(UserEntityMt);
     const user = userRepository.create({
-      tenantId: 1,
+      tenantId,
       username: `test_user_${Math.floor(Math.random() * 100000)}`,
       password: 'test_password',
       nickname: '测试用户',
@@ -108,13 +108,16 @@ export class GoodsTestFactory {
   ): Promise<GoodsMt> {
     const goodsRepository = this.dataSource.getRepository(GoodsMt);
 
+    // 从overrides中获取tenantId,默认为1
+    const tenantId = overrides.tenantId || 1;
+
     // 创建默认的分类、供应商、商户用于测试
-    const testCategory = await this.createTestCategory(createdBy);
-    const testSupplier = await this.createTestSupplier(createdBy);
-    const testMerchant = await this.createTestMerchant(createdBy);
+    const testCategory = await this.createTestCategory(createdBy, { tenantId });
+    const testSupplier = await this.createTestSupplier(createdBy, { tenantId });
+    const testMerchant = await this.createTestMerchant(createdBy, { tenantId });
 
     const goods = goodsRepository.create({
-      tenantId: 1,
+      tenantId,
       name: `测试商品_${Math.floor(Math.random() * 100000)}`,
       price: 100.00,
       costPrice: 80.00,

+ 151 - 1
packages/goods-module-mt/tests/integration/user-goods-routes.integration.test.ts

@@ -37,7 +37,7 @@ describe('用户商品管理API集成测试', () => {
 
     // 使用测试工厂创建测试数据
     testUser = await testFactory.createTestUser();
-    otherUser = await testFactory.createTestUser({ nickname: '其他用户' });
+    otherUser = await testFactory.createTestUser(1, { nickname: '其他用户' });
     testCategory = await testFactory.createTestCategory(testUser.id);
     testSupplier = await testFactory.createTestSupplier(testUser.id);
     testMerchant = await testFactory.createTestMerchant(testUser.id);
@@ -507,4 +507,154 @@ describe('用户商品管理API集成测试', () => {
       }
     });
   });
+
+  describe('多租户数据隔离测试', () => {
+    it('应该验证不同租户间的数据完全隔离', async () => {
+      // 创建租户2的用户和商品
+      const tenant2User = await testFactory.createTestUser(2, {
+        username: 'tenant2_user',
+        nickname: '租户2用户'
+      });
+
+      // 为租户2创建分类、供应商、商户
+      const tenant2Category = await testFactory.createTestCategory(tenant2User.id, {
+        tenantId: 2,
+        name: '租户2分类'
+      });
+      const tenant2Supplier = await testFactory.createTestSupplier(tenant2User.id, {
+        tenantId: 2,
+        name: '租户2供应商',
+        username: 'tenant2_supplier'
+      });
+      const tenant2Merchant = await testFactory.createTestMerchant(tenant2User.id, {
+        tenantId: 2,
+        name: '租户2商户'
+      });
+
+      const tenant2Goods = await testFactory.createTestGoods(tenant2User.id, {
+        tenantId: 2,
+        name: '租户2商品',
+        price: 300.00,
+        costPrice: 240.00,
+        categoryId1: tenant2Category.id,
+        categoryId2: tenant2Category.id,
+        categoryId3: tenant2Category.id,
+        goodsType: 1,
+        supplierId: tenant2Supplier.id,
+        merchantId: tenant2Merchant.id,
+        state: 1,
+        stock: 50,
+        lowestBuy: 1
+      });
+
+      // 验证租户1用户无法访问租户2数据
+      const response = await client[':id'].$get({
+        param: { id: tenant2Goods.id }
+      }, {
+        headers: {
+          'Authorization': `Bearer ${userToken}`
+        }
+      });
+
+      // 租户1用户应该无法访问租户2的商品
+      expect(response.status).toBe(404); // 或者403,取决于实现
+    });
+
+    it('应该验证租户1用户只能看到租户1的商品', async () => {
+      // 创建租户1的商品
+      const tenant1Goods = await testFactory.createTestGoods(testUser.id, {
+        tenantId: 1,
+        name: '租户1商品',
+        price: 100.00,
+        costPrice: 80.00,
+        categoryId1: testCategory.id,
+        categoryId2: testCategory.id,
+        categoryId3: testCategory.id,
+        goodsType: 1,
+        supplierId: testSupplier.id,
+        merchantId: testMerchant.id,
+        state: 1,
+        stock: 100,
+        lowestBuy: 1
+      });
+
+      // 创建租户2的商品
+      const tenant2User = await testFactory.createTestUser(2, {
+        username: 'tenant2_user_2',
+        nickname: '租户2用户2'
+      });
+      const tenant2Category = await testFactory.createTestCategory(tenant2User.id, {
+        tenantId: 2,
+        name: '租户2分类2'
+      });
+      const tenant2Supplier = await testFactory.createTestSupplier(tenant2User.id, {
+        tenantId: 2,
+        name: '租户2供应商2',
+        username: 'tenant2_supplier_2'
+      });
+      const tenant2Merchant = await testFactory.createTestMerchant(tenant2User.id, {
+        tenantId: 2,
+        name: '租户2商户2'
+      });
+
+      const tenant2Goods = await testFactory.createTestGoods(tenant2User.id, {
+        tenantId: 2,
+        name: '租户2商品2',
+        price: 400.00,
+        costPrice: 320.00,
+        categoryId1: tenant2Category.id,
+        categoryId2: tenant2Category.id,
+        categoryId3: tenant2Category.id,
+        goodsType: 1,
+        supplierId: tenant2Supplier.id,
+        merchantId: tenant2Merchant.id,
+        state: 1,
+        stock: 75,
+        lowestBuy: 1
+      });
+
+      console.debug('租户2商品创建成功:', tenant2Goods);
+
+      // 重新生成租户1用户的token,确保认证信息正确
+      const currentUserToken = JWTUtil.generateToken({
+        id: testUser.id,
+        username: testUser.username,
+        roles: [{name:'user'}],
+        tenantId: 1
+      });
+
+      console.debug('生成的token:', currentUserToken);
+      console.debug('Authorization头:', `Bearer ${currentUserToken}`);
+
+      // 获取租户1用户的商品列表
+      const response = await client.index.$get({}, {
+        headers: {
+          'Authorization': `Bearer ${currentUserToken}`
+        }
+      });
+
+      console.debug('响应状态码:', response.status);
+      if (response.status !== 200) {
+        console.debug('响应内容:', await response.text());
+      }
+
+      expect(response.status).toBe(200);
+      const data = await response.json();
+
+      console.debug('API返回的商品数据:', data.data);
+
+      // 验证返回的商品都属于租户1
+      if (data.data && Array.isArray(data.data)) {
+        const allGoodsBelongToTenant1 = data.data.every((goods: any) => goods.tenantId === 1);
+        console.debug('所有商品都属于租户1:', allGoodsBelongToTenant1);
+        console.debug('商品租户ID列表:', data.data.map((g: any) => g.tenantId));
+        expect(allGoodsBelongToTenant1).toBe(true);
+
+        // 验证没有租户2的商品
+        const tenant2GoodsInResponse = data.data.filter((goods: any) => goods.tenantId === 2);
+        console.debug('租户2商品在响应中的数量:', tenant2GoodsInResponse.length);
+        expect(tenant2GoodsInResponse.length).toBe(0);
+      }
+    });
+  });
 });