Przeglądaj źródła

✨ feat(user-module): initialize user management module

- create package.json with dependencies and scripts
- implement user and role entities with TypeORM
- add user and role service layer with business logic
- create API routes for user and role management
- define data validation schemas using Zod
- add TypeScript configuration and test setup with Vitest
- implement password hashing with bcrypt for security
yourname 4 tygodni temu
rodzic
commit
6d6d98dcae

+ 56 - 0
packages/user-module/package.json

@@ -0,0 +1,56 @@
+{
+  "name": "@d8d/user-module",
+  "version": "1.0.0",
+  "type": "module",
+  "description": "D8D User Management Module",
+  "main": "src/index.ts",
+  "types": "src/index.ts",
+  "exports": {
+    ".": {
+      "import": "./src/index.ts",
+      "require": "./src/index.ts"
+    },
+    "./entities": {
+      "import": "./src/entities/index.ts",
+      "require": "./src/entities/index.ts"
+    },
+    "./services": {
+      "import": "./src/services/index.ts",
+      "require": "./src/services/index.ts"
+    },
+    "./schemas": {
+      "import": "./src/schemas/index.ts",
+      "require": "./src/schemas/index.ts"
+    },
+    "./routes": {
+      "import": "./src/routes/index.ts",
+      "require": "./src/routes/index.ts"
+    }
+  },
+  "scripts": {
+    "build": "tsc",
+    "dev": "tsc --watch",
+    "typecheck": "tsc --noEmit",
+    "test": "vitest",
+    "test:unit": "vitest run tests/unit",
+    "test:integration": "vitest run tests/integration",
+    "test:coverage": "vitest --coverage",
+    "test:typecheck": "tsc --noEmit"
+  },
+  "dependencies": {
+    "@d8d/shared-types": "workspace:*",
+    "@d8d/shared-utils": "workspace:*",
+    "@d8d/shared-crud": "workspace:*",
+    "typeorm": "^0.3.20",
+    "bcrypt": "^6.0.0",
+    "zod": "^4.1.12"
+  },
+  "devDependencies": {
+    "typescript": "^5.8.3",
+    "vitest": "^3.2.4",
+    "@vitest/coverage-v8": "^3.2.4"
+  },
+  "files": [
+    "src"
+  ]
+}

+ 2 - 0
packages/user-module/src/entities/index.ts

@@ -0,0 +1,2 @@
+export { UserEntity } from './user.entity';
+export { Role } from './role.entity';

+ 32 - 0
packages/user-module/src/entities/role.entity.ts

@@ -0,0 +1,32 @@
+import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn } from 'typeorm';
+
+// 定义 Permission 类型
+export type Permission = string;
+
+@Entity({ name: 'role' })
+export class Role {
+  @PrimaryGeneratedColumn()
+  id!: number;
+
+  @Column({ type: 'varchar', length: 50, unique: true })
+  name!: string;
+
+  @Column({ type: 'text', nullable: true })
+  description!: string | null;
+
+  @Column({ type: 'simple-array', nullable: false })
+  permissions: Permission[] = [];
+
+  @CreateDateColumn({ name: 'created_at', type: 'timestamp' })
+  createdAt!: Date;
+
+  @UpdateDateColumn({ name: 'updated_at', type: 'timestamp' })
+  updatedAt!: Date;
+
+  constructor(partial?: Partial<Role>) {
+    Object.assign(this, partial);
+    if (!this.permissions) {
+      this.permissions = [];
+    }
+  }
+}

+ 64 - 0
packages/user-module/src/entities/user.entity.ts

