/** * Auth Service * * 认证业务逻辑 * * 支持两种登录方式: * 1. 手机号 + 验证码 * 2. 手机号 + 密码 */ import bcrypt from 'bcryptjs'; import { prisma } from '../../config/database.js'; import { jwtService } from './jwt.service.js'; import type { JWTPayload, TokenResponse } from './jwt.service.js'; import { logger } from '../logging/index.js'; /** * 登录请求 - 密码方式 */ export interface PasswordLoginRequest { phone: string; password: string; } /** * 登录请求 - 验证码方式 */ export interface VerificationCodeLoginRequest { phone: string; code: string; } /** * 修改密码请求 */ export interface ChangePasswordRequest { oldPassword?: string; // 如果用验证码修改,可不提供 newPassword: string; confirmPassword: string; } /** * 用户信息响应 */ export interface UserInfoResponse { id: string; phone: string; name: string; email?: string | null; role: string; tenantId: string; tenantCode?: string; tenantName?: string; departmentId?: string | null; departmentName?: string | null; isDefaultPassword: boolean; permissions: string[]; modules: string[]; // 用户可访问的模块代码列表 } /** * 登录响应 */ export interface LoginResponse { user: UserInfoResponse; tokens: TokenResponse; } /** * Auth Service 类 */ export class AuthService { /** * 密码登录 */ async loginWithPassword(request: PasswordLoginRequest): Promise { const { phone, password } = request; // 1. 查找用户 const user = await prisma.user.findUnique({ where: { phone }, include: { tenants: true, departments: true, }, }); if (!user) { logger.warn('登录失败:用户不存在', { phone }); throw new Error('手机号或密码错误'); } // 2. 验证密码 const isValidPassword = await bcrypt.compare(password, user.password); if (!isValidPassword) { logger.warn('登录失败:密码错误', { phone, userId: user.id }); throw new Error('手机号或密码错误'); } // 3. 检查用户状态 if (user.status !== 'active') { logger.warn('登录失败:用户已禁用', { phone, userId: user.id, status: user.status }); throw new Error('账号已被禁用,请联系管理员'); } // 4. 获取用户权限和模块列表 const permissions = await this.getUserPermissions(user.role); const modules = await this.getUserModules(user.id); // 5. 生成 JWT const jwtPayload: JWTPayload = { userId: user.id, phone: user.phone, role: user.role, tenantId: user.tenant_id, tenantCode: user.tenants?.code, isDefaultPassword: user.is_default_password, }; const tokens = jwtService.generateTokens(jwtPayload); // 6. 更新最后登录时间 await prisma.user.update({ where: { id: user.id }, data: { lastLoginAt: new Date() }, }); logger.info('用户登录成功(密码方式)', { userId: user.id, phone: user.phone, role: user.role, tenantId: user.tenant_id, modules: modules.length, }); return { user: { id: user.id, phone: user.phone, name: user.name, email: user.email, role: user.role, tenantId: user.tenant_id, tenantCode: user.tenants?.code, tenantName: user.tenants?.name, departmentId: user.department_id, departmentName: user.departments?.name, isDefaultPassword: user.is_default_password, permissions, modules, // 新增:返回模块列表 }, tokens, }; } /** * 验证码登录 */ async loginWithVerificationCode(request: VerificationCodeLoginRequest): Promise { const { phone, code } = request; // 1. 验证验证码 const verificationCode = await prisma.verification_codes.findFirst({ where: { phone, code, type: 'LOGIN', is_used: false, expires_at: { gt: new Date() }, }, orderBy: { created_at: 'desc' }, }); if (!verificationCode) { logger.warn('登录失败:验证码无效', { phone }); throw new Error('验证码无效或已过期'); } // 2. 标记验证码已使用 await prisma.verification_codes.update({ where: { id: verificationCode.id }, data: { is_used: true }, }); // 3. 查找用户 const user = await prisma.user.findUnique({ where: { phone }, include: { tenants: true, departments: true, }, }); if (!user) { logger.warn('登录失败:用户不存在', { phone }); throw new Error('用户不存在'); } // 4. 检查用户状态 if (user.status !== 'active') { logger.warn('登录失败:用户已禁用', { phone, userId: user.id, status: user.status }); throw new Error('账号已被禁用,请联系管理员'); } // 5. 获取用户权限和模块列表 const permissions = await this.getUserPermissions(user.role); const modules = await this.getUserModules(user.id); // 6. 生成 JWT const jwtPayload: JWTPayload = { userId: user.id, phone: user.phone, role: user.role, tenantId: user.tenant_id, tenantCode: user.tenants?.code, isDefaultPassword: user.is_default_password, }; const tokens = jwtService.generateTokens(jwtPayload); logger.info('用户登录成功(验证码方式)', { userId: user.id, phone: user.phone, role: user.role, modules: modules.length, }); return { user: { id: user.id, phone: user.phone, name: user.name, email: user.email, role: user.role, tenantId: user.tenant_id, tenantCode: user.tenants?.code, tenantName: user.tenants?.name, departmentId: user.department_id, departmentName: user.departments?.name, isDefaultPassword: user.is_default_password, permissions, modules, // 新增:返回模块列表 }, tokens, }; } /** * 获取当前用户信息 */ async getCurrentUser(userId: string): Promise { const user = await prisma.user.findUnique({ where: { id: userId }, include: { tenants: true, departments: true, }, }); if (!user) { throw new Error('用户不存在'); } const permissions = await this.getUserPermissions(user.role); return { id: user.id, phone: user.phone, name: user.name, email: user.email, role: user.role, tenantId: user.tenant_id, tenantCode: user.tenants?.code, tenantName: user.tenants?.name, departmentId: user.department_id, departmentName: user.departments?.name, isDefaultPassword: user.is_default_password, permissions, }; } /** * 修改密码 */ async changePassword(userId: string, request: ChangePasswordRequest): Promise { const { oldPassword, newPassword, confirmPassword } = request; // 1. 验证新密码 if (newPassword !== confirmPassword) { throw new Error('两次输入的密码不一致'); } if (newPassword.length < 6) { throw new Error('密码长度至少为6位'); } // 2. 获取用户 const user = await prisma.user.findUnique({ where: { id: userId }, }); if (!user) { throw new Error('用户不存在'); } // 3. 如果提供了旧密码,验证旧密码 if (oldPassword) { const isValidPassword = await bcrypt.compare(oldPassword, user.password); if (!isValidPassword) { throw new Error('原密码错误'); } } // 4. 加密新密码 const hashedPassword = await bcrypt.hash(newPassword, 10); // 5. 更新密码 await prisma.user.update({ where: { id: userId }, data: { password: hashedPassword, is_default_password: false, password_changed_at: new Date(), }, }); logger.info('用户修改密码成功', { userId }); } /** * 发送验证码 */ async sendVerificationCode(phone: string, type: 'LOGIN' | 'RESET_PASSWORD'): Promise<{ expiresIn: number }> { // 1. 检查用户是否存在 const user = await prisma.user.findUnique({ where: { phone }, }); if (!user) { throw new Error('用户不存在'); } // 2. 检查是否频繁发送(1分钟内只能发一次) const recentCode = await prisma.verification_codes.findFirst({ where: { phone, created_at: { gt: new Date(Date.now() - 60 * 1000) }, }, }); if (recentCode) { throw new Error('验证码发送过于频繁,请稍后再试'); } // 3. 生成6位验证码 const code = Math.floor(100000 + Math.random() * 900000).toString(); // 4. 设置5分钟过期 const expiresAt = new Date(Date.now() + 5 * 60 * 1000); // 5. 保存验证码 await prisma.verification_codes.create({ data: { phone, code, type: type as any, // VerificationType enum expires_at: expiresAt, }, }); // TODO: 实际发送短信 // 开发环境直接打印验证码 logger.info('📱 验证码已生成', { phone, code, type, expiresAt }); console.log(`\n📱 验证码: ${code} (有效期5分钟)\n`); return { expiresIn: 300 }; // 5分钟 } /** * 刷新 Token */ async refreshToken(refreshToken: string): Promise { return jwtService.refreshToken(refreshToken, async (userId) => { const user = await prisma.user.findUnique({ where: { id: userId }, include: { tenants: true }, }); if (!user) { return null; } return { userId: user.id, phone: user.phone, role: user.role, tenantId: user.tenant_id, tenantCode: user.tenants?.code, isDefaultPassword: user.is_default_password, }; }); } /** * 获取用户权限列表 */ private async getUserPermissions(role: string): Promise { const rolePermissions = await prisma.role_permissions.findMany({ where: { role: role as any }, include: { permissions: true }, }); return rolePermissions.map(rp => rp.permissions.code); } /** * 获取用户可访问的模块列表 * * 逻辑: * 1. 查询用户所有租户关系 * 2. 对每个租户,检查租户订阅的模块 * 3. 如果用户有自定义模块权限,使用自定义权限 * 4. 否则继承租户的全部模块权限 * 5. 去重后返回所有可访问模块 */ private async getUserModules(userId: string): Promise { // 获取用户的所有租户关系 const tenantMembers = await prisma.tenant_members.findMany({ where: { user_id: userId }, }); const allAccessibleModules = new Set(); for (const tm of tenantMembers) { // 获取租户订阅的模块 const tenantModules = await prisma.tenant_modules.findMany({ where: { tenant_id: tm.tenant_id, is_enabled: true, }, }); // 获取用户在该租户的自定义模块权限 const userModules = await prisma.user_modules.findMany({ where: { user_id: userId, tenant_id: tm.tenant_id, is_enabled: true, }, }); if (userModules.length > 0) { // 有自定义权限,使用自定义权限 userModules.forEach(um => allAccessibleModules.add(um.module_code)); } else { // 无自定义权限,继承租户所有模块 tenantModules.forEach(tm => allAccessibleModules.add(tm.module_code)); } } return Array.from(allAccessibleModules).sort(); } /** * 根据用户ID获取JWT Payload(用于刷新Token) */ async getUserPayloadById(userId: string): Promise { const user = await prisma.user.findUnique({ where: { id: userId }, include: { tenants: true }, }); if (!user) { return null; } return { userId: user.id, phone: user.phone, role: user.role, tenantId: user.tenant_id, tenantCode: user.tenants?.code, isDefaultPassword: user.is_default_password, }; } } // 导出单例 export const authService = new AuthService();