Procházet zdrojové kódy

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

- 复制供应商模块为多租户版本 @d8d/supplier-module-mt
- 添加租户ID字段和复合索引实现数据隔离
- 更新所有导入路径使用多租户实体和服务
- 修复测试文件中的实体引用和租户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 před 1 měsícem
rodič
revize
8596a67736

+ 89 - 65
docs/stories/007.008.supplier-module-multi-tenant-replication.md

@@ -2,7 +2,7 @@
 
 ## 状态
 
-Draft
+Completed
 
 ## 故事
 
@@ -24,68 +24,68 @@ Draft
 
 ## 任务 / 子任务
 
-- [ ] 复制供应商管理模块为多租户版本 (AC: 1)
-  - [ ] 复制 `packages/supplier-module` 为 `packages/supplier-module-mt`
-  - [ ] 更新包配置为 `@d8d/supplier-module-mt`
-  - [ ] **清理单租户文件**: 删除多租户包中所有单租户相关文件,避免命名冲突
-  - [ ] 更新依赖:
-    - [ ] 将 `@d8d/user-module` 替换为 `@d8d/user-module-mt`
-    - [ ] 将 `@d8d/auth-module` 替换为 `@d8d/auth-module-mt`
-
-- [ ] 更新多租户供应商实体 (AC: 2)
-  - [ ] 创建 `Supplier` 实体,表名为 `suppliers_mt`
-  - [ ] 为实体添加 `tenantId` 字段和正确的TypeORM配置
-  - [ ] 保持其他字段与单租户版本一致
-
-- [ ] 更新多租户供应商服务 (AC: 3, 4)
-  - [ ] 使用共享CRUD库的GenericCrudService
-  - [ ] 所有查询操作自动添加租户过滤
-  - [ ] 创建操作自动设置租户ID
-  - [ ] 更新关联查询支持租户隔离
-
-- [ ] 更新多租户路由配置 (AC: 3)
-  - [ ] 更新用户路由使用多租户实体和服务
-  - [ ] 更新管理员路由使用多租户实体和服务
-  - [ ] 保持API接口与单租户版本一致
-  - [ ] 启用租户选项:`tenantOptions: { enabled: true, tenantIdField: 'tenantId' }`
-
-- [ ] 更新Schema定义 (AC: 3)
-  - [ ] 使用多租户供应商Schema `SupplierSchema`
-  - [ ] 使用多租户用户专用Schema `UserSupplierSchema`
-  - [ ] 使用多租户管理员专用Schema `AdminSupplierSchema`
-  - [ ] 添加租户ID字段定义
-
-- [ ] 实现租户数据隔离API测试 (AC: 7)
-  - [ ] 在 `packages/supplier-module-mt/tests/integration/user-routes.integration.test.ts` 中添加租户隔离测试用例
-  - [ ] 在 `packages/supplier-module-mt/tests/integration/admin-routes.integration.test.ts` 中添加跨租户供应商访问安全验证
-  - [ ] 在现有功能测试中验证租户过滤功能正确性
-
-- [ ] 验证单租户系统完整性 (AC: 5, 6)
-  - [ ] 运行单租户供应商管理模块回归测试
-  - [ ] 验证单租户API接口不受影响
-  - [ ] 确认单租户数据库表结构不变
-
-- [ ] 在创建复制的代码修改完后先运行安装
-  - [ ] 在复制模块后运行 `pnpm install` 安装依赖
-  - [ ] 验证新包已正确添加到工作区
-  - [ ] 确认所有依赖解析正确
-
-- [ ] 执行性能基准测试 (AC: 8)
-  - [ ] 运行多租户供应商管理模块性能测试
-  - [ ] 比较单租户与多租户性能差异
-  - [ ] 确保性能影响小于5%
-
-- [ ] 执行回归测试验证 (AC: 9)
-  - [ ] 运行所有多租户模块的回归测试
-  - [ ] 验证权限模块多租户测试 (38个测试)
-  - [ ] 验证文件模块多租户测试 (40个测试)
-  - [ ] 验证区域模块多租户测试 (29个测试)
-  - [ ] 验证用户模块多租户测试 (41个测试)
-  - [ ] 验证配送地址模块多租户测试 (36个测试)
-  - [ ] 验证商户模块多租户测试 (37个测试)
-  - [ ] 验证租户模块多租户测试 (16个测试)
-  - [ ] 验证广告模块多租户测试 (22个测试)
-  - [ ] 确认所有239个测试全部通过
+- [x] 复制供应商管理模块为多租户版本 (AC: 1)
+  - [x] 复制 `packages/supplier-module` 为 `packages/supplier-module-mt`
+  - [x] 更新包配置为 `@d8d/supplier-module-mt`
+  - [x] **清理单租户文件**: 删除多租户包中所有单租户相关文件,避免命名冲突
+  - [x] 更新依赖:
+    - [x] 将 `@d8d/user-module` 替换为 `@d8d/user-module-mt`
+    - [x] 将 `@d8d/auth-module` 替换为 `@d8d/auth-module-mt`
+
+- [x] 更新多租户供应商实体 (AC: 2)
+  - [x] 创建 `Supplier` 实体,表名为 `suppliers_mt`
+  - [x] 为实体添加 `tenantId` 字段和正确的TypeORM配置
+  - [x] 保持其他字段与单租户版本一致
+
+- [x] 更新多租户供应商服务 (AC: 3, 4)
+  - [x] 使用共享CRUD库的GenericCrudService
+  - [x] 所有查询操作自动添加租户过滤
+  - [x] 创建操作自动设置租户ID
+  - [x] 更新关联查询支持租户隔离
+
+- [x] 更新多租户路由配置 (AC: 3)
+  - [x] 更新用户路由使用多租户实体和服务
+  - [x] 更新管理员路由使用多租户实体和服务
+  - [x] 保持API接口与单租户版本一致
+  - [x] 启用租户选项:`tenantOptions: { enabled: true, tenantIdField: 'tenantId' }`
+
+- [x] 更新Schema定义 (AC: 3)
+  - [x] 使用多租户供应商Schema `SupplierSchema`
+  - [x] 使用多租户用户专用Schema `UserSupplierSchema`
+  - [x] 使用多租户管理员专用Schema `AdminSupplierSchema`
+  - [x] 添加租户ID字段定义
+
+- [x] 实现租户数据隔离API测试 (AC: 7)
+  - [x] 在 `packages/supplier-module-mt/tests/integration/user-routes.integration.test.ts` 中添加租户隔离测试用例
+  - [x] 在 `packages/supplier-module-mt/tests/integration/admin-routes.integration.test.ts` 中添加跨租户供应商访问安全验证
+  - [x] 在现有功能测试中验证租户过滤功能正确性
+
+- [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] 验证租户模块多租户测试 (16个测试)
+  - [x] 验证广告模块多租户测试 (22个测试)
+  - [x] 确认所有239个测试全部通过
 
 ## 开发说明
 
@@ -190,15 +190,39 @@ Draft
 
 ## 开发代理记录
 
-*此部分由开发代理在实现过程中填写*
-
 ### Agent Model Used
+- Claude Code (d8d-model)
 
 ### Debug Log References
+- 租户ID缺失错误: null value in column "tenant_id" violates not-null constraint
+- 实体注册错误: No metadata for "UserEntityMt" was found
+- 类型检查错误: 测试文件中仍然引用单租户实体
+- 目录结构错误: 多租户模块中错误包含单租户模块目录
 
 ### Completion Notes List
+1. 成功复制供应商模块为多租户版本,包含完整的文件结构
+2. 添加了租户ID字段和复合索引实现数据隔离
+3. 更新所有导入路径使用多租户实体和服务
+4. 修复了测试文件中的实体引用和租户ID设置
+5. 验证了租户数据隔离功能正常工作
+6. 清理了目录结构,确保多租户和单租户模块在同一层级
+7. 所有测试通过,包括单租户和多租户模块的回归测试
 
 ### File List
+**创建/修改的文件:**
+- `packages/supplier-module-mt/package.json` - 包配置和依赖更新
+- `packages/supplier-module-mt/src/entities/supplier.mt.entity.ts` - 多租户供应商实体
+- `packages/supplier-module-mt/src/services/supplier.mt.service.ts` - 多租户供应商服务
+- `packages/supplier-module-mt/src/routes/user-routes.mt.ts` - 多租户用户路由
+- `packages/supplier-module-mt/src/routes/admin-routes.mt.ts` - 多租户管理员路由
+- `packages/supplier-module-mt/src/schemas/supplier.mt.schema.ts` - 多租户供应商Schema
+- `packages/supplier-module-mt/src/schemas/user-supplier.mt.schema.ts` - 多租户用户专用Schema
+- `packages/supplier-module-mt/src/schemas/admin-supplier.mt.schema.ts` - 多租户管理员专用Schema
+- `packages/supplier-module-mt/tests/integration/user-routes.integration.test.ts` - 用户路由集成测试
+- `packages/supplier-module-mt/tests/integration/admin-routes.integration.test.ts` - 管理员路由集成测试
+
+**删除的文件:**
+- `packages/supplier-module-mt/supplier-module/` - 错误包含的单租户模块目录
 
 ## QA结果
 

+ 82 - 0
packages/supplier-module-mt/package.json

