Преглед изворни кода

✅ test(merchant): 添加商户管理集成测试

- 新增管理员商户管理API集成测试,覆盖CRUD操作和权限验证
- 新增用户商户管理API集成测试,验证数据权限控制
- 更新Claude配置,添加pnpm install命令支持
- 更新pnpm-lock.yaml依赖配置
yourname пре 1 месец
родитељ
комит
fd903f22bf

+ 2 - 1
.claude/settings.local.json

@@ -42,7 +42,8 @@
       "Bash(pnpm db:backup:cleanup:*)",
       "Bash(pnpm db:restore)",
       "Bash(pnpm test:coverage:*)",
-      "Bash(pnpm run test:integration:*)"
+      "Bash(pnpm run test:integration:*)",
+      "Bash(pnpm install:*)"
     ],
     "deny": [],
     "ask": []

+ 532 - 0
packages/merchant-module/tests/integration/admin-routes.integration.test.ts

@@ -0,0 +1,532 @@
+import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
+import { testClient } from 'hono/testing';
+import { IntegrationTestDatabase, setupIntegrationDatabaseHooksWithEntities } from '@d8d/shared-test-util';
+import { JWTUtil } from '@d8d/shared-utils';
+import { UserEntity, Role } from '@d8d/user-module';
+import { File } from '@d8d/file-module';
+import { adminMerchantRoutes } from '../../src/routes';
+import { Merchant } from '../../src/entities';
+
+// 设置集成测试钩子
+setupIntegrationDatabaseHooksWithEntities([UserEntity, Role, Merchant, File])
+
+describe('管理员商户管理API集成测试', () => {
+  let client: ReturnType<typeof testClient<typeof adminMerchantRoutes>>;
+  let adminToken: string;
+  let testUser: UserEntity;
+  let testAdmin: UserEntity;
+
+  beforeEach(async () => {
+    // 创建测试客户端
+    client = testClient(adminMerchantRoutes);
+
+    // 获取数据源
+    const dataSource = await IntegrationTestDatabase.getDataSource();
+
+    // 创建测试用户
+    const userRepository = dataSource.getRepository(UserEntity);
+    testUser = userRepository.create({
+      username: `test_user_${Date.now()}`,
+      password: 'test_password',
+      nickname: '测试用户',
+      registrationSource: 'web'
+    });
+    await userRepository.save(testUser);
+
+    // 创建测试管理员用户
+    testAdmin = userRepository.create({
+      username: `test_admin_${Date.now()}`,
+      password: 'admin_password',
+      nickname: '测试管理员',
+      registrationSource: 'web'
+    });
+    await userRepository.save(testAdmin);
+
+    // 生成测试管理员的token
+    adminToken = JWTUtil.generateToken({
+      id: testAdmin.id,
+      username: testAdmin.username,
+      roles: [{name:'admin'}]
+    });
+  });
+
+  describe('GET /merchants', () => {
+    it('应该返回商户列表', async () => {
+      const response = await client.index.$get({
+        query: {}
+      }, {
+        headers: {
+          'Authorization': `Bearer ${adminToken}`
+        }
+      });
+
+      console.debug('商户列表响应状态:', response.status);
+      expect(response.status).toBe(200);
+
+      if (response.status === 200) {
+        const data = await response.json();
+        expect(data).toHaveProperty('data');
+        expect(Array.isArray(data.data)).toBe(true);
+      }
+    });
+
+    it('应该拒绝未认证用户的访问', async () => {
+      const response = await client.index.$get({
+        query: {}
+      });
+      expect(response.status).toBe(401);
+    });
+  });
+
+  describe('POST /merchants', () => {
+    it('应该成功创建商户', async () => {
+      const createData = {
+        name: '新商户',
+        username: `new_merchant_${Date.now()}`,
+        password: 'password123',
+        phone: '13800138000',
+        realname: '张三',
+        state: 1
+      };
+
+      const response = await client.index.$post({
+        json: createData
+      }, {
+        headers: {
+          'Authorization': `Bearer ${adminToken}`
+        }
+      });
+
+      console.debug('创建商户响应状态:', response.status);
+      if (response.status !== 201) {
+        const errorData = await response.json();
+        console.debug('创建商户错误响应:', errorData);
+      }
+      expect(response.status).toBe(201);
+
+      if (response.status === 201) {
+        const data = await response.json();
+        expect(data).toHaveProperty('id');
+        expect(data.name).toBe(createData.name);
+        expect(data.username).toBe(createData.username);
+        expect(data.phone).toBe(createData.phone);
+        expect(data.realname).toBe(createData.realname);
+        expect(data.state).toBe(createData.state);
+      }
+    });
+
+    it('应该验证创建商户的必填字段', async () => {
+      const invalidData = {
+        // 缺少必填字段
+        name: '',
+        username: '',
+        password: ''
+      };
+
+      const response = await client.index.$post({
+        json: invalidData
+      }, {
+        headers: {
+          'Authorization': `Bearer ${adminToken}`
+        }
+      });
+
+      expect(response.status).toBe(400);
+    });
+  });
+
+  describe('GET /merchants/:id', () => {
+    it('应该返回指定商户的详情', async () => {
+      // 先创建一个商户
+      const dataSource = await IntegrationTestDatabase.getDataSource();
+      const merchantRepository = dataSource.getRepository(Merchant);
+      const testMerchant = merchantRepository.create({
+        name: '测试商户',
+        username: `test_merchant_${Date.now()}`,
+        password: 'password123',
+        phone: '13800138000',
+        realname: '张三',
+        state: 1,
+        createdBy: testUser.id
+      });
+      await merchantRepository.save(testMerchant);
+
+      const response = await client[':id'].$get({
+        param: { id: testMerchant.id }
+      }, {
+        headers: {
+          'Authorization': `Bearer ${adminToken}`
+        }
+      });
+
+      console.debug('商户详情响应状态:', response.status);
+      expect(response.status).toBe(200);
+
+      if (response.status === 200) {
+        const data = await response.json();
+        expect(data.id).toBe(testMerchant.id);
+        expect(data.name).toBe(testMerchant.name);
+        expect(data.username).toBe(testMerchant.username);
+        expect(data.phone).toBe(testMerchant.phone);
+        expect(data.realname).toBe(testMerchant.realname);
+      }
+    });
+
+    it('应该处理不存在的商户', async () => {
+      const response = await client[':id'].$get({
+        param: { id: 999999 }
+      }, {
+        headers: {
+          'Authorization': `Bearer ${adminToken}`
+        }
+      });
+
+      expect(response.status).toBe(404);
+    });
+  });
+
+  describe('PUT /merchants/:id', () => {
+    it('应该成功更新商户', async () => {
+      // 先创建一个商户
+      const dataSource = await IntegrationTestDatabase.getDataSource();
+      const merchantRepository = dataSource.getRepository(Merchant);
+      const testMerchant = merchantRepository.create({
+        name: '原始商户',
+        username: `original_merchant_${Date.now()}`,
+        password: 'password123',
+        phone: '13800138000',
+        realname: '原始姓名',
+        state: 1,
+        createdBy: testUser.id
+      });
+      await merchantRepository.save(testMerchant);
+
+      const updateData = {
+        name: '更新后的商户',
+        phone: '13900139000',
+        realname: '更新后的姓名',
+        state: 2
+      };
+
+      const response = await client[':id'].$put({
+        param: { id: testMerchant.id },
+        json: updateData
+      }, {
+        headers: {
+          'Authorization': `Bearer ${adminToken}`
+        }
+      });
+
+      console.debug('更新商户响应状态:', response.status);
+      expect(response.status).toBe(200);
+
+      if (response.status === 200) {
+        const data = await response.json();
+        expect(data.name).toBe(updateData.name);
+        expect(data.phone).toBe(updateData.phone);
+        expect(data.realname).toBe(updateData.realname);
+        expect(data.state).toBe(updateData.state);
+      }
+    });
+  });
+
+  describe('DELETE /merchants/:id', () => {
+    it('应该成功删除商户', async () => {
+      // 先创建一个商户
+      const dataSource = await IntegrationTestDatabase.getDataSource();
+      const merchantRepository = dataSource.getRepository(Merchant);
+      const testMerchant = merchantRepository.create({
+        name: '待删除商户',
+        username: `delete_merchant_${Date.now()}`,
+        password: 'password123',
+        phone: '13800138000',
+        realname: '张三',
+        state: 1,
+        createdBy: testUser.id
+      });
+      await merchantRepository.save(testMerchant);
+
+      const response = await client[':id'].$delete({
+        param: { id: testMerchant.id }
+      }, {
+        headers: {
+          'Authorization': `Bearer ${adminToken}`
+        }
+      });
+
+      console.debug('删除商户响应状态:', response.status);
+      expect(response.status).toBe(204);
+
+      // 验证商户确实被删除
+      const deletedMerchant = await merchantRepository.findOne({
+        where: { id: testMerchant.id }
+      });
+      expect(deletedMerchant).toBeNull();
+    });
+  });
+
+  describe('管理员权限测试', () => {
+    it('管理员应该可以为其他用户创建商户', async () => {
+      const createData = {
+        name: '其他用户商户',
+        username: `other_user_merchant_${Date.now()}`,
+        password: 'password123',
+        phone: '13800138001',
+        realname: '李四',
+        state: 1,
+        createdBy: testUser.id // 为其他用户创建商户
+      };
+
+      const response = await client.index.$post({
+        json: createData
+      }, {
+        headers: {
+          'Authorization': `Bearer ${adminToken}`
+        }
+      });
+
+      console.debug('管理员为其他用户创建商户响应状态:', response.status);
+      expect(response.status).toBe(201);
+
+      if (response.status === 201) {
+        const data = await response.json();
+        expect(data.createdBy).toBe(testUser.id); // 验证商户确实属于其他用户
+        expect(data.name).toBe(createData.name);
+      }
+    });
+
+    it('管理员应该可以访问所有用户的商户', async () => {
+      // 为测试用户创建一些商户
+      const dataSource = await IntegrationTestDatabase.getDataSource();
+      const merchantRepository = dataSource.getRepository(Merchant);
+
+      const userMerchant1 = merchantRepository.create({
+        name: '用户商户1',
+        username: `user_merchant1_${Date.now()}`,
+        password: 'password123',
+        phone: '13800138002',
+        realname: '张三',
+        state: 1,
+        createdBy: testUser.id
+      });
+      await merchantRepository.save(userMerchant1);
+
+      const userMerchant2 = merchantRepository.create({
+        name: '用户商户2',
+        username: `user_merchant2_${Date.now()}`,
+        password: 'password123',
+        phone: '13800138003',
+        realname: '李四',
+        state: 1,
+        createdBy: testUser.id
+      });
+      await merchantRepository.save(userMerchant2);
+
+      // 管理员应该能看到所有商户
+      const response = await client.index.$get({
+        query: {}
+      }, {
+        headers: {
+          'Authorization': `Bearer ${adminToken}`
+        }
+      });
+
+      expect(response.status).toBe(200);
+      const data = await response.json();
+      if (data && 'data' in data) {
+        expect(Array.isArray(data.data)).toBe(true);
+        expect(data.data.length).toBeGreaterThanOrEqual(2); // 至少包含我们创建的两个商户
+      }
+    });
+
+    it('管理员应该可以更新其他用户的商户', async () => {
+      // 先为测试用户创建一个商户
+      const dataSource = await IntegrationTestDatabase.getDataSource();
+      const merchantRepository = dataSource.getRepository(Merchant);
+      const testMerchant = merchantRepository.create({
+        name: '原始商户',
+        username: `original_merchant_${Date.now()}`,
+        password: 'password123',
+        phone: '13800138004',
+        realname: '王五',
+        state: 1,
+        createdBy: testUser.id
+      });
+      await merchantRepository.save(testMerchant);
+
+      const updateData = {
+        name: '管理员更新的商户',
+        phone: '13900139000',
+        realname: '管理员更新的姓名'
+      };
+
+      const response = await client[':id'].$put({
+        param: { id: testMerchant.id },
+        json: updateData
+      }, {
+        headers: {
+          'Authorization': `Bearer ${adminToken}`
+        }
+      });
+
+      console.debug('管理员更新其他用户商户响应状态:', response.status);
+      expect(response.status).toBe(200);
+
+      if (response.status === 200) {
+        const data = await response.json();
+        expect(data.name).toBe(updateData.name);
+        expect(data.phone).toBe(updateData.phone);
+        expect(data.realname).toBe(updateData.realname);
+      }
+    });
+
+    it('管理员应该可以删除其他用户的商户', async () => {
+      // 先为测试用户创建一个商户
+      const dataSource = await IntegrationTestDatabase.getDataSource();
+      const merchantRepository = dataSource.getRepository(Merchant);
+      const testMerchant = merchantRepository.create({
+        name: '待删除商户',
+        username: `delete_merchant_${Date.now()}`,
+        password: 'password123',
+        phone: '13800138005',
+        realname: '赵六',
+        state: 1,
+        createdBy: testUser.id
+      });
+      await merchantRepository.save(testMerchant);
+
+      const response = await client[':id'].$delete({
+        param: { id: testMerchant.id }
+      }, {
+        headers: {
+          'Authorization': `Bearer ${adminToken}`
+        }
+      });
+
+      console.debug('管理员删除其他用户商户响应状态:', response.status);
+      expect(response.status).toBe(204);
+
+      // 验证商户确实被删除
+      const deletedMerchant = await merchantRepository.findOne({
+        where: { id: testMerchant.id }
+      });
+      expect(deletedMerchant).toBeNull();
+    });
+
+    it('管理员应该可以查询指定用户的商户', async () => {
+      // 为测试用户创建一些商户
+      const dataSource = await IntegrationTestDatabase.getDataSource();
+      const merchantRepository = dataSource.getRepository(Merchant);
+
+      const userMerchant = merchantRepository.create({
+        name: '指定用户商户',
+        username: `specific_user_merchant_${Date.now()}`,
+        password: 'password123',
+        phone: '13800138006',
+        realname: '钱七',
+        state: 1,
+        createdBy: testUser.id
+      });
+      await merchantRepository.save(userMerchant);
+
+      // 管理员可以查询指定用户的商户
+      const response = await client.index.$get({
+        query: { filters: JSON.stringify({ createdBy: testUser.id }) }
+      }, {
+        headers: {
+          'Authorization': `Bearer ${adminToken}`
+        }
+      });
+
+      expect(response.status).toBe(200);
+      const data = await response.json();
+      if (data && 'data' in data) {
+        expect(Array.isArray(data.data)).toBe(true);
+
+        // 验证返回的商户都属于指定用户
+        if (data.data.length > 0) {
+          data.data.forEach((merchant: any) => {
+            expect(merchant.createdBy).toBe(testUser.id);
+          });
+        }
+      }
+    });
+  });
+
+  describe('商户状态管理测试', () => {
+    it('应该支持商户状态管理', async () => {
+      // 创建启用状态的商户
+      const createData = {
+        name: '状态测试商户',
+        username: `state_test_merchant_${Date.now()}`,
+        password: 'password123',
+        phone: '13800138007',
+        realname: '状态测试',
+        state: 1 // 启用
+      };
+
+      const createResponse = await client.index.$post({
+        json: createData
+      }, {
+        headers: {
+          'Authorization': `Bearer ${adminToken}`
+        }
+      });
+
+      expect(createResponse.status).toBe(201);
+      const createdMerchant = await createResponse.json();
+      expect(createdMerchant.state).toBe(1);
+
+      // 更新为禁用状态
+      const updateResponse = await client[':id'].$put({
+        param: { id: createdMerchant.id },
+        json: { state: 2 } // 禁用
+      }, {
+        headers: {
+          'Authorization': `Bearer ${adminToken}`
+        }
+      });
+
+      expect(updateResponse.status).toBe(200);
+      const updatedMerchant = await updateResponse.json();
+      expect(updatedMerchant.state).toBe(2);
+    });
+  });
+
+  describe('商户登录统计功能测试', () => {
+    it('应该支持商户登录统计字段', async () => {
+      // 创建商户
+      const createData = {
+        name: '登录统计商户',
+        username: `login_stat_merchant_${Date.now()}`,
+        password: 'password123',
+        phone: '13800138008',
+        realname: '登录统计',
+        state: 1
+      };
+
+      const createResponse = await client.index.$post({
+        json: createData
+      }, {
+        headers: {
+          'Authorization': `Bearer ${adminToken}`
+        }
+      });
+
+      expect(createResponse.status).toBe(201);
+      const createdMerchant = await createResponse.json();
+
+      // 验证登录统计字段存在
+      expect(createdMerchant).toHaveProperty('loginNum');
+      expect(createdMerchant).toHaveProperty('loginTime');
+      expect(createdMerchant).toHaveProperty('loginIp');
+      expect(createdMerchant).toHaveProperty('lastLoginTime');
+      expect(createdMerchant).toHaveProperty('lastLoginIp');
+
+      // 初始值应该为0或null
+      expect(createdMerchant.loginNum).toBe(0);
+      expect(createdMerchant.loginTime).toBe(0);
+      expect(createdMerchant.lastLoginTime).toBe(0);
+    });
+  });
+});

