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

✨ feat(supplier-module): create supplier management module

- add package.json configuration for supplier module
- implement supplier entity with fields including name, username, password, login statistics and status
- create CRUD routes for both admin and user roles with different permissions
- develop supplier service extending generic CRUD functionality
- define supplier schemas for validation and OpenAPI documentation
- add TypeScript types for supplier data structures
- configure TypeScript and Vitest for development and testing
- set up ESLint for code quality control
yourname 1 місяць тому
батько
коміт
53b2da90dd

+ 78 - 0
packages/supplier-module/package.json

@@ -0,0 +1,78 @@
+{
+  "name": "@d8d/supplier-module",
+  "version": "1.0.0",
+  "description": "供应商管理模块 - 提供供应商的完整CRUD功能,包括供应商登录统计、状态管理等",
+  "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",
+    "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": "workspace:*",
+    "@d8d/user-module": "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"
+  },
+  "keywords": [
+    "supplier",
+    "vendor",
+    "crud",
+    "api",
+    "management"
+  ],
+  "author": "D8D Team",
+  "license": "MIT"
+}

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

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

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

@@ -0,0 +1,52 @@
+import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn } from 'typeorm';
+
+@Entity('supplier')
+export class Supplier {
+  @PrimaryGeneratedColumn({ unsigned: true })
+  id!: number;
+
+  @Column({ name: 'name', type: 'varchar', length: 255, nullable: true, comment: '供货商名称' })
+  name!: string | null;
+
+  @Column({ name: 'username', type: 'varchar', length: 20, 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: 'int', unsigned: true, default: 0, comment: '登录时间' })
+  loginTime!: number;
+
+  @Column({ name: 'login_ip', type: 'varchar', length: 15, nullable: true, comment: '登录IP' })
+  loginIp!: string | null;
+
+  @Column({ name: 'last_login_time', type: 'int', unsigned: true, default: 0, comment: '上次登录时间' })
+  lastLoginTime!: number;
+
+  @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/src/index.ts

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

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

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

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

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

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

@@ -0,0 +1,21 @@
+import { createCrudRoutes } from '@d8d/shared-crud';
+import { authMiddleware } from '@d8d/auth-module';
+import { Supplier } from '../entities/supplier.entity';
+import { UserSupplierSchema, CreateUserSupplierDto, UpdateUserSupplierDto } from '../schemas/user-supplier.schema';
+
+export const userSupplierRoutes = createCrudRoutes({
+  entity: Supplier,
+  createSchema: CreateUserSupplierDto,
+  updateSchema: UpdateUserSupplierDto,
+  getSchema: UserSupplierSchema,
+  listSchema: UserSupplierSchema,
+  searchFields: ['name', 'username', 'realname'],
+  middleware: [authMiddleware],
+  userTracking: {
+    createdByField: 'created_by',
+    updatedByField: 'updated_by'
+  },
+  dataPermission: {
+    userIdField: 'created_by'
+  }
+});

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

@@ -0,0 +1,123 @@
+import { z } from '@hono/zod-openapi';
+
+export const AdminSupplierSchema = z.object({
+  id: 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'
+  }),
+  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.number().int().nonnegative('登录时间必须为非负数').default(0).openapi({
+    description: '登录时间',
+    example: 0
+  }),
+  loginIp: z.string().max(15, 'IP地址最多15个字符').nullable().optional().openapi({
+    description: '登录IP',
+    example: '192.168.1.1'
+  }),
+  lastLoginTime: z.number().int().nonnegative('上次登录时间必须为非负数').default(0).openapi({
+    description: '上次登录时间',
+    example: 0
+  }),
+  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(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
+  }),
+  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(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
+  }),
+  updatedBy: z.number().int().positive().optional().openapi({
+    description: '更新用户ID',
+    example: 1
+  })
+});

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

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

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

@@ -0,0 +1,119 @@
+import { z } from '@hono/zod-openapi';
+
+export const SupplierSchema = z.object({
+  id: 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.number().int().nonnegative('登录时间必须为非负数').default(0).openapi({
+    description: '登录时间',
+    example: 0
+  }),
+  loginIp: z.string().max(15, 'IP地址最多15个字符').nullable().optional().openapi({
+    description: '登录IP',
+    example: '192.168.1.1'
+  }),
+  lastLoginTime: z.number().int().nonnegative('上次登录时间必须为非负数').default(0).openapi({
+    description: '上次登录时间',
+    example: 0
+  }),
+  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
+  })
+});

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

@@ -0,0 +1,107 @@
+import { z } from '@hono/zod-openapi';
+
+export const UserSupplierSchema = z.object({
+  id: 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'
+  }),
+  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.number().int().nonnegative('登录时间必须为非负数').default(0).openapi({
+    description: '登录时间',
+    example: 0
+  }),
+  loginIp: z.string().max(15, 'IP地址最多15个字符').nullable().optional().openapi({
+    description: '登录IP',
+    example: '192.168.1.1'
+  }),
+  lastLoginTime: z.number().int().nonnegative('上次登录时间必须为非负数').default(0).openapi({
+    description: '上次登录时间',
+    example: 0
+  }),
+  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'
+  })
+});
+
+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(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 UpdateUserSupplierDto = 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
+  })
+});

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

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

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

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

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

@@ -0,0 +1,31 @@
+import { z } from '@hono/zod-openapi';
+import { SupplierSchema, CreateSupplierDto, UpdateSupplierDto } from '../schemas/supplier.schema';
+import { UserSupplierSchema, CreateUserSupplierDto, UpdateUserSupplierDto } from '../schemas/user-supplier.schema';
+import { AdminSupplierSchema, CreateAdminSupplierDto, UpdateAdminSupplierDto } from '../schemas/admin-supplier.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;
+}

+ 16 - 0
packages/supplier-module/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/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
+  }
+});

+ 52 - 0
pnpm-lock.yaml

@@ -868,6 +868,58 @@ 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/supplier-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/user-module:
     dependencies:
       '@d8d/auth-module':