@@ -0,0 +1,82 @@
+{
+  "name": "@d8d/supplier-module-mt",
+  "version": "1.0.0",
+  "description": "供应商管理模块多租户版本 - 提供供应商的完整CRUD功能,支持租户数据隔离",
+  "keywords": [
+    "supplier",
+    "vendor",
+    "crud",
+    "api",
+    "management",
+    "multi-tenant",
+    "tenant"
+  ],
+  "type": "module",
+  "main": "src/index.ts",
+  "types": "src/index.ts",
+  "exports": {
+    ".": {
+      "types": "./src/index.ts",
+      "import": "./src/index.ts",
+      "require": "./src/index.ts"
+    },
+    "./services": {
+      "types": "./src/services/index.ts",
+      "import": "./src/services/index.ts",
+      "require": "./src/services/index.ts"
+    },
+    "./schemas": {
+      "types": "./src/schemas/index.ts",
+      "import": "./src/schemas/index.ts",
+      "require": "./src/schemas/index.ts"
+    },
+    "./routes": {
+      "types": "./src/routes/index.ts",
+      "import": "./src/routes/index.ts",
+      "require": "./src/routes/index.ts"
+    },
+    "./entities": {
+      "types": "./src/entities/index.ts",
+      "import": "./src/entities/index.ts",
+      "require": "./src/entities/index.ts"
+    }
+  },
+  "files": [
+    "src"
+  ],
+  "scripts": {
+    "build": "tsc",
+    "dev": "tsc --watch",
+    "test": "vitest run",
+    "test:watch": "vitest",
+    "test:coverage": "vitest run --coverage",
+    "test:integration": "vitest run tests/integration",
+    "lint": "eslint src --ext .ts,.tsx",
+    "typecheck": "tsc --noEmit"
+  },
+  "dependencies": {
+    "@d8d/shared-types": "workspace:*",
+    "@d8d/shared-utils": "workspace:*",
+    "@d8d/shared-crud": "workspace:*",
+    "@d8d/auth-module-mt": "workspace:*",
+    "@d8d/user-module-mt": "workspace:*",
+    "@d8d/file-module-mt": "workspace:*",
+    "@hono/zod-openapi": "^1.0.2",
+    "typeorm": "^0.3.20",
+    "zod": "^4.1.12"
+  },
+  "devDependencies": {
+    "@types/node": "^22.10.2",
+    "typescript": "^5.8.3",
+    "vitest": "^3.2.4",
+    "@d8d/shared-test-util": "workspace:*",
+    "@typescript-eslint/eslint-plugin": "^8.18.1",
+    "@typescript-eslint/parser": "^8.18.1",
+    "eslint": "^9.17.0"
+  },
+  "peerDependencies": {
+    "hono": "^4.8.5"
+  },
+  "author": "D8D Team",
+  "license": "MIT"
+}

+ 1 - 0
packages/supplier-module-mt/src/entities/index.ts

@@ -0,0 +1 @@
+export { SupplierMt } from './supplier.mt.entity';

+ 58 - 0
packages/supplier-module-mt/src/entities/supplier.mt.entity.ts

@@ -0,0 +1,58 @@
+import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, Index } from 'typeorm';
+
+@Entity('suppliers_mt')
+@Index(['tenantId', 'state'])
+@Index(['tenantId', 'username'])
+export class SupplierMt {
+  @PrimaryGeneratedColumn({ unsigned: true })
+  id!: number;
+
+  @Index()
+  @Column({ name: 'tenant_id', type: 'int', unsigned: true, comment: '租户ID' })
+  tenantId!: number;
+
+  @Column({ name: 'name', type: 'varchar', length: 255, nullable: true, comment: '供货商名称' })
+  name!: string | null;
+
+  @Column({ name: 'username', type: 'varchar', length: 50, unique: true, comment: '用户名' })
+  username!: string;
+
+  @Column({ name: 'password', type: 'varchar', length: 255, comment: '密码' })
+  password!: string;
+
+  @Column({ name: 'phone', type: 'char', length: 11, nullable: true, comment: '手机号码' })
+  phone!: string | null;
+
+  @Column({ name: 'realname', type: 'varchar', length: 20, nullable: true, comment: '姓名' })
+  realname!: string | null;
+
+  @Column({ name: 'login_num', type: 'int', unsigned: true, default: 0, comment: '登录次数' })
+  loginNum!: number;
+
+  @Column({ name: 'login_time', type: 'timestamp', nullable: true, comment: '登录时间' })
+  loginTime!: Date | null;
+
+  @Column({ name: 'login_ip', type: 'varchar', length: 15, nullable: true, comment: '登录IP' })
+  loginIp!: string | null;
+
+  @Column({ name: 'last_login_time', type: 'timestamp', nullable: true, comment: '上次登录时间' })
+  lastLoginTime!: Date | null;
+
+  @Column({ name: 'last_login_ip', type: 'varchar', length: 15, nullable: true, comment: '上次登录IP' })
+  lastLoginIp!: string | null;
+
+  @Column({ name: 'state', type: 'smallint', unsigned: true, default: 2, comment: '状态 1启用 2禁用' })
+  state!: number;
+
+  @CreateDateColumn({ name: 'created_at', type: 'timestamp', comment: '创建时间' })
+  createdAt!: Date;
+
+  @UpdateDateColumn({ name: 'updated_at', type: 'timestamp', comment: '更新时间' })
+  updatedAt!: Date;
+
+  @Column({ name: 'created_by', type: 'int', unsigned: true, nullable: true, comment: '创建用户ID' })
+  createdBy!: number | null;
+
+  @Column({ name: 'updated_by', type: 'int', unsigned: true, nullable: true, comment: '更新用户ID' })
+  updatedBy!: number | null;
+}

+ 4 - 0
packages/supplier-module-mt/src/index.ts

@@ -0,0 +1,4 @@
+export * from './entities';
+export * from './services';
+export * from './schemas';
+export * from './routes';

+ 23 - 0
packages/supplier-module-mt/src/routes/admin-routes.mt.ts

@@ -0,0 +1,23 @@
+import { createCrudRoutes } from '@d8d/shared-crud';
+import { authMiddleware } from '@d8d/auth-module-mt';
+import { SupplierMt } from '../entities/supplier.mt.entity';
+import { AdminSupplierSchema, CreateAdminSupplierDto, UpdateAdminSupplierDto } from '../schemas/admin-supplier.mt.schema';
+
+export const adminSupplierRoutes = createCrudRoutes({
+  entity: SupplierMt,
+  createSchema: CreateAdminSupplierDto,
+  updateSchema: UpdateAdminSupplierDto,
+  getSchema: AdminSupplierSchema,
+  listSchema: AdminSupplierSchema,
+  searchFields: ['name', 'username', 'realname'],
+  middleware: [authMiddleware],
+  userTracking: {
+    createdByField: 'createdBy',
+    updatedByField: 'updatedBy'
+  },
+  tenantOptions: {
+    enabled: true,
+    tenantIdField: 'tenantId'
+  }
+  // 管理员路由不使用数据权限控制,保持完整CRUD功能
+});

+ 2 - 0
packages/supplier-module-mt/src/routes/index.ts

@@ -0,0 +1,2 @@
+export { userSupplierRoutes } from './user-routes.mt';
+export { adminSupplierRoutes } from './admin-routes.mt';

+ 26 - 0
packages/supplier-module-mt/src/routes/user-routes.mt.ts

@@ -0,0 +1,26 @@
+import { createCrudRoutes } from '@d8d/shared-crud';
+import { authMiddleware } from '@d8d/auth-module-mt';
+import { SupplierMt } from '../entities/supplier.mt.entity';
+import { UserSupplierSchema, CreateUserSupplierDto, UpdateUserSupplierDto } from '../schemas/user-supplier.mt.schema';
+
+export const userSupplierRoutes = createCrudRoutes({
+  entity: SupplierMt,
+  createSchema: CreateUserSupplierDto,
+  updateSchema: UpdateUserSupplierDto,
+  getSchema: UserSupplierSchema,
+  listSchema: UserSupplierSchema,
+  searchFields: ['name', 'username', 'realname'],
+  middleware: [authMiddleware],
+  userTracking: {
+    createdByField: 'createdBy',
+    updatedByField: 'updatedBy'
+  },
+  dataPermission: {
+    enabled: true,
+    userIdField: 'createdBy'
+  },
+  tenantOptions: {
+    enabled: true,
+    tenantIdField: 'tenantId'
+  }
+});

+ 124 - 0
packages/supplier-module-mt/src/schemas/admin-supplier.mt.schema.ts

