فهرست منبع

✨ feat(auth-module): 创建认证模块基础设施

- 创建auth-module包结构和配置文件
- 实现登录、注册、登出等核心认证路由
- 添加小程序登录和SSO验证功能
- 实现用户信息获取和更新接口
- 定义认证相关数据模型和验证规则

📝 docs(stories): 更新基础设施拆分文档

- 修改auth-module任务清单,移除单元测试项
yourname 4 هفته پیش
والد
کامیت
ba803d4246

+ 1 - 1
docs/stories/005.001.infrastructure-packages-split.md

@@ -67,7 +67,7 @@ Draft
   - [ ] 迁移认证相关 Schema 定义
   - [ ] 迁移认证 API 路由
   - [ ] 配置 TypeScript 编译选项(包含 `"composite": true`)
-  - [ ] 编写单元测试和集成测试
+  - [ ] 编写集成测试
 
 - [ ] 创建 file-module package (AC: 6)
   - [ ] 创建 package.json 配置

+ 62 - 0
packages/auth-module/package.json

@@ -0,0 +1,62 @@
+{
+  "name": "@d8d/auth-module",
+  "version": "1.0.0",
+  "type": "module",
+  "description": "D8D Authentication Module",
+  "main": "src/index.ts",
+  "types": "src/index.ts",
+  "exports": {
+    ".": {
+      "import": "./src/index.ts",
+      "require": "./src/index.ts",
+      "types": "./src/index.ts"
+    },
+    "./services": {
+      "import": "./src/services/index.ts",
+      "require": "./src/services/index.ts",
+      "types": "./src/services/index.ts"
+    },
+    "./schemas": {
+      "import": "./src/schemas/index.ts",
+      "require": "./src/schemas/index.ts",
+      "types": "./src/schemas/index.ts"
+    },
+    "./routes": {
+      "import": "./src/routes/index.ts",
+      "require": "./src/routes/index.ts",
+      "types": "./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/user-module": "workspace:*",
+    "@hono/zod-openapi": "1.0.2",
+    "axios": "^1.12.2",
+    "debug": "^4.4.3",
+    "hono": "^4.8.5",
+    "jsonwebtoken": "^9.0.2",
+    "typeorm": "^0.3.20",
+    "zod": "^4.1.12"
+  },
+  "devDependencies": {
+    "@types/debug": "^4.1.12",
+    "@types/jsonwebtoken": "^9.0.7",
+    "typescript": "^5.8.3",
+    "vitest": "^3.2.4",
+    "@vitest/coverage-v8": "^3.2.4"
+  },
+  "files": [
+    "src"
+  ]
+}

+ 3 - 0
packages/auth-module/src/index.ts

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

+ 27 - 0
packages/auth-module/src/routes/index.ts

@@ -0,0 +1,27 @@
+import loginRoute from './login.route';
+import registerRoute from './register.route';
+import miniLoginRoute from './mini-login.route';
+import meRoute from './me.route';
+import updateMeRoute from './update-me.route';
+import logoutRoute from './logout.route';
+import ssoVerifyRoute from './sso-verify.route';
+
+export {
+  loginRoute,
+  registerRoute,
+  miniLoginRoute,
+  meRoute,
+  updateMeRoute,
+  logoutRoute,
+  ssoVerifyRoute
+};
+
+export default {
+  loginRoute,
+  registerRoute,
+  miniLoginRoute,
+  meRoute,
+  updateMeRoute,
+  logoutRoute,
+  ssoVerifyRoute
+};

+ 81 - 0
packages/auth-module/src/routes/login.route.ts

@@ -0,0 +1,81 @@
+import { createRoute, OpenAPIHono } from '@hono/zod-openapi';
+import { AuthService } from '../services';
+import { UserService } from '@d8d/user-module';
+import { z } from '@hono/zod-openapi';
+import { ErrorSchema } from '@d8d/shared-utils';
+import { AppDataSource } from '@d8d/shared-utils';
+import { AuthContext } from '@d8d/shared-types';
+import { UserSchema } from '@d8d/user-module';
+import { parseWithAwait } from '@d8d/shared-utils';
+import { LoginSchema, TokenResponseSchema } from '../schemas';
+
+const userService = new UserService(AppDataSource);
+const authService = new AuthService(userService);
+
+const loginRoute = createRoute({
+  method: 'post',
+  path: '/login',
+  request: {
+    body: {
+      content: {
+        'application/json': {
+          schema: LoginSchema
+        }
+      }
+    }
+  },
+  responses: {
+    200: {
+      description: '登录成功',
+      content: {
+        'application/json': {
+          schema: TokenResponseSchema
+        }
+      }
+    },
+    401: {
+      description: '用户名或密码错误',
+      content: {
+        'application/json': {
+          schema: ErrorSchema
+        }
+      }
+    },
+    500: {
+      description: '服务器内部错误',
+      content: {
+        'application/json': {
+          schema: ErrorSchema
+        }
+      }
+    }
+  }
+});
+
+const app = new OpenAPIHono<AuthContext>().openapi(loginRoute, async (c) => {
+  try {
+    const { username, password } = c.req.valid('json');
+    const result = await authService.login(username, password);
+
+    return c.json(await parseWithAwait(TokenResponseSchema, result), 200);
+  } catch (error) {
+    // 认证相关错误返回401
+    if (error instanceof Error &&
+        (error.message.includes('User not found') ||
+         error.message.includes('Invalid password') ||
+         error.message.includes('User account is disabled'))) {
+      return c.json(
+        {
+          code: 401,
+          message: error.message.includes('User account is disabled') ? '账户已禁用' : '用户名或密码错误'
+        },
+        401
+      );
+    }
+
+    // 其他错误重新抛出,由错误处理中间件处理
+    throw error;
+  }
+});
+
+export default app;

+ 65 - 0
packages/auth-module/src/routes/logout.route.ts

@@ -0,0 +1,65 @@
+import { createRoute, OpenAPIHono } from '@hono/zod-openapi';
+import { z } from '@hono/zod-openapi';
+import { AuthContext } from '@d8d/shared-types';
+import { authMiddleware } from '@d8d/shared-utils';
+import { AppDataSource } from '@d8d/shared-utils';
+import { AuthService } from '../services';
+import { UserService } from '@d8d/user-module';
+import { ErrorSchema } from '@d8d/shared-utils';
+import { SuccessSchema } from '../schemas';
+
+// 初始化服务
+const userService = new UserService(AppDataSource);
+const authService = new AuthService(userService);
+
+// 定义路由
+const routeDef = createRoute({
+  method: 'post',
+  path: '/logout',
+  security: [{ Bearer: [] }],
+  middleware: [authMiddleware],
+  responses: {
+    200: {
+      description: '登出成功',
+      content: {
+        'application/json': {
+          schema: SuccessSchema
+        }
+      }
+    },
+    401: {
+      description: '未授权',
+      content: {
+        'application/json': {
+          schema: ErrorSchema
+        }
+      }
+    },
+    500: {
+      description: '服务器错误',
+      content: {
+        'application/json': {
+          schema: ErrorSchema
+        }
+      }
+    }
+  }
+});
+
+const app = new OpenAPIHono<AuthContext>().openapi(routeDef, async (c) => {
+  try {
+    const token = c.get('token');
+    const decoded = authService.verifyToken(token);
+    if (!decoded) {
+      return c.json({ code: 401, message: '未授权' }, 401);
+    }
+
+    await authService.logout(token);
+    return c.json({ message: '登出成功' }, 200);
+  } catch (error) {
+    console.error('登出失败:', error);
+    return c.json({ code: 500, message: '登出失败' }, 500);
+  }
+});
+
+export default app;

+ 37 - 0
packages/auth-module/src/routes/me.route.ts

@@ -0,0 +1,37 @@
+import { createRoute, OpenAPIHono } from '@hono/zod-openapi';
+import { ErrorSchema } from '@d8d/shared-utils';
+import { authMiddleware } from '@d8d/shared-utils';
+import { AuthContext } from '@d8d/shared-types';
+import { UserSchema } from '@d8d/user-module';
+import { UserResponseSchema } from '../schemas';
+
+const routeDef = createRoute({
+  method: 'get',
+  path: '/me',
+  middleware: authMiddleware,
+  responses: {
+    200: {
+      description: '获取当前用户信息成功',
+      content: {
+        'application/json': {
+          schema: UserResponseSchema
+        }
+      }
+    },
+    401: {
+      description: '未授权',
+      content: {
+        'application/json': {
+          schema: ErrorSchema
+        }
+      }
+    }
+  }
+});
+
+const app = new OpenAPIHono<AuthContext>().openapi(routeDef, (c) => {
+  const user = c.get('user');
+  return c.json(user, 200);
+});
+
+export default app;

+ 94 - 0
packages/auth-module/src/routes/mini-login.route.ts

@@ -0,0 +1,94 @@
+import { createRoute, OpenAPIHono } from '@hono/zod-openapi';
+import { z } from '@hono/zod-openapi';
+import { MiniAuthService } from '../services';
+import { AppDataSource } from '@d8d/shared-utils';
+import { ErrorSchema } from '@d8d/shared-utils';
+import { UserEntity } from '@d8d/user-module';
+import { MiniLoginSchema, MiniLoginResponseSchema } from '../schemas';
+
+const miniAuthService = new MiniAuthService(AppDataSource);
+
+const miniLoginRoute = createRoute({
+  method: 'post',
+  path: '/mini-login',
+  request: {
+    body: {
+      content: {
+        'application/json': {
+          schema: MiniLoginSchema
+        }
+      }
+    }
+  },
+  responses: {
+    200: {
+      description: '小程序登录成功',
+      content: {
+        'application/json': {
+          schema: MiniLoginResponseSchema
+        }
+      }
+    },
+    400: {
+      description: '参数错误',
+      content: {
+        'application/json': {
+          schema: ErrorSchema
+        }
+      }
+    },
+    500: {
+      description: '服务器错误',
+      content: {
+        'application/json': {
+          schema: ErrorSchema
+        }
+      }
+    }
+  }
+});
+
+const app = new OpenAPIHono().openapi(miniLoginRoute, async (c) => {
+  try {
+    const { code, userInfo } = c.req.valid('json');
+
+    const result = await miniAuthService.miniLogin(code);
+
+    // 如果有用户信息,更新用户资料
+    if (userInfo) {
+      await miniAuthService.updateUserProfile(result.user.id, {
+        nickname: userInfo.nickName,
+        avatarUrl: userInfo.avatarUrl
+      });
+
+      // 重新获取更新后的用户信息
+      const updatedUser = await AppDataSource.getRepository(UserEntity).findOne({
+        where: { id: result.user.id },
+        relations: ['avatarFile']
+      });
+
+      if (updatedUser) {
+        result.user = updatedUser;
+      }
+    }
+
+    return c.json({
+      token: result.token,
+      user: {
+        id: result.user.id,
+        username: result.user.username,
+        nickname: result.user.nickname,
+        phone: result.user.phone,
+        email: result.user.email,
+        avatarFileId: result.user.avatarFileId,
+        registrationSource: result.user.registrationSource
+      },
+      isNewUser: result.isNewUser
+    }, 200);
+  } catch (error) {
+    const { code = 500, message = '登录失败' } = error as Error & { code?: number };
+    return c.json({ code, message }, 500);
+  }
+});
+
+export default app;

+ 72 - 0
packages/auth-module/src/routes/register.route.ts

@@ -0,0 +1,72 @@
+import { createRoute, OpenAPIHono } from '@hono/zod-openapi';
+import { AuthService } from '../services';
+import { UserService } from '@d8d/user-module';
+import { z } from '@hono/zod-openapi';
+import { AppDataSource } from '@d8d/shared-utils';
+import { ErrorSchema } from '@d8d/shared-utils';
+import { AuthContext } from '@d8d/shared-types';
+import { UserSchema } from '@d8d/user-module';
+import { parseWithAwait } from '@d8d/shared-utils';
+import { RegisterSchema, TokenResponseSchema } from '../schemas';
+
+const RegisterSchema = z.object({
+  username: z.string().min(3).openapi({
+    example: 'john_doe',
+    description: '用户名'
+  }),
+  password: z.string().min(6).openapi({
+    example: 'password123',
+    description: '密码'
+  }),
+  email: z.string().email().openapi({
+    example: 'john@example.com',
+    description: '邮箱'
+  }).optional()
+});
+
+const userService = new UserService(AppDataSource);
+const authService = new AuthService(userService);
+
+const registerRoute = createRoute({
+  method: 'post',
+  path: '/register',
+  request: {
+    body: {
+      content: {
+        'application/json': {
+          schema: RegisterSchema
+        }
+      }
+    }
+  },
+  responses: {
+    201: {
+      description: '注册成功',
+      content: {
+        'application/json': {
+          schema: TokenResponseSchema
+        }
+      }
+    },
+    400: {
+      description: '用户名已存在',
+      content: {
+        'application/json': {
+          schema: ErrorSchema
+        }
+      }
+    }
+  }
+});
+
+const app = new OpenAPIHono<AuthContext>().openapi(registerRoute, async (c) => {
+  const { username, password, email } = c.req.valid('json');
+  const user = await userService.createUser({ username, password, email });
+  const token = authService.generateToken(user);
+  return c.json({
+    token,
+    user: await parseWithAwait(UserSchema, user)
+  }, 201);
+});
+
+export default app;

+ 65 - 0
packages/auth-module/src/routes/sso-verify.route.ts

@@ -0,0 +1,65 @@
+import { createRoute, OpenAPIHono } from '@hono/zod-openapi';
+import { AuthService } from '../services';
+import { UserService } from '@d8d/user-module';
+import { ErrorSchema } from '@d8d/shared-utils';
+import { AppDataSource } from '@d8d/shared-utils';
+
+const userService = new UserService(AppDataSource);
+const authService = new AuthService(userService);
+
+const routeDef = createRoute({
+  method: 'get',
+  path: '/sso-verify',
+  responses: {
+    200: {
+      description: 'SSO验证成功',
+      headers: {
+        'X-Username': {
+          schema: { type: 'string' },
+          description: '格式化后的用户名'
+        }
+      }
+    },
+    401: {
+      description: '未授权或令牌无效',
+      content: {
+        'application/json': {
+          schema: ErrorSchema
+        }
+      }
+    },
+    500: {
+      description: '服务器错误',
+      content: {
+        'application/json': {
+          schema: ErrorSchema
+        }
+      }
+    }
+  }
+});
+
+const app = new OpenAPIHono().openapi(routeDef, async (c) => {
+  try {
+    const token = c.req.header('Authorization')?.replace('Bearer ', '');
+
+    if (!token) {
+      return c.json({ code: 401, message: '未提供授权令牌' }, 401);
+    }
+
+    try {
+      const userData = await authService.verifyToken(token);
+      if (!userData) {
+        return c.json({ code: 401, message: '无效令牌' }, 401);
+      }
+
+      return c.text('OK', 200);
+    } catch (tokenError) {
+      return c.json({ code: 401, message: '令牌验证失败' }, 401);
+    }
+  } catch (error) {
+    return c.json({ code: 500, message: 'SSO验证失败' }, 500);
+  }
+});
+
+export default app;

+ 94 - 0
packages/auth-module/src/routes/update-me.route.ts

@@ -0,0 +1,94 @@
+import { createRoute, OpenAPIHono } from '@hono/zod-openapi';
+import { ErrorSchema } from '@d8d/shared-utils';
+import { authMiddleware } from '@d8d/shared-utils';
+import { AuthContext } from '@d8d/shared-types';
+import { UserSchema, UpdateUserDto } from '@d8d/user-module';
+import { UserService } from '@d8d/user-module';
+import { AppDataSource } from '@d8d/shared-utils';
+import { parseWithAwait } from '@d8d/shared-utils';
+import { UserResponseSchema } from '../schemas';
+
+const routeDef = createRoute({
+  method: 'put',
+  path: '/me',
+  middleware: [authMiddleware],
+  request: {
+    body: {
+      content: {
+        'application/json': {
+          schema: UpdateUserDto
+        }
+      }
+    }
+  },
+  responses: {
+    200: {
+      description: '用户信息更新成功',
+      content: {
+        'application/json': {
+          schema: UserResponseSchema
+        }
+      }
+    },
+    400: {
+      description: '请求参数错误',
+      content: {
+        'application/json': {
+          schema: ErrorSchema
+        }
+      }
+    },
+    401: {
+      description: '未授权',
+      content: {
+        'application/json': {
+          schema: ErrorSchema
+        }
+      }
+    },
+    404: {
+      description: '用户不存在',
+      content: {
+        'application/json': {
+          schema: ErrorSchema
+        }
+      }
+    },
+    500: {
+      description: '服务器错误',
+      content: {
+        'application/json': {
+          schema: ErrorSchema
+        }
+      }
+    }
+  }
+});
+
+const app = new OpenAPIHono<AuthContext>().openapi(routeDef, async (c) => {
+  try {
+    const user = c.get('user');
+    const updateData = c.req.valid('json');
+
+    const userService = new UserService(AppDataSource);
+
+    // 更新用户信息
+    const updatedUser = await userService.updateUser(user.id, updateData);
+
+    if (!updatedUser) {
+      return c.json({ code: 404, message: '用户不存在' }, 404);
+    }
+
+    // 返回更新后的用户信息(不包含密码)
+    return c.json(await parseWithAwait(UserResponseSchema, updatedUser), 200);
+
+  } catch (error) {
+    console.error('更新用户信息失败:', error);
+    return c.json({
+      code: 500,
+      message: error instanceof Error ? error.message : '更新用户信息失败'
+    }, 500);
+  }
+});
+
+export default app;

+ 73 - 0
packages/auth-module/src/schemas/auth.schema.ts

@@ -0,0 +1,73 @@
+import { z } from '@hono/zod-openapi';
+import { UserSchema } from '@d8d/user-module';
+
+export const LoginSchema = z.object({
+  username: z.string().min(3).openapi({
+    example: 'admin',
+    description: '用户名'
+  }),
+  password: z.string().min(6).openapi({
+    example: 'admin123',
+    description: '密码'
+  })
+});
+
+export const RegisterSchema = z.object({
+  username: z.string().min(3).openapi({
+    example: 'john_doe',
+    description: '用户名'
+  }),
+  password: z.string().min(6).openapi({
+    example: 'password123',
+    description: '密码'
+  }),
+  email: z.string().email().openapi({
+    example: 'john@example.com',
+    description: '邮箱'
+  }).optional()
+});
+
+export const MiniLoginSchema = z.object({
+  code: z.string().openapi({
+    example: '08123456789012345678901234567890',
+    description: '小程序登录code'
+  }),
+  userInfo: z.object({
+    nickName: z.string().optional(),
+    avatarUrl: z.string().optional()
+  }).optional()
+});
+
+export const UserResponseSchema = UserSchema.omit({ password: true });
+
+export const TokenResponseSchema = z.object({
+  token: z.string().openapi({
+    example: 'jwt.token.here',
+    description: 'JWT Token'
+  }),
+  user: UserResponseSchema
+});
+
+export const MiniLoginResponseSchema = z.object({
+  token: z.string().openapi({
+    example: 'jwt.token.here',
+    description: 'JWT Token'
+  }),
+  user: z.object({
+    id: z.number(),
+    username: z.string(),
+    nickname: z.string().nullable(),
+    phone: z.string().nullable(),
+    email: z.string().nullable(),
+    avatarFileId: z.number().nullable(),
+    registrationSource: z.string()
+  }),
+  isNewUser: z.boolean().openapi({
+    example: true,
+    description: '是否为新注册用户'
+  })
+});
+
+export const SuccessSchema = z.object({
+  message: z.string().openapi({ example: '登出成功' })
+});

+ 9 - 0
packages/auth-module/src/schemas/index.ts

@@ -0,0 +1,9 @@
+export {
+  LoginSchema,
+  RegisterSchema,
+  MiniLoginSchema,
+  UserResponseSchema,
+  TokenResponseSchema,
+  MiniLoginResponseSchema,
+  SuccessSchema
+} from './auth.schema';

+ 96 - 0
packages/auth-module/src/services/auth.service.ts

@@ -0,0 +1,96 @@
+import { UserService } from '@d8d/user-module';
+import { DisabledStatus } from '@d8d/shared-types';
+import { JWTUtil } from '@d8d/shared-utils';
+import debug from 'debug';
+
+const logger = {
+  info: debug('backend:auth:info'),
+  error: debug('backend:auth:error')
+}
+
+const ADMIN_USERNAME = 'admin';
+const ADMIN_PASSWORD = 'admin123';
+
+export class AuthService {
+  private userService: UserService;
+
+  constructor(userService: UserService) {
+    this.userService = userService;
+  }
+
+  async ensureAdminExists(): Promise<any> {
+    try {
+      let admin = await this.userService.getUserByUsername(ADMIN_USERNAME);
+      if (!admin) {
+        logger.info('Admin user not found, creating default admin account');
+        admin = await this.userService.createUser({
+          username: ADMIN_USERNAME,
+          password: ADMIN_PASSWORD,
+          nickname: '系统管理员',
+          isDisabled: DisabledStatus.ENABLED
+        });
+        logger.info('Default admin account created successfully');
+      }
+      return admin;
+    } catch (error) {
+      logger.error('Failed to ensure admin account exists:', error);
+      throw error;
+    }
+  }
+
+  async login(username: string, password: string): Promise<{ token: string; user: any }> {
+    try {
+      // 确保admin用户存在
+      if (username === ADMIN_USERNAME) {
+        await this.ensureAdminExists();
+      }
+
+      const user = await this.userService.getUserByUsername(username);
+      if (!user) {
+        throw new Error('User not found');
+      }
+
+      // 检查用户是否被禁用
+      if (user.isDisabled === DisabledStatus.DISABLED) {
+        throw new Error('User account is disabled');
+      }
+
+      const isPasswordValid = await this.userService.verifyPassword(user, password);
+      if (!isPasswordValid) {
+        throw new Error('Invalid password');
+      }
+
+      const token = this.generateToken(user);
+      return { token, user };
+    } catch (error) {
+      logger.error('Login error:', error);
+      throw error;
+    }
+  }
+
+  generateToken(user: any, expiresIn?: string): string {
+    return JWTUtil.generateToken(user, {}, expiresIn);
+  }
+
+  verifyToken(token: string): any {
+    return JWTUtil.verifyToken(token);
+  }
+
+  async logout(token: string): Promise<void> {
+    try {
+      // 验证token有效性
+      const decoded = this.verifyToken(token);
+      if (!decoded) {
+        throw new Error('Invalid token');
+      }
+
+      // 实际项目中这里可以添加token黑名单逻辑
+      // 或者调用Redis等缓存服务使token失效
+
+      return Promise.resolve();
+    } catch (error) {
+      console.error('Logout failed:', error);
+      throw error;
+    }
+  }
+}

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

@@ -0,0 +1,2 @@
+export { AuthService } from './auth.service';
+export { MiniAuthService } from './mini-auth.service';

+ 133 - 0
packages/auth-module/src/services/mini-auth.service.ts

@@ -0,0 +1,133 @@
+import { DataSource, Repository } from 'typeorm';
+import { UserEntity } from '@d8d/user-module';
+import { FileService } from '@d8d/file-module';
+import { JWTUtil } from '@d8d/shared-utils';
+import axios from 'axios';
+import process from 'node:process'
+
+export class MiniAuthService {
+  private userRepository: Repository<UserEntity>;
+  private fileService: FileService;
+
+  constructor(dataSource: DataSource) {
+    this.userRepository = dataSource.getRepository(UserEntity);
+    this.fileService = new FileService(dataSource);
+  }
+
+  async miniLogin(code: string): Promise<{ token: string; user: UserEntity; isNewUser: boolean }> {
+    // 1. 通过code获取openid
+    const openidInfo = await this.getOpenIdByCode(code);
+
+    // 2. 查找或创建用户
+    let user = await this.userRepository.findOne({
+      where: { openid: openidInfo.openid }
+    });
+
+    let isNewUser = false;
+
+    if (!user) {
+      // 自动注册新用户
+      user = await this.createMiniUser(openidInfo);
+      isNewUser = true;
+    }
+
+    // 3. 生成token
+    const token = this.generateToken(user);
+
+    return { token, user, isNewUser };
+  }
+
+  async updateUserProfile(userId: number, profile: { nickname?: string; avatarUrl?: string }): Promise<UserEntity> {
+    const user = await this.userRepository.findOne({
+      where: { id: userId },
+      relations: ['avatarFile']
+    });
+
+    if (!user) throw new Error('用户不存在');
+
+    if (profile.nickname) user.nickname = profile.nickname;
+
+    // 处理头像:如果用户没有头像且提供了小程序头像URL,则下载保存
+    if (profile.avatarUrl && !user.avatarFileId) {
+      try {
+        const avatarFileId = await this.downloadAndSaveAvatar(profile.avatarUrl, userId);
+        if (avatarFileId) {
+          user.avatarFileId = avatarFileId;
+        }
+      } catch (error) {
+        // 头像下载失败不影响主要功能
+        console.error('头像下载失败:', error);
+      }
+    }
+
+    return await this.userRepository.save(user);
+  }
+
+  private async getOpenIdByCode(code: string): Promise<{ openid: string; unionid?: string; session_key: string }> {
+    const appId = process.env.WX_MINI_APP_ID;
+    const appSecret = process.env.WX_MINI_APP_SECRET;
+
+    if (!appId || !appSecret) {
+      throw new Error('微信小程序配置缺失');
+    }
+
+    const url = `https://api.weixin.qq.com/sns/jscode2session?appid=${appId}&secret=${appSecret}&js_code=${code}&grant_type=authorization_code`;
+
+    try {
+      const response = await axios.get(url, { timeout: 10000 });
+
+      if (response.data.errcode) {
+        throw new Error(`微信API错误: ${response.data.errmsg}`);
+      }
+
+      return {
+        openid: response.data.openid,
+        unionid: response.data.unionid,
+        session_key: response.data.session_key
+      };
+    } catch (error) {
+      if (axios.isAxiosError(error)) {
+        throw new Error('微信服务器连接失败');
+      }
+      throw error;
+    }
+  }
+
+  private async createMiniUser(openidInfo: { openid: string; unionid?: string }): Promise<UserEntity> {
+    const user = this.userRepository.create({
+      username: `wx_${Date.now()}_${Math.random().toString(36).substr(2, 6)}`,
+      password: '', // 小程序用户不需要密码
+      openid: openidInfo.openid,
+      unionid: openidInfo.unionid,
+      nickname: '微信用户',
+      registrationSource: 'miniapp',
+      isDisabled: 0,
+      isDeleted: 0
+    });
+
+    return await this.userRepository.save(user);
+  }
+
+  private async downloadAndSaveAvatar(avatarUrl: string, userId: number): Promise<number | null> {
+    try {
+      const result = await this.fileService.downloadAndSaveFromUrl(
+        avatarUrl,
+        {
+          uploadUserId: userId,
+          customPath: `avatars/`,
+          mimeType: 'image/jpeg'
+        },
+        { timeout: 10000 }
+      );
+
+      return result.file.id;
+    } catch (error) {
+      console.error('下载保存头像失败:', error);
+      return null;
+    }
+  }
+
+  private generateToken(user: UserEntity): string {
+    return JWTUtil.generateToken(user);
+  }
+}

+ 21 - 0
packages/auth-module/tsconfig.json

@@ -0,0 +1,21 @@
+{
+  "extends": "../../tsconfig.json",
+  "compilerOptions": {
+    "composite": true,
+    "rootDir": "./src",
+    "outDir": "./dist"
+  },
+  "include": [
+    "src/**/*"
+  ],
+  "exclude": [
+    "dist",
+    "node_modules",
+    "tests"
+  ],
+  "references": [
+    { "path": "../shared-types" },
+    { "path": "../shared-utils" },
+    { "path": "../user-module" }
+  ]
+}