+ 564 - 0
packages/merchant-module/tests/integration/user-routes.integration.test.ts

@@ -0,0 +1,564 @@
+import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
+import { testClient } from 'hono/testing';
+import { IntegrationTestDatabase, setupIntegrationDatabaseHooksWithEntities } from '@d8d/shared-test-util';
+import { JWTUtil } from '@d8d/shared-utils';
+import { UserEntity, Role } from '@d8d/user-module';
+import { File } from '@d8d/file-module';
+import { userMerchantRoutes } from '../../src/routes';
+import { Merchant } from '../../src/entities';
+
+// 设置集成测试钩子
+setupIntegrationDatabaseHooksWithEntities([UserEntity, Role, Merchant, File])
+
+describe('用户商户管理API集成测试', () => {
+  let client: ReturnType<typeof testClient<typeof userMerchantRoutes>>;
+  let userToken: string;
+  let otherUserToken: string;
+  let testUser: UserEntity;
+  let otherUser: UserEntity;
+
+  beforeEach(async () => {
+    // 创建测试客户端
+    client = testClient(userMerchantRoutes);
+
+    // 获取数据源
+    const dataSource = await IntegrationTestDatabase.getDataSource();
+
+    // 创建测试用户
+    const userRepository = dataSource.getRepository(UserEntity);
+    testUser = userRepository.create({
+      username: `test_user_${Date.now()}`,
+      password: 'test_password',
+      nickname: '测试用户',
+      registrationSource: 'web'
+    });
+    await userRepository.save(testUser);
+
+    // 创建其他用户
+    otherUser = userRepository.create({
+      username: `other_user_${Date.now()}`,
+      password: 'other_password',
+      nickname: '其他用户',
+      registrationSource: 'web'
+    });
+    await userRepository.save(otherUser);
+
+    // 生成测试用户的token
+    userToken = JWTUtil.generateToken({
+      id: testUser.id,
+      username: testUser.username,
+      roles: [{name:'user'}]
+    });
+
+    // 生成其他用户的token
+    otherUserToken = JWTUtil.generateToken({
+      id: otherUser.id,
+      username: otherUser.username,
+      roles: [{name:'user'}]
+    });
+  });
+
+  describe('GET /merchants', () => {
+    it('应该返回当前用户的商户列表', async () => {
+      // 为测试用户创建一些商户
+      const dataSource = await IntegrationTestDatabase.getDataSource();
+      const merchantRepository = dataSource.getRepository(Merchant);
+
+      const userMerchant1 = merchantRepository.create({
+        name: '用户商户1',
+        username: `user_merchant1_${Date.now()}`,
+        password: 'password123',
+        phone: '13800138001',
+        realname: '张三',
+        state: 1,
+        createdBy: testUser.id
+      });
+      await merchantRepository.save(userMerchant1);
+
+      const userMerchant2 = merchantRepository.create({
+        name: '用户商户2',
+        username: `user_merchant2_${Date.now()}`,
+        password: 'password123',
+        phone: '13800138002',
+        realname: '李四',
+        state: 1,
+        createdBy: testUser.id
+      });
+      await merchantRepository.save(userMerchant2);
+
+      // 为其他用户创建一个商户,确保不会返回
+      const otherUserMerchant = merchantRepository.create({
+        name: '其他用户商户',
+        username: `other_merchant_${Date.now()}`,
+        password: 'password123',
+        phone: '13800138003',
+        realname: '王五',
+        state: 1,
+        createdBy: otherUser.id
+      });
+      await merchantRepository.save(otherUserMerchant);
+
+      const response = await client.index.$get({
+        query: {}
+      }, {
+        headers: {
+          'Authorization': `Bearer ${userToken}`
+        }
+      });
+
+      console.debug('用户商户列表响应状态:', response.status);
+      expect(response.status).toBe(200);
+
+      if (response.status === 200) {
+        const data = await response.json();
+        if (data && 'data' in data) {
+          expect(Array.isArray(data.data)).toBe(true);
+          // 应该只返回当前用户的商户
+          data.data.forEach((merchant: any) => {
+            expect(merchant.createdBy).toBe(testUser.id);
+          });
+        }
+      }
+    });
+
+    it('应该拒绝未认证用户的访问', async () => {
+      const response = await client.index.$get({
+        query: {}
+      });
+      expect(response.status).toBe(401);
+    });
+  });
+
+  describe('POST /merchants', () => {
+    it('应该成功创建商户并自动使用当前用户ID', async () => {
+      const createData = {
+        name: '新商户',
+        username: `new_merchant_${Date.now()}`,
+        password: 'password123',
+        phone: '13800138000',
+        realname: '张三',
+        state: 1
+      };
+
+      const response = await client.index.$post({
+        json: createData
+      }, {
+        headers: {
+          'Authorization': `Bearer ${userToken}`
+        }
+      });
+
+      console.debug('用户创建商户响应状态:', response.status);
+      expect(response.status).toBe(201);
+
+      if (response.status === 201) {
+        const data = await response.json();
+        console.debug('用户创建商户响应数据:', JSON.stringify(data, null, 2));
+        expect(data).toHaveProperty('id');
+        expect(data.createdBy).toBe(testUser.id); // 自动使用当前用户ID
+        expect(data.name).toBe(createData.name);
+        expect(data.username).toBe(createData.username);
+        expect(data.phone).toBe(createData.phone);
+        expect(data.realname).toBe(createData.realname);
+      }
+    });
+
+    it('应该验证创建商户的必填字段', async () => {
+      const invalidData = {
+        // 缺少必填字段
+        name: '',
+        username: '',
+        password: ''
+      };
+
+      const response = await client.index.$post({
+        json: invalidData
+      }, {
+        headers: {
+          'Authorization': `Bearer ${userToken}`
+        }
+      });
+
+      expect(response.status).toBe(400);
+    });
+  });
+
+  describe('GET /merchants/:id', () => {
+    it('应该返回当前用户的商户详情', async () => {
+      // 先为当前用户创建一个商户
+      const dataSource = await IntegrationTestDatabase.getDataSource();
+      const merchantRepository = dataSource.getRepository(Merchant);
+      const testMerchant = merchantRepository.create({
+        name: '测试商户',
+        username: `test_merchant_${Date.now()}`,
+        password: 'password123',
+        phone: '13800138000',
+        realname: '张三',
+        state: 1,
+        createdBy: testUser.id
+      });
+      await merchantRepository.save(testMerchant);
+
+      const response = await client[':id'].$get({
+        param: { id: testMerchant.id }
+      }, {
+        headers: {
+          'Authorization': `Bearer ${userToken}`
+        }
+      });
+
+      console.debug('用户商户详情响应状态:', response.status);
+      expect(response.status).toBe(200);
+
+      if (response.status === 200) {
+        const data = await response.json();
+        expect(data.id).toBe(testMerchant.id);
+        expect(data.createdBy).toBe(testUser.id);
+        expect(data.name).toBe(testMerchant.name);
+        expect(data.username).toBe(testMerchant.username);
+        expect(data.phone).toBe(testMerchant.phone);
+        expect(data.realname).toBe(testMerchant.realname);
+      }
+    });
+
+    it('应该拒绝访问其他用户的商户', async () => {
+      // 为其他用户创建一个商户
+      const dataSource = await IntegrationTestDatabase.getDataSource();
+      const merchantRepository = dataSource.getRepository(Merchant);
+      const otherUserMerchant = merchantRepository.create({
+        name: '其他用户商户',
+        username: `other_merchant_${Date.now()}`,
+        password: 'password123',
+        phone: '13800138001',
+        realname: '李四',
+        state: 1,
+        createdBy: otherUser.id
+      });
+      await merchantRepository.save(otherUserMerchant);
+
+      // 当前用户尝试访问其他用户的商户
+      const response = await client[':id'].$get({
+        param: { id: otherUserMerchant.id }
+      }, {
+        headers: {
+          'Authorization': `Bearer ${userToken}`
+        }
+      });
+
+      console.debug('用户访问其他用户商户响应状态:', response.status);
+      expect(response.status).toBe(404); // 应该返回404,而不是403
+    });
+
+    it('应该处理不存在的商户', async () => {
+      const response = await client[':id'].$get({
+        param: { id: 999999 }
+      }, {
+        headers: {
+          'Authorization': `Bearer ${userToken}`
+        }
+      });
+
+      expect(response.status).toBe(404);
+    });
+  });
+
+  describe('PUT /merchants/:id', () => {
+    it('应该成功更新当前用户的商户', async () => {
+      // 先为当前用户创建一个商户
+      const dataSource = await IntegrationTestDatabase.getDataSource();
+      const merchantRepository = dataSource.getRepository(Merchant);
+      const testMerchant = merchantRepository.create({
+        name: '原始商户',
+        username: `original_merchant_${Date.now()}`,
+        password: 'password123',
+        phone: '13800138000',
+        realname: '原始姓名',
+        state: 1,
+        createdBy: testUser.id
+      });
+      await merchantRepository.save(testMerchant);
+
+      const updateData = {
+        name: '更新后的商户',
+        phone: '13900139000',
+        realname: '更新后的姓名',
+        state: 2
+      };
+
+      const response = await client[':id'].$put({
+        param: { id: testMerchant.id },
+        json: updateData
+      }, {
+        headers: {
+          'Authorization': `Bearer ${userToken}`
+        }
+      });
+
+      console.debug('用户更新商户响应状态:', response.status);
+      expect(response.status).toBe(200);
+
+      if (response.status === 200) {
+        const data = await response.json();
+        expect(data.name).toBe(updateData.name);
+        expect(data.phone).toBe(updateData.phone);
+        expect(data.realname).toBe(updateData.realname);
+        expect(data.state).toBe(updateData.state);
+      }
+    });
+
+    it('应该拒绝更新其他用户的商户', async () => {
+      // 为其他用户创建一个商户
+      const dataSource = await IntegrationTestDatabase.getDataSource();
+      const merchantRepository = dataSource.getRepository(Merchant);
+      const otherUserMerchant = merchantRepository.create({
+        name: '其他用户商户',
+        username: `other_merchant_${Date.now()}`,
+        password: 'password123',
+        phone: '13800138001',
+        realname: '李四',
+        state: 1,
+        createdBy: otherUser.id
+      });
+      await merchantRepository.save(otherUserMerchant);
+
+      const updateData = {
+        name: '尝试更新的商户',
+        phone: '13900139001',
+        realname: '尝试更新的姓名'
+      };
+
+      // 当前用户尝试更新其他用户的商户
+      const response = await client[':id'].$put({
+        param: { id: otherUserMerchant.id },
+        json: updateData
+      }, {
+        headers: {
+          'Authorization': `Bearer ${userToken}`
+        }
+      });
+
+      console.debug('用户更新其他用户商户响应状态:', response.status);
+      expect(response.status).toBe(403); // 数据权限控制返回403
+    });
+  });
+
+  describe('DELETE /merchants/:id', () => {
+    it('应该成功删除当前用户的商户', async () => {
+      // 先为当前用户创建一个商户
+      const dataSource = await IntegrationTestDatabase.getDataSource();
+      const merchantRepository = dataSource.getRepository(Merchant);
+      const testMerchant = merchantRepository.create({
+        name: '待删除商户',
+        username: `delete_merchant_${Date.now()}`,
+        password: 'password123',
+        phone: '13800138000',
+        realname: '张三',
+        state: 1,
+        createdBy: testUser.id
+      });
+      await merchantRepository.save(testMerchant);
+
+      const response = await client[':id'].$delete({
+        param: { id: testMerchant.id }
+      }, {
+        headers: {
+          'Authorization': `Bearer ${userToken}`
+        }
+      });
+
+      console.debug('用户删除商户响应状态:', response.status);
+      expect(response.status).toBe(204);
+
+      // 验证商户确实被删除
+      const deletedMerchant = await merchantRepository.findOne({
+        where: { id: testMerchant.id }
+      });
+      expect(deletedMerchant).toBeNull();
+    });
+
+    it('应该拒绝删除其他用户的商户', async () => {
+      // 为其他用户创建一个商户
+      const dataSource = await IntegrationTestDatabase.getDataSource();
+      const merchantRepository = dataSource.getRepository(Merchant);
+      const otherUserMerchant = merchantRepository.create({
+        name: '其他用户商户',
+        username: `other_merchant_${Date.now()}`,
+        password: 'password123',
+        phone: '13800138001',
+        realname: '李四',
+        state: 1,
+        createdBy: otherUser.id
+      });
+      await merchantRepository.save(otherUserMerchant);
+
+      // 当前用户尝试删除其他用户的商户
+      const response = await client[':id'].$delete({
+        param: { id: otherUserMerchant.id }
+      }, {
+        headers: {
+          'Authorization': `Bearer ${userToken}`
+        }
+      });
+
+      console.debug('用户删除其他用户商户响应状态:', response.status);
+      expect(response.status).toBe(403); // 数据权限控制返回403
+    });
+  });
+
+  describe('数据权限验证', () => {
+    it('用户应该只能访问和操作自己的数据', async () => {
+      // 为两个用户都创建商户
+      const dataSource = await IntegrationTestDatabase.getDataSource();
+      const merchantRepository = dataSource.getRepository(Merchant);
+
+      const userMerchant = merchantRepository.create({
+        name: '用户商户',
+        username: `user_merchant_${Date.now()}`,
+        password: 'password123',
+        phone: '13800138004',
+        realname: '张三',
+        state: 1,
+        createdBy: testUser.id
+      });
+      await merchantRepository.save(userMerchant);
+
+      const otherUserMerchant = merchantRepository.create({
+        name: '其他用户商户',
+        username: `other_merchant_${Date.now()}`,
+        password: 'password123',
+        phone: '13800138005',
+        realname: '李四',
+        state: 1,
+        createdBy: otherUser.id
+      });
+      await merchantRepository.save(otherUserMerchant);
+
+      // 当前用户应该只能看到自己的商户
+      const listResponse = await client.index.$get({
+        query: {}
+      }, {
+        headers: {
+          'Authorization': `Bearer ${userToken}`
+        }
+      });
+
+      expect(listResponse.status).toBe(200);
+      const listData = await listResponse.json();
+      if (listData && 'data' in listData) {
+        expect(Array.isArray(listData.data)).toBe(true);
+        // 应该只包含当前用户的商户
+        listData.data.forEach((merchant: any) => {
+          expect(merchant.createdBy).toBe(testUser.id);
+        });
+      }
+
+      // 当前用户应该无法访问其他用户的商户详情
+      const getResponse = await client[':id'].$get({
+        param: { id: otherUserMerchant.id }
+      }, {
+        headers: {
+          'Authorization': `Bearer ${userToken}`
+        }
+      });
+      expect(getResponse.status).toBe(404);
+
+      // 当前用户应该无法更新其他用户的商户
+      const updateResponse = await client[':id'].$put({
+        param: { id: otherUserMerchant.id },
+        json: { name: '尝试更新' }
+      }, {
+        headers: {
+          'Authorization': `Bearer ${userToken}`
+        }
+      });
+      expect(updateResponse.status).toBe(403);
+
+      // 当前用户应该无法删除其他用户的商户
+      const deleteResponse = await client[':id'].$delete({
+        param: { id: otherUserMerchant.id }
+      }, {
+        headers: {
+          'Authorization': `Bearer ${userToken}`
+        }
+      });
+      expect(deleteResponse.status).toBe(403);
+    });
+  });
+
+  describe('商户状态管理测试', () => {
+    it('应该支持商户状态管理', async () => {
+      // 创建启用状态的商户
+      const createData = {
+        name: '状态测试商户',
+        username: `state_test_merchant_${Date.now()}`,
+        password: 'password123',
+        phone: '13800138006',
+        realname: '状态测试',
+        state: 1 // 启用
+      };
+
+      const createResponse = await client.index.$post({
+        json: createData
+      }, {
+        headers: {
+          'Authorization': `Bearer ${userToken}`
+        }
+      });
+
+      expect(createResponse.status).toBe(201);
+      const createdMerchant = await createResponse.json();
+      expect(createdMerchant.state).toBe(1);
+
+      // 更新为禁用状态
+      const updateResponse = await client[':id'].$put({
+        param: { id: createdMerchant.id },
+        json: { state: 2 } // 禁用
+      }, {
+        headers: {
+          'Authorization': `Bearer ${userToken}`
+        }
+      });
+
+      expect(updateResponse.status).toBe(200);
+      const updatedMerchant = await updateResponse.json();
+      expect(updatedMerchant.state).toBe(2);
+    });
+  });
+
+  describe('商户登录统计功能测试', () => {
+    it('应该支持商户登录统计字段', async () => {
+      // 创建商户
+      const createData = {
+        name: '登录统计商户',
+        username: `login_stat_merchant_${Date.now()}`,
+        password: 'password123',
+        phone: '13800138007',
+        realname: '登录统计',
+        state: 1
+      };
+
+      const createResponse = await client.index.$post({
+        json: createData
+      }, {
+        headers: {
+          'Authorization': `Bearer ${userToken}`
+        }
+      });
+
+      expect(createResponse.status).toBe(201);
+      const createdMerchant = await createResponse.json();
+
+      // 验证登录统计字段存在
+      expect(createdMerchant).toHaveProperty('loginNum');
+      expect(createdMerchant).toHaveProperty('loginTime');
+      expect(createdMerchant).toHaveProperty('loginIp');
+      expect(createdMerchant).toHaveProperty('lastLoginTime');
+      expect(createdMerchant).toHaveProperty('lastLoginIp');
+
+      // 初始值应该为0或null
+      expect(createdMerchant.loginNum).toBe(0);
+      expect(createdMerchant.loginTime).toBe(0);
+      expect(createdMerchant.lastLoginTime).toBe(0);
+    });
+  });
+});

