Ver código fonte

✨ feat(merchant): 完成商户模块基础结构和核心功能

- 创建商户模块基础结构,包括package.json、tsconfig.json和vitest.config.ts配置文件
- 迁移商户实体和类型定义,包含完整的商户字段和状态管理
- 重构商户服务,继承GenericCrudService并添加登录统计和查询功能
- 创建三种路由类型:基础路由、用户专用路由和管理员专用路由
- 实现数据权限控制,用户路由限制只能操作自己的数据,管理员路由拥有完整权限
- 创建对应的schema定义,包括基础schema、用户专用schema和管理员专用schema
- 集成认证中间件和用户追踪功能,记录创建和更新用户信息
yourname 1 mês atrás
pai
commit
40ac51f5ba

+ 58 - 37
docs/stories/005.012.merchant-module.story.md

@@ -22,43 +22,43 @@ Draft
 
 ## Tasks / Subtasks
 
-- [ ] Task 1: 创建 merchant-module package 基础结构 (AC: 1)
-  - [ ] 创建 packages/merchant-module 目录结构
-  - [ ] 配置 package.json,参考广告模块的依赖版本 [Source: packages/advertisements-module/package.json#L47-L66]
-  - [ ] 配置 tsconfig.json,参考广告模块配置 [Source: packages/advertisements-module/tsconfig.json#L1-L16]
-  - [ ] 配置 vitest.config.ts,参考广告模块配置 [Source: packages/advertisements-module/vitest.config.ts#L1-L21]
-  - [ ] 创建 src/index.ts 导出文件
-
-- [ ] Task 2: 迁移商户实体和类型定义 (AC: 2, 4)
-  - [ ] 迁移 Merchant 实体到 packages/merchant-module/src/entities/
-  - [ ] 迁移 MerchantSchema、CreateMerchantDto、UpdateMerchantDto 到 packages/merchant-module/src/schemas/
-  - [ ] 创建类型定义文件 packages/merchant-module/src/types/merchant.types.ts
-  - [ ] 更新实体导入路径,使用 workspace:* 依赖
-
-- [ ] Task 3: 迁移商户服务 (AC: 2, 3)
-  - [ ] 迁移 MerchantService 到 packages/merchant-module/src/services/
-  - [ ] 重构服务使用 shared-crud 基础设施
-  - [ ] 更新服务依赖注入配置
-
-- [ ] Task 4: 创建商户路由 (AC: 3, 4)
-  - [ ] 创建商户管理路由 packages/merchant-module/src/routes/index.ts
-  - [ ] 迁移商户的完整CRUD路由,使用 shared-crud 基础设施
-  - [ ] 集成认证中间件
-  - [ ] 配置用户追踪字段
-
-- [ ] Task 5: 创建当前用户权限API路由文件 (AC: 3, 4)
-  - [ ] 创建 packages/merchant-module/src/schemas/user-merchant.schema.ts - 用户专用schema
-  - [ ] 移除userId字段,自动使用当前登录用户权限
-  - [ ] 创建 packages/merchant-module/src/schemas/admin-merchant.schema.ts - 管理员专用schema
-  - [ ] 保留userId字段,允许管理员指定用户
-  - [ ] 创建 packages/merchant-module/src/routes/user-routes.ts - 仅限当前用户使用的路由
-  - [ ] 配置数据权限控制,使用 shared-crud 的 dataPermission 配置
-  - [ ] 设置 userIdField: 'createdBy',确保用户只能操作自己的数据
-  - [ ] 使用用户专用schema
-  - [ ] 创建 packages/merchant-module/src/routes/admin-routes.ts - 管理员使用的完整权限路由
-  - [ ] 配置管理员路由不使用数据权限控制,保持完整CRUD功能
-  - [ ] 使用管理员专用schema
-  - [ ] 更新 packages/merchant-module/src/routes/index.ts 导出两个路由集合
+- [x] Task 1: 创建 merchant-module package 基础结构 (AC: 1)
+  - [x] 创建 packages/merchant-module 目录结构
+  - [x] 配置 package.json,参考广告模块的依赖版本 [Source: packages/advertisements-module/package.json#L47-L66]
+  - [x] 配置 tsconfig.json,参考广告模块配置 [Source: packages/advertisements-module/tsconfig.json#L1-L16]
+  - [x] 配置 vitest.config.ts,参考广告模块配置 [Source: packages/advertisements-module/vitest.config.ts#L1-L21]
+  - [x] 创建 src/index.ts 导出文件
+
+- [x] Task 2: 迁移商户实体和类型定义 (AC: 2, 4)
+  - [x] 迁移 Merchant 实体到 packages/merchant-module/src/entities/
+  - [x] 迁移 MerchantSchema、CreateMerchantDto、UpdateMerchantDto 到 packages/merchant-module/src/schemas/
+  - [x] 创建类型定义文件 packages/merchant-module/src/types/merchant.types.ts
+  - [x] 更新实体导入路径,使用 workspace:* 依赖
+
+- [x] Task 3: 迁移商户服务 (AC: 2, 3)
+  - [x] 迁移 MerchantService 到 packages/merchant-module/src/services/
+  - [x] 重构服务使用 shared-crud 基础设施
+  - [x] 更新服务依赖注入配置
+
+- [x] Task 4: 创建商户路由 (AC: 3, 4)
+  - [x] 创建商户管理路由 packages/merchant-module/src/routes/index.ts
+  - [x] 迁移商户的完整CRUD路由,使用 shared-crud 基础设施
+  - [x] 集成认证中间件
+  - [x] 配置用户追踪字段
+
+- [x] Task 5: 创建当前用户权限API路由文件 (AC: 3, 4)
+  - [x] 创建 packages/merchant-module/src/schemas/user-merchant.schema.ts - 用户专用schema
+  - [x] 移除userId字段,自动使用当前登录用户权限
+  - [x] 创建 packages/merchant-module/src/schemas/admin-merchant.schema.ts - 管理员专用schema
+  - [x] 保留userId字段,允许管理员指定用户
+  - [x] 创建 packages/merchant-module/src/routes/user-routes.ts - 仅限当前用户使用的路由
+  - [x] 配置数据权限控制,使用 shared-crud 的 dataPermission 配置
+  - [x] 设置 userIdField: 'createdBy',确保用户只能操作自己的数据
+  - [x] 使用用户专用schema
+  - [x] 创建 packages/merchant-module/src/routes/admin-routes.ts - 管理员使用的完整权限路由
+  - [x] 配置管理员路由不使用数据权限控制,保持完整CRUD功能
+  - [x] 使用管理员专用schema
+  - [x] 更新 packages/merchant-module/src/routes/index.ts 导出两个路由集合
   - [ ] 验证用户路由只能访问和操作当前用户的数据
   - [ ] 验证管理员路由可以访问所有用户的数据
 
@@ -208,8 +208,29 @@ Draft
 ### Debug Log References
 
 ### Completion Notes List
+- Task 1-5 已完成:商户模块基础结构、实体迁移、服务重构、路由创建和权限API路由文件
+- 所有核心代码文件已创建并添加到git暂存区
+- 测试套件尚未创建(Task 6)
+- 系统集成尚未完成(Task 7-8)
 
 ### File List
+- packages/merchant-module/package.json
+- packages/merchant-module/tsconfig.json
+- packages/merchant-module/vitest.config.ts
+- packages/merchant-module/src/index.ts
+- packages/merchant-module/src/entities/merchant.entity.ts
+- packages/merchant-module/src/entities/index.ts
+- packages/merchant-module/src/types/merchant.types.ts
+- packages/merchant-module/src/types/index.ts
+- packages/merchant-module/src/schemas/merchant.schema.ts
+- packages/merchant-module/src/schemas/user-merchant.schema.ts
+- packages/merchant-module/src/schemas/admin-merchant.schema.ts
+- packages/merchant-module/src/schemas/index.ts
+- packages/merchant-module/src/services/merchant.service.ts
+- packages/merchant-module/src/services/index.ts
+- packages/merchant-module/src/routes/index.ts
+- packages/merchant-module/src/routes/user-routes.ts
+- packages/merchant-module/src/routes/admin-routes.ts
 
 ## QA Results
 

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

@@ -0,0 +1,78 @@
+{
+  "name": "@d8d/merchant-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": [
+    "merchant",
+    "business",
+    "crud",
+    "api",
+    "management"
+  ],
+  "author": "D8D Team",
+  "license": "MIT"
+}

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

@@ -0,0 +1 @@
+export { Merchant } from './merchant.entity';

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

@@ -0,0 +1,58 @@
+import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn } from 'typeorm';
+
+@Entity('merchant')
+export class Merchant {
+  @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;
+
+  @Column({ name: 'rsa_public_key', type: 'varchar', length: 2000, nullable: true, comment: '公钥' })
+  rsaPublicKey!: string | null;
+
+  @Column({ name: 'aes_key', type: 'varchar', length: 32, nullable: true, comment: 'aes秘钥' })
+  aesKey!: string | null;
+
+  @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;
+}

+ 5 - 0
packages/merchant-module/src/index.ts

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

+ 22 - 0
packages/merchant-module/src/routes/admin-routes.ts

@@ -0,0 +1,22 @@
+import { createCrudRoutes } from '@d8d/shared-crud';
+import { Merchant } from '../entities/merchant.entity';
+import { AdminMerchantSchema, AdminCreateMerchantDto, AdminUpdateMerchantDto } from '../schemas/admin-merchant.schema';
+import { authMiddleware } from '@d8d/auth-module';
+
+// 管理员专用商户路由(完整权限)
+export const adminMerchantRoutes = createCrudRoutes({
+  entity: Merchant,
+  createSchema: AdminCreateMerchantDto,
+  updateSchema: AdminUpdateMerchantDto,
+  getSchema: AdminMerchantSchema,
+  listSchema: AdminMerchantSchema,
+  searchFields: ['name', 'username', 'realname', 'phone'],
+  middleware: [authMiddleware],
+  userTracking: {
+    createdByField: 'createdBy',
+    updatedByField: 'updatedBy'
+  },
+  dataPermission: {
+    enabled: false // 管理员路由不使用数据权限控制
+  }
+});

+ 23 - 0
packages/merchant-module/src/routes/index.ts

@@ -0,0 +1,23 @@
+import { createCrudRoutes } from '@d8d/shared-crud';
+import { Merchant } from '../entities/merchant.entity';
+import { MerchantSchema, CreateMerchantDto, UpdateMerchantDto } from '../schemas/merchant.schema';
+import { authMiddleware } from '@d8d/auth-module';
+
+// 基础商户路由(完整权限)
+export const merchantRoutes = createCrudRoutes({
+  entity: Merchant,
+  createSchema: CreateMerchantDto,
+  updateSchema: UpdateMerchantDto,
+  getSchema: MerchantSchema,
+  listSchema: MerchantSchema,
+  searchFields: ['name', 'username', 'realname', 'phone'],
+  middleware: [authMiddleware],
+  userTracking: {
+    createdByField: 'createdBy',
+    updatedByField: 'updatedBy'
+  }
+});
+
+// 导出所有路由
+export { userMerchantRoutes } from './user-routes';
+export { adminMerchantRoutes } from './admin-routes';

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

@@ -0,0 +1,26 @@
+import { createCrudRoutes } from '@d8d/shared-crud';
+import { Merchant } from '../entities/merchant.entity';
+import { UserMerchantSchema, UserCreateMerchantDto, UserUpdateMerchantDto } from '../schemas/user-merchant.schema';
+import { authMiddleware } from '@d8d/auth-module';
+
+// 用户专用商户路由(仅限当前用户使用)
+export const userMerchantRoutes = createCrudRoutes({
+  entity: Merchant,
+  createSchema: UserCreateMerchantDto,
+  updateSchema: UserUpdateMerchantDto,
+  getSchema: UserMerchantSchema,
+  listSchema: UserMerchantSchema,
+  searchFields: ['name', 'username', 'realname', 'phone'],
+  middleware: [authMiddleware],
+  userTracking: {
+    createdByField: 'createdBy',
+    updatedByField: 'updatedBy'
+  },
+  dataPermission: {
+    enabled: true,
+    userIdField: 'createdBy',
+    adminOverride: {
+      enabled: false // 用户路由不允许管理员覆盖
+    }
+  }
+});

+ 151 - 0
packages/merchant-module/src/schemas/admin-merchant.schema.ts

@@ -0,0 +1,151 @@
+import { z } from '@hono/zod-openapi';
+
+export const AdminMerchantSchema = 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: 'merchant001'
+  }),
+  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
+  }),
+  rsaPublicKey: z.string().max(2000, '公钥最多2000个字符').nullable().optional().openapi({
+    description: '公钥',
+    example: '-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...\n-----END PUBLIC KEY-----'
+  }),
+  aesKey: z.string().max(32, 'aes秘钥最多32个字符').nullable().optional().openapi({
+    description: 'aes秘钥',
+    example: 'a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6'
+  }),
+  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 AdminCreateMerchantDto = 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: 'merchant001'
+  }),
+  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
+  }),
+  rsaPublicKey: z.string().max(2000, '公钥最多2000个字符').nullable().optional().openapi({
+    description: '公钥',
+    example: '-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...\n-----END PUBLIC KEY-----'
+  }),
+  aesKey: z.string().max(32, 'aes秘钥最多32个字符').nullable().optional().openapi({
+    description: 'aes秘钥',
+    example: 'a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6'
+  }),
+  createdBy: z.number().int().positive().nullable().optional().openapi({
+    description: '创建用户ID',
+    example: 1
+  })
+});
+
+export const AdminUpdateMerchantDto = 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: 'merchant001'
+  }),
+  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
+  }),
+  rsaPublicKey: z.string().max(2000, '公钥最多2000个字符').nullable().optional().openapi({
+    description: '公钥',
+    example: '-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...\n-----END PUBLIC KEY-----'
+  }),
+  aesKey: z.string().max(32, 'aes秘钥最多32个字符').nullable().optional().openapi({
+    description: 'aes秘钥',
+    example: 'a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6'
+  }),
+  updatedBy: z.number().int().positive().nullable().optional().openapi({
+    description: '更新用户ID',
+    example: 1
+  })
+});

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