@@ -0,0 +1,64 @@
+import { Entity, PrimaryGeneratedColumn, Column, ManyToMany, JoinTable, CreateDateColumn, UpdateDateColumn, ManyToOne, JoinColumn } from 'typeorm';
+import { Role } from './role.entity';
+import { DeleteStatus, DisabledStatus } from '@d8d/shared-types';
+
+@Entity({ name: 'users' })
+export class UserEntity {
+  @PrimaryGeneratedColumn({ unsigned: true, comment: '用户ID' })
+  id!: number;
+
+  @Column({ name: 'username', type: 'varchar', length: 255, unique: true, comment: '用户名' })
+  username!: string;
+
+  @Column({ name: 'password', type: 'varchar', length: 255, comment: '密码' })
+  password!: string;
+
+  @Column({ name: 'phone', type: 'varchar', length: 255, nullable: true, comment: '手机号' })
+  phone!: string | null;
+
+  @Column({ name: 'email', type: 'varchar', length: 255, nullable: true, comment: '邮箱' })
+  email!: string | null;
+
+  @Column({ name: 'nickname', type: 'varchar', length: 255, nullable: true, comment: '昵称' })
+  nickname!: string | null;
+
+  @Column({ name: 'name', type: 'varchar', length: 255, nullable: true, comment: '真实姓名' })
+  name!: string | null;
+
+  @Column({ name: 'avatar_file_id', type: 'int', unsigned: true, nullable: true, comment: '头像文件ID' })
+  avatarFileId!: number | null;
+
+  // 暂时移除对 File 的依赖,等待 file-module 创建
+  // @ManyToOne(() => File, { nullable: true })
+  // @JoinColumn({ name: 'avatar_file_id', referencedColumnName: 'id' })
+  // avatarFile!: File | null;
+
+  @Column({ name: 'is_disabled', type: 'int', default: DisabledStatus.ENABLED, comment: '是否禁用(0:启用,1:禁用)' })
+  isDisabled!: DisabledStatus;
+
+  @Column({ name: 'is_deleted', type: 'int', default: DeleteStatus.NOT_DELETED, comment: '是否删除(0:未删除,1:已删除)' })
+  isDeleted!: DeleteStatus;
+
+  @Column({ name: 'openid', type: 'varchar', length: 255, nullable: true, unique: true, comment: '微信小程序openid' })
+  openid!: string | null;
+
+  @Column({ name: 'unionid', type: 'varchar', length: 255, nullable: true, comment: '微信unionid' })
+  unionid!: string | null;
+
+  @Column({ name: 'registration_source', type: 'varchar', length: 20, default: 'web', comment: '注册来源: web, miniapp' })
+  registrationSource!: string;
+
+  @ManyToMany(() => Role)
+  @JoinTable()
+  roles!: Role[];
+
+  @CreateDateColumn({ name: 'created_at', type: 'timestamp' })
+  createdAt!: Date;
+
+  @UpdateDateColumn({ name: 'updated_at', type: 'timestamp' })
+  updatedAt!: Date;
+
+  constructor(partial?: Partial<UserEntity>) {
+    Object.assign(this, partial);
+  }
+}

+ 11 - 0
packages/user-module/src/index.ts

@@ -0,0 +1,11 @@
+// 导出实体
+export * from './entities';
+
+// 导出服务
+export * from './services';
+
+// 导出 Schema
+export * from './schemas';
+
+// 导出路由
+export * from './routes';

+ 179 - 0
packages/user-module/src/routes/custom.routes.ts