@@ -0,0 +1,124 @@
+import { z } from '@hono/zod-openapi';
+
+export const AdminSupplierSchema = 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个字符').nullable().openapi({
+    description: '供货商名称',
+    example: '供应商A'
+  }),
+  username: z.string().min(1, '用户名不能为空').max(50, '用户名最多50个字符').openapi({
+    description: '用户名',
+    example: 'supplier001'
+  }),
+  phone: z.string().regex(/^1[3-9]\d{9}$/, '请输入正确的手机号').nullable().optional().openapi({
+    description: '手机号码',
+    example: '13800138000'
+  }),
+  realname: z.string().max(20, '姓名最多20个字符').nullable().optional().openapi({
+    description: '姓名',
+    example: '张三'
+  }),
+  loginNum: z.number().int().nonnegative('登录次数必须为非负数').default(0).openapi({
+    description: '登录次数',
+    example: 0
+  }),
+  loginTime: z.coerce.date().nullable().optional().openapi({
+    description: '登录时间',
+    example: '2024-01-01T12:00:00Z'
+  }),
+  loginIp: z.string().max(15, 'IP地址最多15个字符').nullable().optional().openapi({
+    description: '登录IP',
+    example: '192.168.1.1'
+  }),
+  lastLoginTime: z.coerce.date().nullable().optional().openapi({
+    description: '上次登录时间',
+    example: '2024-01-01T12:00:00Z'
+  }),
+  lastLoginIp: z.string().max(15, 'IP地址最多15个字符').nullable().optional().openapi({
+    description: '上次登录IP',
+    example: '192.168.1.1'
+  }),
+  state: z.number().int().min(1).max(2).default(2).openapi({
+    description: '状态 1启用 2禁用',
+    example: 1
+  }),
+  createdAt: z.coerce.date().openapi({
+    description: '创建时间',
+    example: '2024-01-01T12:00:00Z'
+  }),
+  updatedAt: z.coerce.date().openapi({
+    description: '更新时间',
+    example: '2024-01-01T12:00:00Z'
+  }),
+  createdBy: z.number().int().positive().nullable().openapi({
+    description: '创建用户ID',
+    example: 1
+  }),
+  updatedBy: z.number().int().positive().nullable().openapi({
+    description: '更新用户ID',
+    example: 1
+  })
+});
+
+export const CreateAdminSupplierDto = z.object({
+  name: z.string().min(1, '供货商名称不能为空').max(255, '供货商名称最多255个字符').nullable().optional().openapi({
+    description: '供货商名称',
+    example: '供应商A'
+  }),
+  username: z.string().min(1, '用户名不能为空').max(50, '用户名最多50个字符').openapi({
+    description: '用户名',
+    example: 'supplier001'
+  }),
+  password: z.string().min(6, '密码至少6位').max(255, '密码最多255位').openapi({
+    description: '密码',
+    example: 'password123'
+  }),
+  phone: z.string().regex(/^1[3-9]\d{9}$/, '请输入正确的手机号').nullable().optional().openapi({
+    description: '手机号码',
+    example: '13800138000'
+  }),
+  realname: z.string().max(20, '姓名最多20个字符').nullable().optional().openapi({
+    description: '姓名',
+    example: '张三'
+  }),
+  state: z.number().int().min(1).max(2).default(2).openapi({
+    description: '状态 1启用 2禁用',
+    example: 1
+  }),
+  createdBy: z.number().int().positive().optional().openapi({
+    description: '创建用户ID',
+    example: 1
+  })
+});
+
+export const UpdateAdminSupplierDto = z.object({
+  name: z.string().min(1, '供货商名称不能为空').max(255, '供货商名称最多255个字符').nullable().optional().openapi({
+    description: '供货商名称',
+    example: '供应商A'
+  }),
+  username: z.string().min(1, '用户名不能为空').max(50, '用户名最多50个字符').optional().openapi({
+    description: '用户名',
+    example: 'supplier001'
+  }),
+  password: z.string().min(6, '密码至少6位').max(255, '密码最多255位').optional().openapi({
+    description: '密码',
+    example: 'password123'
+  }),
+  phone: z.string().regex(/^1[3-9]\d{9}$/, '请输入正确的手机号').nullable().optional().openapi({
+    description: '手机号码',
+    example: '13800138000'
+  }),
+  realname: z.string().max(20, '姓名最多20个字符').nullable().optional().openapi({
+    description: '姓名',
+    example: '张三'
+  }),
+  state: z.number().int().min(1).max(2).optional().openapi({
+    description: '状态 1启用 2禁用',
+    example: 1
+  }),
+  updatedBy: z.number().int().positive().optional().openapi({
+    description: '更新用户ID',
+    example: 1
+  })
+});

+ 3 - 0
packages/supplier-module-mt/src/schemas/index.ts

@@ -0,0 +1,3 @@
+export { SupplierSchema, CreateSupplierDto, UpdateSupplierDto } from './supplier.mt.schema';
+export { UserSupplierSchema, CreateUserSupplierDto, UpdateUserSupplierDto } from './user-supplier.mt.schema';
+export { AdminSupplierSchema, CreateAdminSupplierDto, UpdateAdminSupplierDto } from './admin-supplier.mt.schema';

+ 120 - 0
packages/supplier-module-mt/src/schemas/supplier.mt.schema.ts

@@ -0,0 +1,120 @@
+import { z } from '@hono/zod-openapi';
+
+export const SupplierSchema = 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个字符').nullable().openapi({
+    description: '供货商名称',
+    example: '供应商A'
+  }),
+  username: z.string().min(1, '用户名不能为空').max(20, '用户名最多20个字符').openapi({
+    description: '用户名',
+    example: 'supplier001'
+  }),
+  password: z.string().min(6, '密码至少6位').max(255, '密码最多255位').openapi({
+    description: '密码',
+    example: 'password123'
+  }),
+  phone: z.string().regex(/^1[3-9]\d{9}$/, '请输入正确的手机号').nullable().optional().openapi({
+    description: '手机号码',
+    example: '13800138000'
+  }),
+  realname: z.string().max(20, '姓名最多20个字符').nullable().optional().openapi({
+    description: '姓名',
+    example: '张三'
+  }),
+  loginNum: z.number().int().nonnegative('登录次数必须为非负数').default(0).openapi({
+    description: '登录次数',
+    example: 0
+  }),
+  loginTime: z.coerce.date().nullable().openapi({
+    description: '登录时间',
+    example: '2024-01-01T12:00:00Z'
+  }),
+  loginIp: z.string().max(15, 'IP地址最多15个字符').nullable().optional().openapi({
+    description: '登录IP',
+    example: '192.168.1.1'
+  }),
+  lastLoginTime: z.coerce.date().nullable().openapi({
+    description: '上次登录时间',
+    example: '2024-01-01T12:00:00Z'
+  }),
+  lastLoginIp: z.string().max(15, 'IP地址最多15个字符').nullable().optional().openapi({
+    description: '上次登录IP',
+    example: '192.168.1.1'
+  }),
+  state: z.number().int().min(1).max(2).default(2).openapi({
+    description: '状态 1启用 2禁用',
+    example: 1
+  }),
+  createdAt: z.coerce.date().openapi({
+    description: '创建时间',
+    example: '2024-01-01T12:00:00Z'
+  }),
+  updatedAt: z.coerce.date().openapi({
+    description: '更新时间',
+    example: '2024-01-01T12:00:00Z'
+  }),
+  createdBy: z.number().int().positive().nullable().openapi({
+    description: '创建用户ID',
+    example: 1
+  }),
+  updatedBy: z.number().int().positive().nullable().openapi({
+    description: '更新用户ID',
+    example: 1
+  })
+});
+
+export const CreateSupplierDto = z.object({
+  name: z.string().min(1, '供货商名称不能为空').max(255, '供货商名称最多255个字符').nullable().optional().openapi({
+    description: '供货商名称',
+    example: '供应商A'
+  }),
+  username: z.string().min(1, '用户名不能为空').max(20, '用户名最多20个字符').openapi({
+    description: '用户名',
+    example: 'supplier001'
+  }),
+  password: z.string().min(6, '密码至少6位').max(255, '密码最多255位').openapi({
+    description: '密码',
+    example: 'password123'
+  }),
+  phone: z.string().regex(/^1[3-9]\d{9}$/, '请输入正确的手机号').nullable().optional().openapi({
+    description: '手机号码',
+    example: '13800138000'
+  }),
+  realname: z.string().max(20, '姓名最多20个字符').nullable().optional().openapi({
+    description: '姓名',
+    example: '张三'
+  }),
+  state: z.number().int().min(1).max(2).default(2).openapi({
+    description: '状态 1启用 2禁用',
+    example: 1
+  })
+});
+
+export const UpdateSupplierDto = z.object({
+  name: z.string().min(1, '供货商名称不能为空').max(255, '供货商名称最多255个字符').nullable().optional().openapi({
+    description: '供货商名称',
+    example: '供应商A'
+  }),
+  username: z.string().min(1, '用户名不能为空').max(20, '用户名最多20个字符').optional().openapi({
+    description: '用户名',
+    example: 'supplier001'
+  }),
+  password: z.string().min(6, '密码至少6位').max(255, '密码最多255位').optional().openapi({
+    description: '密码',
+    example: 'password123'
+  }),
+  phone: z.string().regex(/^1[3-9]\d{9}$/, '请输入正确的手机号').nullable().optional().openapi({
+    description: '手机号码',
+    example: '13800138000'
+  }),
+  realname: z.string().max(20, '姓名最多20个字符').nullable().optional().openapi({
+    description: '姓名',
+    example: '张三'
+  }),
+  state: z.number().int().min(1).max(2).optional().openapi({
+    description: '状态 1启用 2禁用',
+    example: 1
+  })
+});

+ 112 - 0
packages/supplier-module-mt/src/schemas/user-supplier.mt.schema.ts

@@ -0,0 +1,112 @@
+import { z } from '@hono/zod-openapi';
+
+export const UserSupplierSchema = 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个字符').nullable().openapi({
+    description: '供货商名称',
+    example: '供应商A'
+  }),
+  username: z.string().min(1, '用户名不能为空').max(50, '用户名最多50个字符').openapi({
+    description: '用户名',
+    example: 'supplier001'
+  }),
+  phone: z.string().regex(/^1[3-9]\d{9}$/, '请输入正确的手机号').nullable().optional().openapi({
+    description: '手机号码',
+    example: '13800138000'
+  }),
+  realname: z.string().max(20, '姓名最多20个字符').nullable().optional().openapi({
+    description: '姓名',
+    example: '张三'
+  }),
+  loginNum: z.number().int().nonnegative('登录次数必须为非负数').default(0).openapi({
+    description: '登录次数',
+    example: 0
+  }),
+  loginTime: z.coerce.date().nullable().optional().openapi({
+    description: '登录时间',
+    example: '2024-01-01T12:00:00Z'
+  }),
+  loginIp: z.string().max(15, 'IP地址最多15个字符').nullable().optional().openapi({
+    description: '登录IP',
+    example: '192.168.1.1'
+  }),
+  lastLoginTime: z.coerce.date().nullable().optional().openapi({
+    description: '上次登录时间',
+    example: '2024-01-01T12:00:00Z'
+  }),
+  lastLoginIp: z.string().max(15, 'IP地址最多15个字符').nullable().optional().openapi({
+    description: '上次登录IP',
+    example: '192.168.1.1'
+  }),
+  state: z.number().int().min(1).max(2).default(2).openapi({
+    description: '状态 1启用 2禁用',
+    example: 1
+  }),
+  createdBy: z.number().int().positive().nullable().openapi({
+    description: '创建用户ID',
+    example: 1
+  }),
+  createdAt: z.coerce.date().openapi({
+    description: '创建时间',
+    example: '2024-01-01T12:00:00Z'
+  }),
+  updatedAt: z.coerce.date().openapi({
+    description: '更新时间',
+    example: '2024-01-01T12:00:00Z'
+  })
+});
+
+export const CreateUserSupplierDto = z.object({
+  name: z.string().min(1, '供货商名称不能为空').max(255, '供货商名称最多255个字符').nullable().optional().openapi({
+    description: '供货商名称',
+    example: '供应商A'
+  }),
+  username: z.string().min(1, '用户名不能为空').max(50, '用户名最多50个字符').openapi({
+    description: '用户名',
+    example: 'supplier001'
+  }),
+  password: z.string().min(6, '密码至少6位').max(255, '密码最多255位').openapi({
+    description: '密码',
+    example: 'password123'
+  }),
+  phone: z.string().regex(/^1[3-9]\d{9}$/, '请输入正确的手机号').nullable().optional().openapi({
+    description: '手机号码',
+    example: '13800138000'
+  }),
+  realname: z.string().max(20, '姓名最多20个字符').nullable().optional().openapi({
+    description: '姓名',
+    example: '张三'
+  }),
+  state: z.number().int().min(1).max(2).default(2).openapi({
+    description: '状态 1启用 2禁用',
+    example: 1
+  })
+});
+
+export const UpdateUserSupplierDto = z.object({
+  name: z.string().min(1, '供货商名称不能为空').max(255, '供货商名称最多255个字符').nullable().optional().openapi({
+    description: '供货商名称',
+    example: '供应商A'
+  }),
+  username: z.string().min(1, '用户名不能为空').max(50, '用户名最多50个字符').optional().openapi({
+    description: '用户名',
+    example: 'supplier001'
+  }),
+  password: z.string().min(6, '密码至少6位').max(255, '密码最多255位').optional().openapi({
+    description: '密码',
+    example: 'password123'
+  }),
+  phone: z.string().regex(/^1[3-9]\d{9}$/, '请输入正确的手机号').nullable().optional().openapi({
+    description: '手机号码',
+    example: '13800138000'
+  }),
+  realname: z.string().max(20, '姓名最多20个字符').nullable().optional().openapi({
+    description: '姓名',
+    example: '张三'
+  }),
+  state: z.number().int().min(1).max(2).optional().openapi({
+    description: '状态 1启用 2禁用',
+    example: 1
+  })
+});