@@ -0,0 +1,3 @@
+export { MerchantSchema, CreateMerchantDto, UpdateMerchantDto } from './merchant.schema';
+export { UserMerchantSchema, UserCreateMerchantDto, UserUpdateMerchantDto } from './user-merchant.schema';
+export { AdminMerchantSchema, AdminCreateMerchantDto, AdminUpdateMerchantDto } from './admin-merchant.schema';

+ 143 - 0
packages/merchant-module/src/schemas/merchant.schema.ts

@@ -0,0 +1,143 @@
+import { z } from '@hono/zod-openapi';
+
+export const MerchantSchema = 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: 'merchant001'
+  }),
+  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
+  }),
+  rsaPublicKey: z.string().max(2000, '公钥最多2000个字符').nullable().optional().openapi({
+    description: '公钥',
+    example: '-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...\n-----END PUBLIC KEY-----'
+  }),
+  aesKey: z.string().max(32, 'aes秘钥最多32个字符').nullable().optional().openapi({
+    description: 'aes秘钥',
+    example: 'a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6'
+  }),
+  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 CreateMerchantDto = 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: 'merchant001'
+  }),
+  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
+  }),
+  rsaPublicKey: z.string().max(2000, '公钥最多2000个字符').nullable().optional().openapi({
+    description: '公钥',
+    example: '-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...\n-----END PUBLIC KEY-----'
+  }),
+  aesKey: z.string().max(32, 'aes秘钥最多32个字符').nullable().optional().openapi({
+    description: 'aes秘钥',
+    example: 'a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6'
+  })
+});
+
+export const UpdateMerchantDto = 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: 'merchant001'
+  }),
+  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
+  }),
+  rsaPublicKey: z.string().max(2000, '公钥最多2000个字符').nullable().optional().openapi({
+    description: '公钥',
+    example: '-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...\n-----END PUBLIC KEY-----'
+  }),
+  aesKey: z.string().max(32, 'aes秘钥最多32个字符').nullable().optional().openapi({
+    description: 'aes秘钥',
+    example: 'a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6'
+  })
+});