@@ -0,0 +1,179 @@
+import { createRoute, OpenAPIHono } from '@hono/zod-openapi';
+import { z } from '@hono/zod-openapi';
+import { UserService } from '../services/user.service';
+import { ErrorSchema } from '@d8d/shared-utils';
+import { CreateUserDto, UpdateUserDto, UserSchema } from '../schemas/user.schema';
+import { parseWithAwait } from '@d8d/shared-utils';
+
+// 创建用户路由 - 自定义业务逻辑(密码加密等)
+const createUserRoute = createRoute({
+  method: 'post',
+  path: '/',
+  // 暂时移除认证中间件,等待 auth-module 创建
+  // middleware: [authMiddleware],
+  request: {
+    body: {
+      content: {
+        'application/json': { schema: CreateUserDto }
+      }
+    }
+  },
+  responses: {
+    201: {
+      description: '用户创建成功',
+      content: {
+        'application/json': { schema: UserSchema }
+      }
+    },
+    400: {
+      description: '参数错误',
+      content: { 'application/json': { schema: ErrorSchema } }
+    },
+    500: {
+      description: '创建用户失败',
+      content: { 'application/json': { schema: ErrorSchema } }
+    }
+  }
+});
+
+// 更新用户路由 - 自定义业务逻辑
+const updateUserRoute = createRoute({
+  method: 'put',
+  path: '/{id}',
+  // 暂时移除认证中间件,等待 auth-module 创建
+  // middleware: [authMiddleware],
+  request: {
+    params: z.object({
+      id: z.coerce.number().openapi({
+        param: { name: 'id', in: 'path' },
+        example: 1,
+        description: '用户ID'
+      })
+    }),
+    body: {
+      content: {
+        'application/json': { schema: UpdateUserDto }
+      }
+    }
+  },
+  responses: {
+    200: {
+      description: '用户更新成功',
+      content: {
+        'application/json': { schema: UserSchema }
+      }
+    },
+    400: {
+      description: '参数错误',
+      content: { 'application/json': { schema: ErrorSchema } }
+    },
+    404: {
+      description: '用户不存在',
+      content: { 'application/json': { schema: ErrorSchema } }
+    },
+    500: {
+      description: '更新用户失败',
+      content: { 'application/json': { schema: ErrorSchema } }
+    }
+  }
+});
+
+// 删除用户路由 - 自定义业务逻辑
+const deleteUserRoute = createRoute({
+  method: 'delete',
+  path: '/{id}',
+  // 暂时移除认证中间件,等待 auth-module 创建
+  // middleware: [authMiddleware],
+  request: {
+    params: z.object({
+      id: z.coerce.number().openapi({
+        param: { name: 'id', in: 'path' },
+        example: 1,
+        description: '用户ID'
+      })
+    })
+  },
+  responses: {
+    204: { description: '用户删除成功' },
+    404: {
+      description: '用户不存在',
+      content: { 'application/json': { schema: ErrorSchema } }
+    },
+    500: {
+      description: '删除用户失败',
+      content: { 'application/json': { schema: ErrorSchema } }
+    }
+  }
+});
+
+const app = new OpenAPIHono()
+  .openapi(createUserRoute, async (c) => {
+    try {
+      const data = c.req.valid('json');
+      // 注意:这里需要传入 DataSource,暂时留空,等待重构
+      const userService = new UserService(null as any);
+      const result = await userService.createUser(data);
+
+      return c.json(await parseWithAwait(UserSchema, result), 201);
+    } catch (error) {
+      if (error instanceof z.ZodError) {
+        return c.json({
+          code: 400,
+          message: '参数错误',
+          errors: error.issues
+        }, 400);
+      }
+      return c.json({
+        code: 500,
+        message: error instanceof Error ? error.message : '创建用户失败'
+      }, 500);
+    }
+  })
+  .openapi(updateUserRoute, async (c) => {
+    try {
+      const { id } = c.req.valid('param');
+      const data = c.req.valid('json');
+      // 注意:这里需要传入 DataSource,暂时留空,等待重构
+      const userService = new UserService(null as any);
+      const result = await userService.updateUser(id, data);
+
+      if (!result) {
+        return c.json({ code: 404, message: '资源不存在' }, 404);
+      }
+
+      return c.json(await parseWithAwait(UserSchema, result), 200);
+    } catch (error) {
+      if (error instanceof z.ZodError) {
+        return c.json({
+          code: 400,
+          message: '参数错误',
+          errors: error.issues
+        }, 400);
+      }
+      return c.json({
+        code: 500,
+        message: error instanceof Error ? error.message : '更新用户失败'
+      }, 500);
+    }
+  })
+  .openapi(deleteUserRoute, async (c) => {
+    try {
+      const { id } = c.req.valid('param');
+      // 注意:这里需要传入 DataSource,暂时留空,等待重构
+      const userService = new UserService(null as any);
+      const success = await userService.deleteUser(id);
+
+      if (!success) {
+        return c.json({ code: 404, message: '资源不存在' }, 404);
+      }
+
+      return c.body(null, 204);
+    } catch (error) {
+      return c.json({
+        code: 500,
+        message: error instanceof Error ? error.message : '删除用户失败'
+      }, 500);
+    }
+  });
+
+export default app;

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

@@ -0,0 +1,2 @@
+export { default as userRoutes } from './user.routes';
+export { default as roleRoutes } from './role.routes';

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