+ 1 - 0
packages/supplier-module-mt/src/services/index.ts

@@ -0,0 +1 @@
+export { SupplierService } from './supplier.mt.service';

+ 9 - 0
packages/supplier-module-mt/src/services/supplier.mt.service.ts

@@ -0,0 +1,9 @@
+import { GenericCrudService } from '@d8d/shared-crud';
+import { DataSource } from 'typeorm';
+import { SupplierMt } from '../entities/supplier.mt.entity';
+
+export class SupplierService extends GenericCrudService<SupplierMt> {
+  constructor(dataSource: DataSource) {
+    super(dataSource, SupplierMt);
+  }
+}

+ 31 - 0
packages/supplier-module-mt/src/types/supplier.mt.types.ts

@@ -0,0 +1,31 @@
+import { z } from '@hono/zod-openapi';
+import { SupplierSchema, CreateSupplierDto, UpdateSupplierDto } from '../schemas/supplier.mt.schema';
+import { UserSupplierSchema, CreateUserSupplierDto, UpdateUserSupplierDto } from '../schemas/user-supplier.mt.schema';
+import { AdminSupplierSchema, CreateAdminSupplierDto, UpdateAdminSupplierDto } from '../schemas/admin-supplier.mt.schema';
+
+export type Supplier = z.infer<typeof SupplierSchema>;
+export type CreateSupplier = z.infer<typeof CreateSupplierDto>;
+export type UpdateSupplier = z.infer<typeof UpdateSupplierDto>;
+
+export type UserSupplier = z.infer<typeof UserSupplierSchema>;
+export type CreateUserSupplier = z.infer<typeof CreateUserSupplierDto>;
+export type UpdateUserSupplier = z.infer<typeof UpdateUserSupplierDto>;
+
+export type AdminSupplier = z.infer<typeof AdminSupplierSchema>;
+export type CreateAdminSupplier = z.infer<typeof CreateAdminSupplierDto>;
+export type UpdateAdminSupplier = z.infer<typeof UpdateAdminSupplierDto>;
+
+export interface SupplierQueryOptions {
+  page?: number;
+  limit?: number;
+  search?: string;
+  state?: number;
+  createdBy?: number;
+}
+
+export interface SupplierStats {
+  total: number;
+  active: number;
+  inactive: number;
+  todayLogins: number;
+}

+ 805 - 0
packages/supplier-module-mt/tests/integration/admin-routes.integration.test.ts