+ 131 - 0
packages/merchant-module/src/schemas/user-merchant.schema.ts

@@ -0,0 +1,131 @@
+import { z } from '@hono/zod-openapi';
+
+export const UserMerchantSchema = 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: 'merchant001'
+  }),
+  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
+  }),
+  rsaPublicKey: z.string().max(2000, '公钥最多2000个字符').nullable().optional().openapi({
+    description: '公钥',
+    example: '-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...\n-----END PUBLIC KEY-----'
+  }),
+  aesKey: z.string().max(32, 'aes秘钥最多32个字符').nullable().optional().openapi({
+    description: 'aes秘钥',
+    example: 'a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6'
+  }),
+  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 UserCreateMerchantDto = 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: 'merchant001'
+  }),
+  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
+  }),
+  rsaPublicKey: z.string().max(2000, '公钥最多2000个字符').nullable().optional().openapi({
+    description: '公钥',
+    example: '-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...\n-----END PUBLIC KEY-----'
+  }),
+  aesKey: z.string().max(32, 'aes秘钥最多32个字符').nullable().optional().openapi({
+    description: 'aes秘钥',
+    example: 'a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6'
+  })
+});
+
+export const UserUpdateMerchantDto = 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: 'merchant001'
+  }),
+  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
+  }),
+  rsaPublicKey: z.string().max(2000, '公钥最多2000个字符').nullable().optional().openapi({
+    description: '公钥',
+    example: '-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...\n-----END PUBLIC KEY-----'
+  }),
+  aesKey: z.string().max(32, 'aes秘钥最多32个字符').nullable().optional().openapi({
+    description: 'aes秘钥',
+    example: 'a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6'
+  })
+});

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