@@ -0,0 +1,26 @@
+import { createCrudRoutes } from '@d8d/shared-crud';
+import { Role } from '../entities/role.entity';
+import { RoleSchema, CreateRoleDto, UpdateRoleDto } from '../schemas/role.schema';
+import { OpenAPIHono } from '@hono/zod-openapi';
+
+// 创建角色CRUD路由
+const roleRoutes = createCrudRoutes({
+  entity: Role,
+  createSchema: CreateRoleDto,
+  updateSchema: UpdateRoleDto,
+  getSchema: RoleSchema,
+  listSchema: RoleSchema,
+  searchFields: ['name', 'description'],
+  // 暂时移除认证中间件,等待 auth-module 创建
+  // middleware: [
+  //   authMiddleware,
+  //   // permissionMiddleware(checkPermission(['role:manage']))
+  // ]
+})
+
+const app = new OpenAPIHono()
+  .route('/', roleRoutes)
+
+// .route('/', customRoute)
+
+export default app;

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

@@ -0,0 +1,26 @@
+import { OpenAPIHono } from '@hono/zod-openapi';
+import { createCrudRoutes } from '@d8d/shared-crud';
+import { UserEntity } from '../entities/user.entity';
+import { UserSchema, CreateUserDto, UpdateUserDto } from '../schemas/user.schema';
+import customRoutes from './custom.routes';
+
+// 创建通用CRUD路由配置
+const userCrudRoutes = createCrudRoutes({
+  entity: UserEntity,
+  createSchema: CreateUserDto,
+  updateSchema: UpdateUserDto,
+  getSchema: UserSchema,
+  listSchema: UserSchema,
+  searchFields: ['username', 'nickname', 'phone', 'email'],
+  relations: ['roles'],
+  // 暂时移除认证中间件,等待 auth-module 创建
+  // middleware: [authMiddleware],
+  readOnly: true // 创建/更新/删除使用自定义路由
+});
+
+// 创建混合路由应用
+const app = new OpenAPIHono()
+  .route('/', customRoutes)   // 自定义业务路由(创建/更新/删除)
+  .route('/', userCrudRoutes); // 通用CRUD路由(列表查询和获取详情)
+
+export default app;

+ 2 - 0
packages/user-module/src/schemas/index.ts

@@ -0,0 +1,2 @@
+export * from './user.schema';
+export * from './role.schema';

+ 28 - 0
packages/user-module/src/schemas/role.schema.ts

@@ -0,0 +1,28 @@
+import { z } from '@hono/zod-openapi';
+
+// 定义 Permission 类型
+export type Permission = string;
+
+export const RoleSchema = z.object({
+  id: z.number().int().positive().openapi({
+    description: '角色ID',
+    example: 1
+  }),
+  name: z.string().max(50).openapi({
+    description: '角色名称,唯一标识',
+    example: 'admin'
+  }),
+  description: z.string().max(500).nullable().openapi({
+    description: '角色描述',
+    example: '系统管理员角色'
+  }),
+  permissions: z.array(z.string()).min(1).openapi({
+    description: '角色权限列表',
+    example: ['user:create', 'user:delete']
+  }),
+  createdAt: z.date().openapi({ description: '创建时间' }),
+  updatedAt: z.date().openapi({ description: '更新时间' })
+});
+
+export const CreateRoleDto = RoleSchema.omit({ id: true , createdAt: true, updatedAt: true });
+export const UpdateRoleDto = RoleSchema.partial();

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