+ 107 - 0
pnpm-lock.yaml

@@ -492,6 +492,113 @@ importers:
         specifier: ^3.2.4
         version: 3.2.4(@types/debug@4.1.12)(@types/node@24.9.1)(happy-dom@18.0.1)(jiti@2.6.1)(jsdom@24.1.3)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.64.0)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)
 
+  packages/goods-module:
+    dependencies:
+      '@d8d/auth-module':
+        specifier: workspace:*
+        version: link:../auth-module
+      '@d8d/file-module':
+        specifier: workspace:*
+        version: link:../file-module
+      '@d8d/shared-crud':
+        specifier: workspace:*
+        version: link:../shared-crud
+      '@d8d/shared-types':
+        specifier: workspace:*
+        version: link:../shared-types
+      '@d8d/shared-utils':
+        specifier: workspace:*
+        version: link:../shared-utils
+      '@d8d/user-module':
+        specifier: workspace:*
+        version: link:../user-module
+      '@hono/zod-openapi':
+        specifier: ^1.0.2
+        version: 1.0.2(hono@4.8.5)(zod@4.1.12)
+      hono:
+        specifier: ^4.8.5
+        version: 4.8.5
+      typeorm:
+        specifier: ^0.3.20
+        version: 0.3.27(ioredis@5.8.2)(pg@8.16.3)(redis@4.7.1)(reflect-metadata@0.2.2)
+      zod:
+        specifier: ^4.1.12
+        version: 4.1.12
+    devDependencies:
+      '@d8d/shared-test-util':
+        specifier: workspace:*
+        version: link:../shared-test-util
+      '@types/node':
+        specifier: ^22.10.2
+        version: 22.19.0
+      '@typescript-eslint/eslint-plugin':
+        specifier: ^8.18.1
+        version: 8.46.2(@typescript-eslint/parser@8.46.2(eslint@9.38.0(jiti@2.6.1))(typescript@5.8.3))(eslint@9.38.0(jiti@2.6.1))(typescript@5.8.3)
+      '@typescript-eslint/parser':
+        specifier: ^8.18.1
+        version: 8.46.2(eslint@9.38.0(jiti@2.6.1))(typescript@5.8.3)
+      eslint:
+        specifier: ^9.17.0
+        version: 9.38.0(jiti@2.6.1)
+      typescript:
+        specifier: ^5.8.3
+        version: 5.8.3
+      vitest:
+        specifier: ^3.2.4
+        version: 3.2.4(@types/debug@4.1.12)(@types/node@22.19.0)(happy-dom@18.0.1)(jiti@2.6.1)(jsdom@24.1.3)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.64.0)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)
+
+  packages/merchant-module:
+    dependencies:
+      '@d8d/auth-module':
+        specifier: workspace:*
+        version: link:../auth-module
+      '@d8d/shared-crud':
+        specifier: workspace:*
+        version: link:../shared-crud
+      '@d8d/shared-types':
+        specifier: workspace:*
+        version: link:../shared-types
+      '@d8d/shared-utils':
+        specifier: workspace:*
+        version: link:../shared-utils
+      '@d8d/user-module':
+        specifier: workspace:*
+        version: link:../user-module
+      '@hono/zod-openapi':
+        specifier: ^1.0.2
+        version: 1.0.2(hono@4.8.5)(zod@4.1.12)
+      hono:
+        specifier: ^4.8.5
+        version: 4.8.5
+      typeorm:
+        specifier: ^0.3.20
+        version: 0.3.27(ioredis@5.8.2)(pg@8.16.3)(redis@4.7.1)(reflect-metadata@0.2.2)
+      zod:
+        specifier: ^4.1.12
+        version: 4.1.12
+    devDependencies:
+      '@d8d/shared-test-util':
+        specifier: workspace:*
+        version: link:../shared-test-util
+      '@types/node':
+        specifier: ^22.10.2
+        version: 22.19.0
+      '@typescript-eslint/eslint-plugin':
+        specifier: ^8.18.1
+        version: 8.46.2(@typescript-eslint/parser@8.46.2(eslint@9.38.0(jiti@2.6.1))(typescript@5.8.3))(eslint@9.38.0(jiti@2.6.1))(typescript@5.8.3)
+      '@typescript-eslint/parser':
+        specifier: ^8.18.1
+        version: 8.46.2(eslint@9.38.0(jiti@2.6.1))(typescript@5.8.3)
+      eslint:
+        specifier: ^9.17.0
+        version: 9.38.0(jiti@2.6.1)
+      typescript:
+        specifier: ^5.8.3
+        version: 5.8.3
+      vitest:
+        specifier: ^3.2.4
+        version: 3.2.4(@types/debug@4.1.12)(@types/node@22.19.0)(happy-dom@18.0.1)(jiti@2.6.1)(jsdom@24.1.3)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.64.0)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)
+
   packages/mini-payment:
     dependencies:
       '@d8d/auth-module':