@@ -0,0 +1 @@
+export { MerchantService } from './merchant.service';

+ 58 - 0
packages/merchant-module/src/services/merchant.service.ts

@@ -0,0 +1,58 @@
+import { GenericCrudService } from '@d8d/shared-crud';
+import { DataSource } from 'typeorm';
+import { Merchant } from '../entities/merchant.entity';
+
+export class MerchantService extends GenericCrudService<Merchant> {
+  constructor(dataSource: DataSource) {
+    super(dataSource, Merchant, {
+      userTracking: {
+        createdByField: 'createdBy',
+        updatedByField: 'updatedBy'
+      }
+    });
+  }
+
+  /**
+   * 更新商户登录统计信息
+   */
+  async updateLoginStats(
+    merchantId: number,
+    loginTime: number,
+    loginIp: string
+  ): Promise<boolean> {
+    const merchant = await this.getById(merchantId);
+    if (!merchant) {
+      return false;
+    }
+
+    // 更新登录统计
+    const updateData = {
+      loginNum: merchant.loginNum + 1,
+      lastLoginTime: merchant.loginTime,
+      lastLoginIp: merchant.loginIp,
+      loginTime,
+      loginIp
+    };
+
+    await this.update(merchantId, updateData);
+    return true;
+  }
+
+  /**
+   * 根据用户名查找商户
+   */
+  async findByUsername(username: string): Promise<Merchant | null> {
+    return this.repository.findOne({
+      where: { username }
+    });
+  }
+
+  /**
+   * 根据状态获取商户列表
+   */
+  async getByState(state: number): Promise<Merchant[]> {
+    return this.repository.find({
+      where: { state }
+    });
+  }
+}

+ 8 - 0
packages/merchant-module/src/types/index.ts

@@ -0,0 +1,8 @@
+export type {
+  MerchantState,
+  MerchantStateType,
+  MerchantLoginStats,
+  MerchantSecurity,
+} from './merchant.types';
+
+export { MERCHANT_STATE } from './merchant.types';

+ 26 - 0
packages/merchant-module/src/types/merchant.types.ts

@@ -0,0 +1,26 @@
+import { z } from 'zod';
+
+export interface MerchantState {
+  ENABLED: 1;
+  DISABLED: 2;
+}
+
+export const MERCHANT_STATE: MerchantState = {
+  ENABLED: 1,
+  DISABLED: 2,
+} as const;
+
+export type MerchantStateType = typeof MERCHANT_STATE[keyof typeof MERCHANT_STATE];
+
+export interface MerchantLoginStats {
+  loginNum: number;
+  loginTime: number;
+  loginIp: string | null;
+  lastLoginTime: number;
+  lastLoginIp: string | null;
+}
+
+export interface MerchantSecurity {
+  rsaPublicKey: string | null;
+  aesKey: string | null;
+}

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