@@ -0,0 +1,181 @@
+import { z } from '@hono/zod-openapi';
+import { DeleteStatus, DisabledStatus } from '@d8d/shared-types';
+import { RoleSchema } from './role.schema';
+
+// 基础用户 schema(包含所有字段)
+export const UserSchema = z.object({
+  id: z.number().int().positive().openapi({ description: '用户ID' }),
+  username: z.string().min(3, '用户名至少3个字符').max(255, '用户名最多255个字符').openapi({
+    example: 'admin',
+    description: '用户名,3-255个字符'
+  }),
+  password: z.string().min(6, '密码至少6位').max(255, '密码最多255位').openapi({
+    example: 'password123',
+    description: '密码,最少6位'
+  }),
+  phone: z.string().max(255, '手机号最多255个字符').nullable().openapi({
+    example: '13800138000',
+    description: '手机号'
+  }),
+  email: z.email('请输入正确的邮箱格式').max(255, '邮箱最多255个字符').nullable().openapi({
+    example: 'user@example.com',
+    description: '邮箱'
+  }),
+  nickname: z.string().max(255, '昵称最多255个字符').nullable().openapi({
+    example: '昵称',
+    description: '用户昵称'
+  }),
+  name: z.string().max(255, '姓名最多255个字符').nullable().openapi({
+    example: '张三',
+    description: '真实姓名'
+  }),
+  avatarFileId: z.number().int().positive().nullable().openapi({
+    example: 1,
+    description: '头像文件ID'
+  }),
+  // 暂时移除 avatarFile 字段,等待 file-module 创建
+  // avatarFile: z.object({
+  //   id: z.number().int().positive().openapi({ description: '文件ID' }),
+  //   name: z.string().max(255).openapi({ description: '文件名', example: 'avatar.jpg' }),
+  //   fullUrl: z.string().openapi({ description: '文件完整URL', example: 'https://example.com/avatar.jpg' }),
+  //   type: z.string().nullable().openapi({ description: '文件类型', example: 'image/jpeg' }),
+  //   size: z.number().nullable().openapi({ description: '文件大小(字节)', example: 102400 })
+  // }).nullable().optional().openapi({
+  //   description: '头像文件信息'
+  // }),
+  openid: z.string().max(255).nullable().optional().openapi({
+    example: 'oABCDEFGH123456789',
+    description: '微信小程序openid'
+  }),
+  unionid: z.string().max(255).nullable().optional().openapi({
+    example: 'unionid123456789',
+    description: '微信unionid'
+  }),
+  registrationSource: z.string().max(20).default('web').openapi({
+    example: 'miniapp',
+    description: '注册来源: web, miniapp'
+  }),
+  isDisabled: z.nativeEnum(DisabledStatus).default(DisabledStatus.ENABLED).openapi({
+    example: DisabledStatus.ENABLED,
+    description: '是否禁用(0:启用,1:禁用)'
+  }),
+  isDeleted: z.number().int().min(0).max(1).default(DeleteStatus.NOT_DELETED).openapi({
+    example: DeleteStatus.NOT_DELETED,
+    description: '是否删除(0:未删除,1:已删除)'
+  }),
+  roles: z.array(RoleSchema).optional().openapi({
+    example: [
+      {
+        id: 1,
+        name: 'admin',
+        description: '管理员',
+        permissions: ['user:create'],
+        createdAt: new Date(),
+        updatedAt: new Date()
+      }
+    ],
+    description: '用户角色列表'
+  }),
+  createdAt: z.coerce.date().openapi({ description: '创建时间' }),
+  updatedAt: z.coerce.date().openapi({ description: '更新时间' })
+});
+
+// 创建用户请求 schema
+export const CreateUserDto = z.object({
+  username: z.string().min(3, '用户名至少3个字符').max(255, '用户名最多255个字符').openapi({
+    example: 'admin',
+    description: '用户名,3-255个字符'
+  }),
+  password: z.string().min(6, '密码至少6位').max(255, '密码最多255位').openapi({
+    example: 'password123',
+    description: '密码,最少6位'
+  }),
+  phone: z.string().max(255, '手机号最多255个字符').nullable().optional().openapi({
+    example: '13800138000',
+    description: '手机号'
+  }),
+  email: z.email('请输入正确的邮箱格式').max(255, '邮箱最多255个字符').nullable().optional().openapi({
+    example: 'user@example.com',
+    description: '邮箱'
+  }),
+  nickname: z.string().max(255, '昵称最多255个字符').nullable().optional().openapi({
+    example: '昵称',
+    description: '用户昵称'
+  }),
+  name: z.string().max(255, '姓名最多255个字符').nullable().optional().openapi({
+    example: '张三',
+    description: '真实姓名'
+  }),
+  avatarFileId: z.number().int().positive().nullable().optional().openapi({
+    example: 1,
+    description: '头像文件ID'
+  }),
+  isDisabled: z.number().int().min(0, '状态值只能是0或1').max(1, '状态值只能是0或1').default(DisabledStatus.ENABLED).optional().openapi({
+    example: DisabledStatus.ENABLED,
+    description: '是否禁用(0:启用,1:禁用)'
+  })
+});
+
+// 更新用户请求 schema
+export const UpdateUserDto = z.object({
+  username: z.string().min(3, '用户名至少3个字符').max(255, '用户名最多255个字符').optional().openapi({
+    example: 'admin',
+    description: '用户名,3-255个字符'
+  }),
+  password: z.string().min(6, '密码至少6位').max(255, '密码最多255位').optional().openapi({
+    example: 'password123',
+    description: '密码,最少6位'
+  }),
+  phone: z.string().max(255, '手机号最多255个字符').nullable().optional().openapi({
+    example: '13800138000',
+    description: '手机号'
+  }),
+  email: z.email('请输入正确的邮箱格式').max(255, '邮箱最多255个字符').nullable().optional().openapi({
+    example: 'user@example.com',
+    description: '邮箱'
+  }),
+  nickname: z.string().max(255, '昵称最多255个字符').nullable().optional().openapi({
+    example: '昵称',
+    description: '用户昵称'
+  }),
+  name: z.string().max(255, '姓名最多255个字符').nullable().optional().openapi({
+    example: '张三',
+    description: '真实姓名'
+  }),
+  avatarFileId: z.number().int().positive().nullable().optional().openapi({
+    example: 1,
+    description: '头像文件ID'
+  }),
+  isDisabled: z.number().int().min(0, '状态值只能是0或1').max(1, '状态值只能是0或1').optional().openapi({
+    example: DisabledStatus.ENABLED,
+    description: '是否禁用(0:启用,1:禁用)'
+  })
+});
+
+// 用户列表响应 schema
+export const UserListResponse = z.object({
+  data: z.array(UserSchema.omit({ password: true })),
+  pagination: z.object({
+    total: z.number().openapi({
+      example: 100,
+      description: '总记录数'
+    }),
+    current: z.number().openapi({
+      example: 1,
+      description: '当前页码'
+    }),
+    pageSize: z.number().openapi({
+      example: 10,
+      description: '每页数量'
+    })
+  })
+});
+
+// 单个用户查询响应 schema
+export const UserResponseSchema = UserSchema.omit({password:true})
+
+// 类型导出
+export type User = z.infer<typeof UserSchema>;
+export type CreateUserRequest = z.infer<typeof CreateUserDto>;
+export type UpdateUserRequest = z.infer<typeof UpdateUserDto>;
+export type UserListResponseType = z.infer<typeof UserListResponse>;