@@ -0,0 +1,805 @@
+import { describe, it, expect, beforeEach } from 'vitest';
+import { testClient } from 'hono/testing';
+import { IntegrationTestDatabase, setupIntegrationDatabaseHooksWithEntities } from '@d8d/shared-test-util';
+import { JWTUtil } from '@d8d/shared-utils';
+import { UserEntityMt, RoleMt } from '@d8d/user-module-mt';
+import { FileMt } from '@d8d/file-module-mt';
+import { adminSupplierRoutes } from '../../src/routes';
+import { SupplierMt } from '../../src/entities';
+
+// 设置集成测试钩子
+setupIntegrationDatabaseHooksWithEntities([UserEntityMt, RoleMt, SupplierMt, FileMt])
+
+describe('管理员供应商管理API集成测试', () => {
+  let client: ReturnType<typeof testClient<typeof adminSupplierRoutes>>;
+  let adminToken: string;
+  let testUser: UserEntityMt;
+  let testAdmin: UserEntityMt;
+
+  beforeEach(async () => {
+    // 创建测试客户端
+    client = testClient(adminSupplierRoutes);
+
+    // 获取数据源
+    const dataSource = await IntegrationTestDatabase.getDataSource();
+
+    // 创建测试用户
+    const userRepository = dataSource.getRepository(UserEntityMt);
+    testUser = userRepository.create({
+      username: `test_user_${Math.floor(Math.random() * 100000)}`,
+      password: 'test_password',
+      nickname: '测试用户',
+      registrationSource: 'web',
+      tenantId: 1
+    });
+    await userRepository.save(testUser);
+
+    // 创建测试管理员用户
+    testAdmin = userRepository.create({
+      username: `test_admin_${Math.floor(Math.random() * 100000)}`,
+      password: 'admin_password',
+      nickname: '测试管理员',
+      registrationSource: 'web',
+      tenantId: 1
+    });
+    await userRepository.save(testAdmin);
+
+    // 生成测试管理员的token
+    adminToken = JWTUtil.generateToken({
+      id: testAdmin.id,
+      username: testAdmin.username,
+      roles: [{name:'admin'}],
+      tenantId: testAdmin.tenantId
+    });
+  });
+
+  describe('GET /suppliers', () => {
+    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 /suppliers', () => {
+    it('应该成功创建供应商', async () => {
+      const createData = {
+        name: '管理员创建供应商',
+        username: `admin_created_supplier_${Math.floor(Math.random() * 100000)}`,
+        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: '',
+        phone: '',
+        realname: ''
+      };
+
+      const response = await client.index.$post({
+        json: invalidData
+      }, {
+        headers: {
+          'Authorization': `Bearer ${adminToken}`
+        }
+      });
+
+      expect(response.status).toBe(400);
+    });
+  });
+
+  describe('GET /suppliers/:id', () => {
+    it('应该返回指定供应商的详情', async () => {
+      // 先创建一个供应商
+      const dataSource = await IntegrationTestDatabase.getDataSource();
+      const supplierRepository = dataSource.getRepository(SupplierMt);
+      const testSupplier = supplierRepository.create({
+        tenantId: 1,
+        name: '测试供应商详情',
+        username: `test_supplier_detail_${Math.floor(Math.random() * 100000)}`,
+        password: 'password123',
+        phone: '13600136000',
+        realname: '测试供应商详情',
+        loginNum: 5,
+        loginTime: new Date('2024-01-01T12:00:00Z'),
+        loginIp: '192.168.1.1',
+        lastLoginTime: new Date('2024-01-01T12:00:00Z'),
+        lastLoginIp: '192.168.1.1',
+        state: 1,
+        createdBy: testUser.id
+      });
+      await supplierRepository.save(testSupplier);
+
+      const response = await client[':id'].$get({
+        param: { id: testSupplier.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(testSupplier.id);
+        expect(data.name).toBe(testSupplier.name);
+        expect(data.username).toBe(testSupplier.username);
+        expect(data.phone).toBe(testSupplier.phone);
+        expect(data.realname).toBe(testSupplier.realname);
+      }
+    });
+
+    it('应该处理不存在的供应商', async () => {
+      const response = await client[':id'].$get({
+        param: { id: 999999 }
+      }, {
+        headers: {
+          'Authorization': `Bearer ${adminToken}`
+        }
+      });
+
+      expect(response.status).toBe(404);
+    });
+  });
+
+  describe('PUT /suppliers/:id', () => {
+    it('应该成功更新供应商', async () => {
+      // 先创建一个供应商
+      const dataSource = await IntegrationTestDatabase.getDataSource();
+      const supplierRepository = dataSource.getRepository(SupplierMt);
+      const testSupplier = supplierRepository.create({
+        tenantId: 1,
+        name: '原始供应商',
+        username: `original_supplier_${Math.floor(Math.random() * 100000)}`,
+        password: 'password123',
+        phone: '13500135000',
+        realname: '原始供应商',
+        loginNum: 0,
+        loginTime: null,
+        loginIp: null,
+        lastLoginTime: null,
+        lastLoginIp: null,
+        state: 1,
+        createdBy: testUser.id
+      });
+      await supplierRepository.save(testSupplier);
+
+      const updateData = {
+        name: '更新后的供应商',
+        phone: '13700137000',
+        realname: '更新后的供应商',
+        state: 2
+      };
+
+      const response = await client[':id'].$put({
+        param: { id: testSupplier.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 /suppliers/:id', () => {
+    it('应该成功删除供应商', async () => {
+      // 先创建一个供应商
+      const dataSource = await IntegrationTestDatabase.getDataSource();
+      const supplierRepository = dataSource.getRepository(SupplierMt);
+      const testSupplier = supplierRepository.create({
+        tenantId: 1,
+        name: '待删除供应商',
+        username: `delete_supplier_${Math.floor(Math.random() * 100000)}`,
+        password: 'password123',
+        phone: '13400134000',
+        realname: '待删除供应商',
+        loginNum: 0,
+        loginTime: null,
+        loginIp: null,
+        lastLoginTime: null,
+        lastLoginIp: null,
+        state: 1,
+        createdBy: testUser.id
+      });
+      await supplierRepository.save(testSupplier);
+
+      const response = await client[':id'].$delete({
+        param: { id: testSupplier.id }
+      }, {
+        headers: {
+          'Authorization': `Bearer ${adminToken}`
+        }
+      });
+
+      console.debug('删除供应商响应状态:', response.status);
+      expect(response.status).toBe(204);
+
+      // 验证供应商确实被删除
+      const deletedSupplier = await supplierRepository.findOne({
+        where: { id: testSupplier.id }
+      });
+      expect(deletedSupplier).toBeNull();
+    });
+  });
+
+  describe('管理员权限测试', () => {
+    it('管理员应该可以为其他用户创建供应商', async () => {
+      const createData = {
+        name: '为其他用户创建供应商',
+        username: `other_user_supplier_${Math.floor(Math.random() * 100000)}`,
+        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 supplierRepository = dataSource.getRepository(SupplierMt);
+
+      const userSupplier1 = supplierRepository.create({
+        tenantId: 1,
+        name: '用户供应商1',
+        username: `user_supplier1_${Math.floor(Math.random() * 100000)}`,
+        password: 'password123',
+        phone: '13800138002',
+        realname: '用户供应商1',
+        loginNum: 0,
+        loginTime: null,
+        loginIp: null,
+        lastLoginTime: null,
+        lastLoginIp: null,
+        state: 1,
+        createdBy: testUser.id
+      });
+      await supplierRepository.save(userSupplier1);
+
+      const userSupplier2 = supplierRepository.create({
+        tenantId: 1,
+        name: '用户供应商2',
+        username: `user_supplier2_${Math.floor(Math.random() * 100000)}`,
+        password: 'password123',
+        phone: '13800138003',
+        realname: '用户供应商2',
+        loginNum: 0,
+        loginTime: null,
+        loginIp: null,
+        lastLoginTime: null,
+        lastLoginIp: null,
+        state: 1,
+        createdBy: testUser.id
+      });
+      await supplierRepository.save(userSupplier2);
+
+      // 管理员应该能看到所有供应商
+      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 supplierRepository = dataSource.getRepository(SupplierMt);
+      const testSupplier = supplierRepository.create({
+        tenantId: 1,
+        name: '原始供应商',
+        username: `original_supplier_admin_${Math.floor(Math.random() * 100000)}`,
+        password: 'password123',
+        phone: '13800138004',
+        realname: '原始供应商',
+        loginNum: 0,
+        loginTime: null,
+        loginIp: null,
+        lastLoginTime: null,
+        lastLoginIp: null,
+        state: 1,
+        createdBy: testUser.id
+      });
+      await supplierRepository.save(testSupplier);
+
+      const updateData = {
+        name: '管理员更新的供应商',
+        phone: '13900139000',
+        realname: '管理员更新的供应商'
+      };
+
+      const response = await client[':id'].$put({
+        param: { id: testSupplier.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 supplierRepository = dataSource.getRepository(SupplierMt);
+      const testSupplier = supplierRepository.create({
+        tenantId: 1,
+        name: '待删除供应商',
+        username: `delete_supplier_admin_${Math.floor(Math.random() * 100000)}`,
+        password: 'password123',
+        phone: '13800138005',
+        realname: '待删除供应商',
+        loginNum: 0,
+        loginTime: null,
+        loginIp: null,
+        lastLoginTime: null,
+        lastLoginIp: null,
+        state: 1,
+        createdBy: testUser.id
+      });
+      await supplierRepository.save(testSupplier);
+
+      const response = await client[':id'].$delete({
+        param: { id: testSupplier.id }
+      }, {
+        headers: {
+          'Authorization': `Bearer ${adminToken}`
+        }
+      });
+
+      console.debug('管理员删除其他用户供应商响应状态:', response.status);
+      expect(response.status).toBe(204);
+
+      // 验证供应商确实被删除
+      const deletedSupplier = await supplierRepository.findOne({
+        where: { id: testSupplier.id }
+      });
+      expect(deletedSupplier).toBeNull();
+    });
+
+    it('管理员应该可以查询指定用户的供应商', async () => {
+      // 为测试用户创建一些供应商
+      const dataSource = await IntegrationTestDatabase.getDataSource();
+      const supplierRepository = dataSource.getRepository(SupplierMt);
+
+      const userSupplier = supplierRepository.create({
+        tenantId: 1,
+        name: '指定用户供应商',
+        username: `specified_user_supplier_${Math.floor(Math.random() * 100000)}`,
+        password: 'password123',
+        phone: '13800138006',
+        realname: '指定用户供应商',
+        loginNum: 0,
+        loginTime: null,
+        loginIp: null,
+        lastLoginTime: null,
+        lastLoginIp: null,
+        state: 1,
+        createdBy: testUser.id
+      });
+      await supplierRepository.save(userSupplier);
+
+      // 管理员可以查询指定用户的供应商
+      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((supplier: any) => {
+            expect(supplier.createdBy).toBe(testUser.id);
+          });
+        }
+      }
+    });
+  });
+
+  describe('供应商状态管理', () => {
+    it('应该支持供应商状态管理', async () => {
+      // 创建启用状态的供应商
+      const createData = {
+        name: '状态测试供应商',
+        username: `status_test_supplier_${Math.floor(Math.random() * 100000)}`,
+        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 createdData = await createResponse.json();
+
+      // 更新为禁用状态
+      if (typeof createdData === 'object' && createdData !== null && 'id' in createdData) {
+        const updateResponse = await client[':id'].$put({
+          param: { id: createdData.id },
+          json: { state: 2 } // 禁用状态
+        }, {
+          headers: {
+            'Authorization': `Bearer ${adminToken}`
+          }
+        });
+
+        expect(updateResponse.status).toBe(200);
+        const updatedData = await updateResponse.json();
+        if (typeof updatedData === 'object' && updatedData !== null && 'state' in updatedData) {
+          expect(updatedData.state).toBe(2);
+        }
+      }
+    });
+  });
+
+  describe('供应商登录统计', () => {
+    it('应该支持供应商登录统计功能', async () => {
+      // 创建供应商
+      const createData = {
+        name: '登录统计供应商',
+        username: `login_stat_supplier_${Math.floor(Math.random() * 100000)}`,
+        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 createdData = await createResponse.json();
+
+      // 验证初始登录统计
+      if (typeof createdData === 'object' && createdData !== null) {
+        if ('loginNum' in createdData) expect(createdData.loginNum).toBe(0);
+        if ('loginTime' in createdData) expect(createdData.loginTime).toBeNull();
+        if ('lastLoginTime' in createdData) expect(createdData.lastLoginTime).toBeNull();
+        if ('loginIp' in createdData) expect(createdData.loginIp).toBeNull();
+        if ('lastLoginIp' in createdData) expect(createdData.lastLoginIp).toBeNull();
+      }
+
+      // 获取供应商详情验证字段存在
+      if (typeof createdData === 'object' && createdData !== null && 'id' in createdData) {
+        const getResponse = await client[':id'].$get({
+          param: { id: createdData.id }
+        }, {
+          headers: {
+            'Authorization': `Bearer ${adminToken}`
+          }
+        });
+
+        expect(getResponse.status).toBe(200);
+        const supplierData = await getResponse.json();
+        if (typeof supplierData === 'object' && supplierData !== null) {
+          expect(supplierData).toHaveProperty('loginNum');
+          expect(supplierData).toHaveProperty('loginTime');
+          expect(supplierData).toHaveProperty('lastLoginTime');
+          expect(supplierData).toHaveProperty('loginIp');
+          expect(supplierData).toHaveProperty('lastLoginIp');
+        }
+      }
+    });
+  });
+
+  describe('跨租户数据隔离', () => {
+    let tenant2AdminToken: string;
+    let tenant2Admin: UserEntityMt;
+
+    beforeEach(async () => {
+      // 获取数据源
+      const dataSource = await IntegrationTestDatabase.getDataSource();
+      const userRepository = dataSource.getRepository(UserEntityMt);
+
+      // 创建租户2的管理员
+      tenant2Admin = userRepository.create({
+        username: `tenant2_admin_${Math.floor(Math.random() * 100000)}`,
+        password: 'admin_password',
+        nickname: '租户2管理员',
+        registrationSource: 'web',
+        tenantId: 2
+      });
+      await userRepository.save(tenant2Admin);
+
+      // 生成租户2管理员的token
+      tenant2AdminToken = JWTUtil.generateToken({
+        id: tenant2Admin.id,
+        username: tenant2Admin.username,
+        roles: [{name:'admin'}],
+        tenantId: tenant2Admin.tenantId
+      });
+
+      // 为租户1创建一些供应商
+      const supplierRepository = dataSource.getRepository(SupplierMt);
+      const tenant1Supplier1 = supplierRepository.create({
+        name: '租户1供应商1',
+        username: `tenant1_supplier1_${Math.floor(Math.random() * 100000)}`,
+        password: 'password123',
+        phone: '13800138009',
+        realname: '租户1供应商1',
+        state: 1,
+        tenantId: 1,
+        createdBy: testUser.id
+      });
+      await supplierRepository.save(tenant1Supplier1);
+
+      const tenant1Supplier2 = supplierRepository.create({
+        name: '租户1供应商2',
+        username: `tenant1_supplier2_${Math.floor(Math.random() * 100000)}`,
+        password: 'password123',
+        phone: '13800138010',
+        realname: '租户1供应商2',
+        state: 1,
+        tenantId: 1,
+        createdBy: testUser.id
+      });
+      await supplierRepository.save(tenant1Supplier2);
+
+      // 为租户2创建一些供应商
+      const tenant2Supplier1 = supplierRepository.create({
+        name: '租户2供应商1',
+        username: `tenant2_supplier1_${Math.floor(Math.random() * 100000)}`,
+        password: 'password123',
+        phone: '13800138011',
+        realname: '租户2供应商1',
+        state: 1,
+        tenantId: 2,
+        createdBy: tenant2Admin.id
+      });
+      await supplierRepository.save(tenant2Supplier1);
+    });
+
+    it('应该实现租户数据隔离 - 租户1管理员只能看到租户1的供应商', async () => {
+      const response = await client.index.$get({
+        query: {}
+      }, {
+        headers: {
+          'Authorization': `Bearer ${adminToken}`
+        }
+      });
+
+      expect(response.status).toBe(200);
+      const data = await response.json();
+
+      // 验证租户1管理员只能看到租户1的供应商
+      if (data.data && Array.isArray(data.data)) {
+        data.data.forEach((supplier: any) => {
+          expect(supplier.tenantId).toBe(1);
+        });
+      }
+    });
+
+    it('应该实现租户数据隔离 - 租户2管理员只能看到租户2的供应商', async () => {
+      const response = await client.index.$get({
+        query: {}
+      }, {
+        headers: {
+          'Authorization': `Bearer ${tenant2AdminToken}`
+        }
+      });
+
+      expect(response.status).toBe(200);
+      const data = await response.json();
+
+      // 验证租户2管理员只能看到租户2的供应商
+      if (data.data && Array.isArray(data.data)) {
+        data.data.forEach((supplier: any) => {
+          expect(supplier.tenantId).toBe(2);
+        });
+      }
+    });
+
+    it('应该阻止跨租户访问供应商详情', async () => {
+      // 获取租户1的供应商列表
+      const listResponse = await client.index.$get({
+        query: {}
+      }, {
+        headers: {
+          'Authorization': `Bearer ${adminToken}`
+        }
+      });
+
+      expect(listResponse.status).toBe(200);
+      const listData = await listResponse.json();
+
+      if (listData.data && Array.isArray(listData.data) && listData.data.length > 0) {
+        const tenant1SupplierId = listData.data[0].id;
+
+        // 租户2管理员尝试访问租户1的供应商
+        const getResponse = await client[':id'].$get({
+          param: { id: tenant1SupplierId }
+        }, {
+          headers: {
+            'Authorization': `Bearer ${tenant2AdminToken}`
+          }
+        });
+
+        // 跨租户访问应该返回404
+        expect(getResponse.status).toBe(404);
+      }
+    });
+
+    it('应该阻止跨租户更新供应商', async () => {
+      // 获取租户1的供应商列表
+      const listResponse = await client.index.$get({
+        query: {}
+      }, {
+        headers: {
+          'Authorization': `Bearer ${adminToken}`
+        }
+      });
+
+      expect(listResponse.status).toBe(200);
+      const listData = await listResponse.json();
+
+      if (listData.data && Array.isArray(listData.data) && listData.data.length > 0) {
+        const tenant1SupplierId = listData.data[0].id;
+
+        // 租户2管理员尝试更新租户1的供应商
+        const updateResponse = await client[':id'].$put({
+          param: { id: tenant1SupplierId },
+          json: { name: '跨租户更新测试' }
+        }, {
+          headers: {
+            'Authorization': `Bearer ${tenant2AdminToken}`
+          }
+        });
+
+        // 跨租户更新应该返回404
+        expect(updateResponse.status).toBe(404);
+      }
+    });
+
+    it('应该阻止跨租户删除供应商', async () => {
+      // 获取租户1的供应商列表
+      const listResponse = await client.index.$get({
+        query: {}
+      }, {
+        headers: {
+          'Authorization': `Bearer ${adminToken}`
+        }
+      });
+
+      expect(listResponse.status).toBe(200);
+      const listData = await listResponse.json();
+
+      if (listData.data && Array.isArray(listData.data) && listData.data.length > 0) {
+        const tenant1SupplierId = listData.data[0].id;
+
+        // 租户2管理员尝试删除租户1的供应商
+        const deleteResponse = await client[':id'].$delete({
+          param: { id: tenant1SupplierId }
+        }, {
+          headers: {
+            'Authorization': `Bearer ${tenant2AdminToken}`
+          }
+        });
+
+        // 跨租户删除应该返回404
+        expect(deleteResponse.status).toBe(404);
+      }
+    });
+  });
+});

+ 846 - 0
packages/supplier-module-mt/tests/integration/user-routes.integration.test.ts

@@ -0,0 +1,846 @@
+import { describe, it, expect, beforeEach } from 'vitest';
+import { testClient } from 'hono/testing';
+import { IntegrationTestDatabase, setupIntegrationDatabaseHooksWithEntities } from '@d8d/shared-test-util';
+import { JWTUtil } from '@d8d/shared-utils';
+import { UserEntityMt, RoleMt } from '@d8d/user-module-mt';
+import { FileMt } from '@d8d/file-module-mt';
+import { userSupplierRoutes } from '../../src/routes';
+import { SupplierMt } from '../../src/entities';
+
+// 设置集成测试钩子
+setupIntegrationDatabaseHooksWithEntities([UserEntityMt, RoleMt, SupplierMt, FileMt])
+
+describe('用户供应商管理API集成测试', () => {
+  let client: ReturnType<typeof testClient<typeof userSupplierRoutes>>;
+  let userToken: string;
+  let otherUserToken: string;
+  let testUser: UserEntityMt;
+  let otherUser: UserEntityMt;
+
+  beforeEach(async () => {
+    // 创建测试客户端
+    client = testClient(userSupplierRoutes);
+
+    // 获取数据源
+    const dataSource = await IntegrationTestDatabase.getDataSource();
+
+    // 创建测试用户
+    const userRepository = dataSource.getRepository(UserEntityMt);
+    testUser = userRepository.create({
+      username: `test_user_${Math.floor(Math.random() * 100000)}`,
+      password: 'test_password',
+      nickname: '测试用户',
+      registrationSource: 'web',
+      tenantId: 1
+    });
+    await userRepository.save(testUser);
+
+    // 创建其他用户
+    otherUser = userRepository.create({
+      username: `other_user_${Math.floor(Math.random() * 100000)}`,
+      password: 'other_password',
+      nickname: '其他用户',
+      registrationSource: 'web',
+      tenantId: 1
+    });
+    await userRepository.save(otherUser);
+
+    // 生成测试用户的token
+    userToken = JWTUtil.generateToken({
+      id: testUser.id,
+      username: testUser.username,
+      roles: [{name:'user'}],
+      tenantId: testUser.tenantId
+    });
+
+    // 生成其他用户的token
+    otherUserToken = JWTUtil.generateToken({
+      id: otherUser.id,
+      username: otherUser.username,
+      roles: [{name:'user'}],
+      tenantId: otherUser.tenantId
+    });
+  });
+
+  describe('GET /suppliers', () => {
+    it('应该返回当前用户的供应商列表', async () => {
+      // 为测试用户创建一些供应商
+      const dataSource = await IntegrationTestDatabase.getDataSource();
+      const supplierRepository = dataSource.getRepository(SupplierMt);
+
+      const userSupplier1 = supplierRepository.create({
+        tenantId: 1,
+        name: '用户供应商1',
+        username: `user_supplier1_${Math.floor(Math.random() * 100000)}`,
+        password: 'password123',
+        phone: '13800138001',
+        realname: '用户供应商1',
+        loginNum: 0,
+        loginTime: null,
+        loginIp: null,
+        lastLoginTime: null,
+        lastLoginIp: null,
+        state: 1,
+        createdBy: testUser.id
+      });
+      await supplierRepository.save(userSupplier1);
+
+      const userSupplier2 = supplierRepository.create({
+        tenantId: 1,
+        name: '用户供应商2',
+        username: `user_supplier2_${Math.floor(Math.random() * 100000)}`,
+        password: 'password123',
+        phone: '13800138002',
+        realname: '用户供应商2',
+        loginNum: 0,
+        loginTime: null,
+        loginIp: null,
+        lastLoginTime: null,
+        lastLoginIp: null,
+        state: 1,
+        createdBy: testUser.id
+      });
+      await supplierRepository.save(userSupplier2);
+
+      // 为其他用户创建一个供应商,确保不会返回
+      const otherUserSupplier = supplierRepository.create({
+        tenantId: 1,
+        name: '其他用户供应商',
+        username: `other_supplier_${Math.floor(Math.random() * 100000)}`,
+        password: 'password123',
+        phone: '13800138003',
+        realname: '其他用户供应商',
+        loginNum: 0,
+        loginTime: null,
+        loginIp: null,
+        lastLoginTime: null,
+        lastLoginIp: null,
+        state: 1,
+        createdBy: otherUser.id
+      });
+      await supplierRepository.save(otherUserSupplier);
+
+      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((supplier: any) => {
+            expect(supplier.createdBy).toBe(testUser.id);
+          });
+        }
+      }
+    });
+
+    it('应该拒绝未认证用户的访问', async () => {
+      const response = await client.index.$get({
+        query: {}
+      });
+      expect(response.status).toBe(401);
+    });
+  });
+
+  describe('POST /suppliers', () => {
+    it('应该成功创建供应商并自动使用当前用户ID', async () => {
+      const createData = {
+        name: '测试供应商',
+        username: `test_supplier_${Math.floor(Math.random() * 100000)}`,
+        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: '',
+        phone: '',
+        realname: ''
+      };
+
+      const response = await client.index.$post({
+        json: invalidData
+      }, {
+        headers: {
+          'Authorization': `Bearer ${userToken}`
+        }
+      });
+
+      expect(response.status).toBe(400);
+    });
+  });
+
+  describe('GET /suppliers/:id', () => {
+    it('应该返回当前用户的供应商详情', async () => {
+      // 先为当前用户创建一个供应商
+      const dataSource = await IntegrationTestDatabase.getDataSource();
+      const supplierRepository = dataSource.getRepository(SupplierMt);
+      const testSupplier = supplierRepository.create({
+        tenantId: 1,
+        name: '测试供应商详情',
+        username: `test_supplier_detail_${Math.floor(Math.random() * 100000)}`,
+        password: 'password123',
+        phone: '13600136000',
+        realname: '测试供应商详情',
+        loginNum: 5,
+        loginTime: new Date('2024-01-01T12:00:00Z'),
+        loginIp: '192.168.1.1',
+        lastLoginTime: new Date('2024-01-01T12:00:00Z'),
+        lastLoginIp: '192.168.1.1',
+        state: 1,
+        createdBy: testUser.id
+      });
+      await supplierRepository.save(testSupplier);
+
+      const response = await client[':id'].$get({
+        param: { id: testSupplier.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(testSupplier.id);
+        expect(data.createdBy).toBe(testUser.id);
+        expect(data.name).toBe(testSupplier.name);
+        expect(data.username).toBe(testSupplier.username);
+        expect(data.phone).toBe(testSupplier.phone);
+        expect(data.realname).toBe(testSupplier.realname);
+      }
+    });
+
+    it('应该拒绝访问其他用户的供应商', async () => {
+      // 为其他用户创建一个供应商
+      const dataSource = await IntegrationTestDatabase.getDataSource();
+      const supplierRepository = dataSource.getRepository(SupplierMt);
+      const otherUserSupplier = supplierRepository.create({
+        tenantId: 1,
+        name: '其他用户供应商',
+        username: `other_supplier_detail_${Math.floor(Math.random() * 100000)}`,
+        password: 'password123',
+        phone: '13600136001',
+        realname: '其他用户供应商',
+        loginNum: 0,
+        loginTime: null,
+        loginIp: null,
+        lastLoginTime: null,
+        lastLoginIp: null,
+        state: 1,
+        createdBy: otherUser.id
+      });
+      await supplierRepository.save(otherUserSupplier);
+
+      // 当前用户尝试访问其他用户的供应商
+      const response = await client[':id'].$get({
+        param: { id: otherUserSupplier.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 /suppliers/:id', () => {
+    it('应该成功更新当前用户的供应商', async () => {
+      // 先为当前用户创建一个供应商
+      const dataSource = await IntegrationTestDatabase.getDataSource();
+      const supplierRepository = dataSource.getRepository(SupplierMt);
+      const testSupplier = supplierRepository.create({
+        tenantId: 1,
+        name: '原始供应商',
+        username: `original_supplier_${Math.floor(Math.random() * 100000)}`,
+        password: 'password123',
+        phone: '13500135000',
+        realname: '原始供应商',
+        loginNum: 0,
+        loginTime: null,
+        loginIp: null,
+        lastLoginTime: null,
+        lastLoginIp: null,
+        state: 1,
+        createdBy: testUser.id
+      });
+      await supplierRepository.save(testSupplier);
+
+      const updateData = {
+        name: '更新后的供应商',
+        phone: '13700137000',
+        realname: '更新后的供应商',
+        state: 2
+      };
+
+      const response = await client[':id'].$put({
+        param: { id: testSupplier.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 supplierRepository = dataSource.getRepository(SupplierMt);
+      const otherUserSupplier = supplierRepository.create({
+        tenantId: 1,
+        name: '其他用户供应商',
+        username: `other_supplier_update_${Math.floor(Math.random() * 100000)}`,
+        password: 'password123',
+        phone: '13500135001',
+        realname: '其他用户供应商',
+        loginNum: 0,
+        loginTime: null,
+        loginIp: null,
+        lastLoginTime: null,
+        lastLoginIp: null,
+        state: 1,
+        createdBy: otherUser.id
+      });
+      await supplierRepository.save(otherUserSupplier);
+
+      const updateData = {
+        name: '尝试更新的供应商',
+        phone: '13700137001',
+        realname: '尝试更新的供应商'
+      };
+
+      // 当前用户尝试更新其他用户的供应商
+      const response = await client[':id'].$put({
+        param: { id: otherUserSupplier.id },
+        json: updateData
+      }, {
+        headers: {
+          'Authorization': `Bearer ${userToken}`
+        }
+      });
+
+      console.debug('用户更新其他用户供应商响应状态:', response.status);
+      expect(response.status).toBe(403); // 数据权限控制返回403
+    });
+  });
+
+  describe('DELETE /suppliers/:id', () => {
+    it('应该成功删除当前用户的供应商', async () => {
+      // 先为当前用户创建一个供应商
+      const dataSource = await IntegrationTestDatabase.getDataSource();
+      const supplierRepository = dataSource.getRepository(SupplierMt);
+      const testSupplier = supplierRepository.create({
+        tenantId: 1,
+        name: '待删除供应商',
+        username: `delete_supplier_${Math.floor(Math.random() * 100000)}`,
+        password: 'password123',
+        phone: '13400134000',
+        realname: '待删除供应商',
+        loginNum: 0,
+        loginTime: null,
+        loginIp: null,
+        lastLoginTime: null,
+        lastLoginIp: null,
+        state: 1,
+        createdBy: testUser.id
+      });
+      await supplierRepository.save(testSupplier);
+
+      const response = await client[':id'].$delete({
+        param: { id: testSupplier.id }
+      }, {
+        headers: {
+          'Authorization': `Bearer ${userToken}`
+        }
+      });
+
+      console.debug('用户删除供应商响应状态:', response.status);
+      expect(response.status).toBe(204);
+
+      // 验证供应商确实被删除
+      const deletedSupplier = await supplierRepository.findOne({
+        where: { id: testSupplier.id }
+      });
+      expect(deletedSupplier).toBeNull();
+    });
+
+    it('应该拒绝删除其他用户的供应商', async () => {
+      // 为其他用户创建一个供应商
+      const dataSource = await IntegrationTestDatabase.getDataSource();
+      const supplierRepository = dataSource.getRepository(SupplierMt);
+      const otherUserSupplier = supplierRepository.create({
+        tenantId: 1,
+        name: '其他用户供应商',
+        username: `other_supplier_delete_${Math.floor(Math.random() * 100000)}`,
+        password: 'password123',
+        phone: '13400134001',
+        realname: '其他用户供应商',
+        loginNum: 0,
+        loginTime: null,
+        loginIp: null,
+        lastLoginTime: null,
+        lastLoginIp: null,
+        state: 1,
+        createdBy: otherUser.id
+      });
+      await supplierRepository.save(otherUserSupplier);
+
+      // 当前用户尝试删除其他用户的供应商
+      const response = await client[':id'].$delete({
+        param: { id: otherUserSupplier.id }
+      }, {
+        headers: {
+          'Authorization': `Bearer ${userToken}`
+        }
+      });
+
+      console.debug('用户删除其他用户供应商响应状态:', response.status);
+      expect(response.status).toBe(403); // 数据权限控制返回403
+    });
+  });
+
+  describe('数据权限验证', () => {
+    it('用户应该只能访问和操作自己的数据', async () => {
+      // 为两个用户都创建供应商
+      const dataSource = await IntegrationTestDatabase.getDataSource();
+      const supplierRepository = dataSource.getRepository(SupplierMt);
+
+      const userSupplier = supplierRepository.create({
+        tenantId: 1,
+        name: '用户供应商',
+        username: `user_supplier_perm_${Math.floor(Math.random() * 100000)}`,
+        password: 'password123',
+        phone: '13800138004',
+        realname: '用户供应商',
+        loginNum: 0,
+        loginTime: null,
+        loginIp: null,
+        lastLoginTime: null,
+        lastLoginIp: null,
+        state: 1,
+        createdBy: testUser.id
+      });
+      await supplierRepository.save(userSupplier);
+
+      const otherUserSupplier = supplierRepository.create({
+        tenantId: 1,
+        name: '其他用户供应商',
+        username: `other_supplier_perm_${Math.floor(Math.random() * 100000)}`,
+        password: 'password123',
+        phone: '13800138005',
+        realname: '其他用户供应商',
+        loginNum: 0,
+        loginTime: null,
+        loginIp: null,
+        lastLoginTime: null,
+        lastLoginIp: null,
+        state: 1,
+        createdBy: otherUser.id
+      });
+      await supplierRepository.save(otherUserSupplier);
+
+      // 当前用户应该只能看到自己的供应商
+      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((supplier: any) => {
+          expect(supplier.createdBy).toBe(testUser.id);
+        });
+      }
+
+      // 当前用户应该无法访问其他用户的供应商详情
+      const getResponse = await client[':id'].$get({
+        param: { id: otherUserSupplier.id }
+      }, {
+        headers: {
+          'Authorization': `Bearer ${userToken}`
+        }
+      });
+      expect(getResponse.status).toBe(404);
+
+      // 当前用户应该无法更新其他用户的供应商
+      const updateResponse = await client[':id'].$put({
+        param: { id: otherUserSupplier.id },
+        json: { name: '尝试更新' }
+      }, {
+        headers: {
+          'Authorization': `Bearer ${userToken}`
+        }
+      });
+      expect(updateResponse.status).toBe(403);
+
+      // 当前用户应该无法删除其他用户的供应商
+      const deleteResponse = await client[':id'].$delete({
+        param: { id: otherUserSupplier.id }
+      }, {
+        headers: {
+          'Authorization': `Bearer ${userToken}`
+        }
+      });
+      expect(deleteResponse.status).toBe(403);
+    });
+  });
+
+  describe('供应商状态管理', () => {
+    it('应该支持供应商状态管理', async () => {
+      // 创建启用状态的供应商
+      const createData = {
+        name: '状态测试供应商',
+        username: `status_test_supplier_${Math.floor(Math.random() * 100000)}`,
+        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 createdData = await createResponse.json();
+
+      // 更新为禁用状态
+      if (typeof createdData === 'object' && createdData !== null && 'id' in createdData) {
+        const updateResponse = await client[':id'].$put({
+          param: { id: createdData.id },
+          json: { state: 2 } // 禁用状态
+        }, {
+          headers: {
+            'Authorization': `Bearer ${userToken}`
+          }
+        });
+
+        expect(updateResponse.status).toBe(200);
+        const updatedData = await updateResponse.json();
+        if (typeof updatedData === 'object' && updatedData !== null && 'state' in updatedData) {
+          expect(updatedData.state).toBe(2);
+        }
+      }
+    });
+  });
+
+  describe('供应商登录统计', () => {
+    it('应该支持供应商登录统计功能', async () => {
+      // 创建供应商
+      const createData = {
+        name: '登录统计供应商',
+        username: `login_stat_supplier_${Math.floor(Math.random() * 100000)}`,
+        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 createdData = await createResponse.json();
+
+      // 验证初始登录统计
+      if (typeof createdData === 'object' && createdData !== null) {
+        if ('loginNum' in createdData) expect(createdData.loginNum).toBe(0);
+        if ('loginTime' in createdData) expect(createdData.loginTime).toBeNull();
+        if ('lastLoginTime' in createdData) expect(createdData.lastLoginTime).toBeNull();
+        if ('loginIp' in createdData) expect(createdData.loginIp).toBeNull();
+        if ('lastLoginIp' in createdData) expect(createdData.lastLoginIp).toBeNull();
+      }
+
+      // 获取供应商详情验证字段存在
+      if (typeof createdData === 'object' && createdData !== null && 'id' in createdData) {
+        const getResponse = await client[':id'].$get({
+          param: { id: createdData.id }
+        }, {
+          headers: {
+            'Authorization': `Bearer ${userToken}`
+          }
+        });
+
+        expect(getResponse.status).toBe(200);
+        const supplierData = await getResponse.json();
+        if (typeof supplierData === 'object' && supplierData !== null) {
+          expect(supplierData).toHaveProperty('loginNum');
+          expect(supplierData).toHaveProperty('loginTime');
+          expect(supplierData).toHaveProperty('lastLoginTime');
+          expect(supplierData).toHaveProperty('loginIp');
+          expect(supplierData).toHaveProperty('lastLoginIp');
+        }
+      }
+    });
+  });
+
+  describe('跨租户数据隔离', () => {
+    let tenant2UserToken: string;
+    let tenant2User: UserEntityMt;
+
+    beforeEach(async () => {
+      // 获取数据源
+      const dataSource = await IntegrationTestDatabase.getDataSource();
+      const userRepository = dataSource.getRepository(UserEntityMt);
+
+      // 创建租户2的用户
+      tenant2User = userRepository.create({
+        username: `tenant2_user_${Math.floor(Math.random() * 100000)}`,
+        password: 'test_password',
+        nickname: '租户2用户',
+        registrationSource: 'web',
+        tenantId: 2
+      });
+      await userRepository.save(tenant2User);
+
+      // 生成租户2用户的token
+      tenant2UserToken = JWTUtil.generateToken({
+        id: tenant2User.id,
+        username: tenant2User.username,
+        roles: [],
+        tenantId: tenant2User.tenantId
+      });
+
+      // 为租户1创建一些供应商
+      const supplierRepository = dataSource.getRepository(SupplierMt);
+      const tenant1Supplier1 = supplierRepository.create({
+        name: '租户1供应商1',
+        username: `tenant1_supplier1_${Math.floor(Math.random() * 100000)}`,
+        password: 'password123',
+        phone: '13800138008',
+        realname: '租户1供应商1',
+        state: 1,
+        tenantId: 1,
+        createdBy: testUser.id
+      });
+      await supplierRepository.save(tenant1Supplier1);
+
+      // 为租户2创建一些供应商
+      const tenant2Supplier1 = supplierRepository.create({
+        name: '租户2供应商1',
+        username: `tenant2_supplier1_${Math.floor(Math.random() * 100000)}`,
+        password: 'password123',
+        phone: '13800138009',
+        realname: '租户2供应商1',
+        state: 1,
+        tenantId: 2,
+        createdBy: tenant2User.id
+      });
+      await supplierRepository.save(tenant2Supplier1);
+    });
+
+    it('应该实现租户数据隔离 - 租户1用户只能看到租户1的供应商', async () => {
+      const response = await client.index.$get({
+        query: {}
+      }, {
+        headers: {
+          'Authorization': `Bearer ${userToken}`
+        }
+      });
+
+      expect(response.status).toBe(200);
+      const data = await response.json();
+
+      // 验证租户1用户只能看到租户1的供应商
+      if (data.data && Array.isArray(data.data)) {
+        data.data.forEach((supplier: any) => {
+          expect(supplier.tenantId).toBe(1);
+          expect(supplier.createdBy).toBe(testUser.id);
+        });
+      }
+    });
+
+    it('应该实现租户数据隔离 - 租户2用户只能看到租户2的供应商', async () => {
+      const response = await client.index.$get({
+        query: {}
+      }, {
+        headers: {
+          'Authorization': `Bearer ${tenant2UserToken}`
+        }
+      });
+
+      expect(response.status).toBe(200);
+      const data = await response.json();
+
+      // 验证租户2用户只能看到租户2的供应商
+      if (data.data && Array.isArray(data.data)) {
+        data.data.forEach((supplier: any) => {
+          expect(supplier.tenantId).toBe(2);
+          expect(supplier.createdBy).toBe(tenant2User.id);
+        });
+      }
+    });
+
+    it('应该阻止跨租户访问供应商详情', async () => {
+      // 获取租户1的供应商列表
+      const listResponse = await client.index.$get({
+        query: {}
+      }, {
+        headers: {
+          'Authorization': `Bearer ${userToken}`
+        }
+      });
+
+      expect(listResponse.status).toBe(200);
+      const listData = await listResponse.json();
+
+      if (listData.data && Array.isArray(listData.data) && listData.data.length > 0) {
+        const tenant1SupplierId = listData.data[0].id;
+
+        // 租户2用户尝试访问租户1的供应商
+        const getResponse = await client[':id'].$get({
+          param: { id: tenant1SupplierId }
+        }, {
+          headers: {
+            'Authorization': `Bearer ${tenant2UserToken}`
+          }
+        });
+
+        // 跨租户访问应该返回404
+        expect(getResponse.status).toBe(404);
+      }
+    });
+
+    it('应该阻止跨租户更新供应商', async () => {
+      // 获取租户1的供应商列表
+      const listResponse = await client.index.$get({
+        query: {}
+      }, {
+        headers: {
+          'Authorization': `Bearer ${userToken}`
+        }
+      });
+
+      expect(listResponse.status).toBe(200);
+      const listData = await listResponse.json();
+
+      if (listData.data && Array.isArray(listData.data) && listData.data.length > 0) {
+        const tenant1SupplierId = listData.data[0].id;
+
+        // 租户2用户尝试更新租户1的供应商
+        const updateResponse = await client[':id'].$put({
+          param: { id: tenant1SupplierId },
+          json: { name: '跨租户更新测试' }
+        }, {
+          headers: {
+            'Authorization': `Bearer ${tenant2UserToken}`
+          }
+        });
+
+        // 跨租户更新应该返回404
+        expect(updateResponse.status).toBe(404);
+      }
+    });
+
+    it('应该阻止跨租户删除供应商', async () => {
+      // 获取租户1的供应商列表
+      const listResponse = await client.index.$get({
+        query: {}
+      }, {
+        headers: {
+          'Authorization': `Bearer ${userToken}`
+        }
+      });
+
+      expect(listResponse.status).toBe(200);
+      const listData = await listResponse.json();
+
+      if (listData.data && Array.isArray(listData.data) && listData.data.length > 0) {
+        const tenant1SupplierId = listData.data[0].id;
+
+        // 租户2用户尝试删除租户1的供应商
+        const deleteResponse = await client[':id'].$delete({
+          param: { id: tenant1SupplierId }
+        }, {
+          headers: {
+            'Authorization': `Bearer ${tenant2UserToken}`
+          }
+        });
+
+        // 跨租户删除应该返回404
+        expect(deleteResponse.status).toBe(404);
+      }
+    });
+  });
+});

+ 16 - 0
packages/supplier-module-mt/tsconfig.json

@@ -0,0 +1,16 @@
+{
+  "extends": "../../tsconfig.json",
+  "compilerOptions": {
+    "composite": true,
+    "rootDir": ".",
+    "outDir": "dist"
+  },
+  "include": [
+    "src/**/*",
+    "tests/**/*"
+  ],
+  "exclude": [
+    "node_modules",
+    "dist"
+  ]
+}

+ 21 - 0
packages/supplier-module-mt/vitest.config.ts

@@ -0,0 +1,21 @@
+import { defineConfig } from 'vitest/config';
+
+export default defineConfig({
+  test: {
+    globals: true,
+    environment: 'node',
+    include: ['tests/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
+    coverage: {
+      provider: 'v8',
+      reporter: ['text', 'json', 'html'],
+      exclude: [
+        'tests/**',
+        '**/*.d.ts',
+        '**/*.config.*',
+        '**/dist/**'
+      ]
+    },
+    // 关闭并行测试以避免数据库连接冲突
+    fileParallelism: false
+  }
+});

+ 55 - 0
pnpm-lock.yaml

@@ -1341,6 +1341,61 @@ importers:
         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/supplier-module-mt:
+    dependencies:
+      '@d8d/auth-module-mt':
+        specifier: workspace:*
+        version: link:../auth-module-mt
+      '@d8d/file-module-mt':
+        specifier: workspace:*
+        version: link:../file-module-mt
+      '@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-mt':
+        specifier: workspace:*
+        version: link:../user-module-mt
+      '@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/tenant-module-mt:
     dependencies:
       '@d8d/auth-module':