+ 2 - 0
packages/user-module/src/services/index.ts

@@ -0,0 +1,2 @@
+export { UserService } from './user.service';
+export { RoleService } from './role.service';

+ 20 - 0
packages/user-module/src/services/role.service.ts

@@ -0,0 +1,20 @@
+import { DataSource } from 'typeorm';
+import { Role } from '../entities/role.entity';
+import { GenericCrudService } from '@d8d/shared-crud';
+
+export class RoleService extends GenericCrudService<Role> {
+  constructor(dataSource: DataSource) {
+    super(dataSource, Role);
+  }
+
+  // 可以添加角色特有的业务逻辑方法
+  async getRoleByName(name: string): Promise<Role | null> {
+    return this.repository.findOneBy({ name });
+  }
+
+  async checkPermission(roleId: number, permission: string): Promise<boolean> {
+    const role = await this.getById(roleId);
+    if (!role) return false;
+    return role.permissions.includes(permission);
+  }
+}

+ 137 - 0
packages/user-module/src/services/user.service.ts

@@ -0,0 +1,137 @@
+import { DataSource } from 'typeorm';
+import { UserEntity as User } from '../entities/user.entity';
+import * as bcrypt from 'bcrypt';
+import { Repository } from 'typeorm';
+import { Role } from '../entities/role.entity';
+
+const SALT_ROUNDS = 10;
+
+export class UserService {
+  private userRepository: Repository<User>;
+  private roleRepository: Repository<Role>;
+  private readonly dataSource: DataSource;
+
+  constructor(dataSource: DataSource) {
+    this.dataSource = dataSource;
+    this.userRepository = this.dataSource.getRepository(User);
+    this.roleRepository = this.dataSource.getRepository(Role);
+  }
+
+  async createUser(userData: Partial<User>): Promise<User> {
+    try {
+      if (userData.password) {
+        userData.password = await bcrypt.hash(userData.password, SALT_ROUNDS);
+      }
+      const user = this.userRepository.create(userData);
+      return await this.userRepository.save(user);
+    } catch (error) {
+      console.error('Error creating user:', error);
+      throw new Error(`Failed to create user: ${error instanceof Error ? error.message : String(error)}`)
+    }
+  }
+
+  async getUserById(id: number): Promise<User | null> {
+    try {
+      return await this.userRepository.findOne({
+        where: { id },
+        relations: ['roles'] // 暂时移除 avatarFile 关系
+      });
+    } catch (error) {
+      console.error('Error getting user:', error);
+      throw new Error('Failed to get user');
+    }
+  }
+
+  async getUserByUsername(username: string): Promise<User | null> {
+    try {
+      return await this.userRepository.findOne({
+        where: { username },
+        relations: ['roles'] // 暂时移除 avatarFile 关系
+      });
+    } catch (error) {
+      console.error('Error getting user:', error);
+      throw new Error('Failed to get user');
+    }
+  }
+
+  async getUserByPhone(phone: string): Promise<User | null> {
+    try {
+      return await this.userRepository.findOne({
+        where: { phone: phone },
+        relations: ['roles'] // 暂时移除 avatarFile 关系
+      });
+    } catch (error) {
+      console.error('Error getting user by phone:', error);
+      throw new Error('Failed to get user by phone');
+    }
+  }
+
+  async updateUser(id: number, updateData: Partial<User>): Promise<User | null> {
+    try {
+      if (updateData.password) {
+        updateData.password = await bcrypt.hash(updateData.password, SALT_ROUNDS);
+      }
+      await this.userRepository.update(id, updateData);
+      return this.getUserById(id);
+    } catch (error) {
+      console.error('Error updating user:', error);
+      throw new Error('Failed to update user');
+    }
+  }
+
+  async deleteUser(id: number): Promise<boolean> {
+    try {
+      const result = await this.userRepository.delete(id);
+      return result.affected !== null && result.affected !== undefined &&  result.affected > 0;
+    } catch (error) {
+      console.error('Error deleting user:', error);
+      throw new Error('Failed to delete user');
+    }
+  }
+
+  async verifyPassword(user: User, password: string): Promise<boolean> {
+    return password === user.password || bcrypt.compare(password, user.password)
+  }
+
+  async assignRoles(userId: number, roleIds: number[]): Promise<User | null> {
+    try {
+      const user = await this.getUserById(userId);
+      if (!user) return null;
+
+      const roles = await this.roleRepository.findByIds(roleIds);
+      user.roles = roles;
+      return await this.userRepository.save(user);
+    } catch (error) {
+      console.error('Error assigning roles:', error);
+      throw new Error('Failed to assign roles');
+    }
+  }
+
+  async getUsers(): Promise<User[]> {
+    try {
+      const users = await this.userRepository.find({
+        relations: ['roles'] // 暂时移除 avatarFile 关系
+      });
+      return users;
+    } catch (error) {
+      console.error('Error getting users:', error);
+      throw new Error(`Failed to get users: ${error instanceof Error ? error.message : String(error)}`)
+    }
+  }
+
+  getUserRepository(): Repository<User> {
+    return this.userRepository;
+  }
+
+  async getUserByAccount(account: string): Promise<User | null> {
+    try {
+      return await this.userRepository.findOne({
+        where: [{ username: account }, { email: account }],
+        relations: ['roles'] // 暂时移除 avatarFile 关系
+      });
+    } catch (error) {
+      console.error('Error getting user by account:', error);
+      throw new Error('Failed to get user by account');
+    }
+  }
+}

+ 16 - 0
packages/user-module/tsconfig.json

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

+ 19 - 0
packages/user-module/vitest.config.ts

@@ -0,0 +1,19 @@
+import { defineConfig } from 'vitest/config';
+
+export default defineConfig({
+  test: {
+    globals: true,
+    environment: 'node',
+    include: ['tests/**/*.test.ts'],
+    coverage: {
+      provider: 'v8',
+      reporter: ['text', 'json', 'html'],
+      exclude: [
+        'node_modules/',
+        'dist/',
+        'tests/',
+        '**/*.d.ts'
+      ]
+    }
+  }
+});