diff --git a/backend/services/identity-service/prisma/migrations/20251219130000_add_admin_accounts/migration.sql b/backend/services/identity-service/prisma/migrations/20251219130000_add_admin_accounts/migration.sql new file mode 100644 index 00000000..dab0de55 --- /dev/null +++ b/backend/services/identity-service/prisma/migrations/20251219130000_add_admin_accounts/migration.sql @@ -0,0 +1,26 @@ +-- CreateTable +CREATE TABLE "admin_accounts" ( + "id" BIGSERIAL NOT NULL, + "email" VARCHAR(100) NOT NULL, + "password_hash" VARCHAR(100) NOT NULL, + "nickname" VARCHAR(100) NOT NULL, + "role" VARCHAR(20) NOT NULL DEFAULT 'admin', + "status" VARCHAR(20) NOT NULL DEFAULT 'ACTIVE', + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "last_login_at" TIMESTAMP(3), + "updated_at" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "admin_accounts_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "admin_accounts_email_key" ON "admin_accounts"("email"); + +-- CreateIndex +CREATE INDEX "idx_admin_email" ON "admin_accounts"("email"); + +-- CreateIndex +CREATE INDEX "idx_admin_role" ON "admin_accounts"("role"); + +-- CreateIndex +CREATE INDEX "idx_admin_status" ON "admin_accounts"("status"); diff --git a/backend/services/identity-service/prisma/schema.prisma b/backend/services/identity-service/prisma/schema.prisma index bcdea888..6837936a 100644 --- a/backend/services/identity-service/prisma/schema.prisma +++ b/backend/services/identity-service/prisma/schema.prisma @@ -307,6 +307,27 @@ model ReferralLink { @@map("referral_links") } +// ============================================ +// 管理员账户表 - 用于后台管理系统登录 +// ============================================ +model AdminAccount { + id BigInt @id @default(autoincrement()) + email String @unique @db.VarChar(100) + passwordHash String @map("password_hash") @db.VarChar(100) // bcrypt 哈希 + nickname String @db.VarChar(100) + role String @default("admin") @db.VarChar(20) // super_admin, admin, operator + status String @default("ACTIVE") @db.VarChar(20) // ACTIVE, INACTIVE, LOCKED + + createdAt DateTime @default(now()) @map("created_at") + lastLoginAt DateTime? @map("last_login_at") + updatedAt DateTime @updatedAt @map("updated_at") + + @@index([email], name: "idx_admin_email") + @@index([role], name: "idx_admin_role") + @@index([status], name: "idx_admin_status") + @@map("admin_accounts") +} + // ============================================ // Outbox 事件表 - 保证事件可靠发送 // 使用 Outbox Pattern 确保领域事件100%送达 diff --git a/backend/services/identity-service/prisma/seed.ts b/backend/services/identity-service/prisma/seed.ts index 07623050..79819311 100644 --- a/backend/services/identity-service/prisma/seed.ts +++ b/backend/services/identity-service/prisma/seed.ts @@ -1,4 +1,5 @@ import { PrismaClient } from '@prisma/client'; +import * as bcrypt from 'bcrypt'; const prisma = new PrismaClient(); @@ -6,6 +7,19 @@ const prisma = new PrismaClient(); // 系统账户定义 // 系统账户使用特殊序列号格式: S + 00000 + 序号 (S0000000001 ~ S0000000004) // ============================================ +// ============================================ +// 管理员账户定义 +// 默认管理员密码: Admin@123456 +// ============================================ +const ADMIN_ACCOUNTS = [ + { + email: 'admin@rwadurian.com', + password: 'Admin@123456', + nickname: '超级管理员', + role: 'super_admin', + }, +]; + const SYSTEM_ACCOUNTS = [ { userId: BigInt(1), @@ -76,9 +90,32 @@ async function main() { console.log(` - Created system account: ${account.nickname} (accountSequence=${account.accountSequence})`); } + // 创建管理员账户 + console.log('Creating admin accounts...'); + for (const admin of ADMIN_ACCOUNTS) { + const passwordHash = await bcrypt.hash(admin.password, 10); + await prisma.adminAccount.upsert({ + where: { email: admin.email }, + update: { + passwordHash, + nickname: admin.nickname, + role: admin.role, + }, + create: { + email: admin.email, + passwordHash, + nickname: admin.nickname, + role: admin.role, + status: 'ACTIVE', + }, + }); + console.log(` - Created admin account: ${admin.email} (role=${admin.role})`); + } + console.log('Database seeded successfully!'); console.log(`- Initialized account sequence generator for date ${dateKey}`); console.log(`- Created ${SYSTEM_ACCOUNTS.length} system accounts (S0000000001-S0000000004)`); + console.log(`- Created ${ADMIN_ACCOUNTS.length} admin accounts`); } main() diff --git a/backend/services/identity-service/src/api/controllers/auth.controller.ts b/backend/services/identity-service/src/api/controllers/auth.controller.ts index 70241959..79dc6bdf 100644 --- a/backend/services/identity-service/src/api/controllers/auth.controller.ts +++ b/backend/services/identity-service/src/api/controllers/auth.controller.ts @@ -1,14 +1,23 @@ -import { Controller, Post, Body } from '@nestjs/common'; -import { ApiTags, ApiOperation } from '@nestjs/swagger'; +import { Controller, Post, Body, UnauthorizedException, Logger } from '@nestjs/common'; +import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger'; +import { JwtService } from '@nestjs/jwt'; +import { PrismaService } from '@/infrastructure/persistence/prisma.service'; import { UserApplicationService } from '@/application/services/user-application.service'; import { Public } from '@/shared/guards/jwt-auth.guard'; import { AutoLoginCommand } from '@/application/commands'; -import { AutoLoginDto } from '@/api/dto'; +import { AutoLoginDto, AdminLoginDto, AdminLoginResponseDto } from '@/api/dto'; +import * as bcrypt from 'bcrypt'; @ApiTags('Auth') @Controller('auth') export class AuthController { - constructor(private readonly userService: UserApplicationService) {} + private readonly logger = new Logger(AuthController.name); + + constructor( + private readonly userService: UserApplicationService, + private readonly jwtService: JwtService, + private readonly prisma: PrismaService, + ) {} @Public() @Post('refresh') @@ -16,4 +25,65 @@ export class AuthController { async refresh(@Body() dto: AutoLoginDto) { return this.userService.autoLogin(new AutoLoginCommand(dto.refreshToken, dto.deviceId)); } + + @Public() + @Post('login') + @ApiOperation({ summary: '管理员登录 (邮箱+密码)' }) + @ApiResponse({ status: 200, type: AdminLoginResponseDto }) + @ApiResponse({ status: 401, description: '邮箱或密码错误' }) + async adminLogin(@Body() dto: AdminLoginDto): Promise { + this.logger.log(`[AdminLogin] 尝试登录: ${dto.email}`); + + // 从数据库查找管理员 + const admin = await this.prisma.adminAccount.findUnique({ + where: { email: dto.email }, + }); + + if (!admin) { + this.logger.warn(`[AdminLogin] 管理员不存在: ${dto.email}`); + throw new UnauthorizedException('邮箱或密码错误'); + } + + // 检查账户状态 + if (admin.status !== 'ACTIVE') { + this.logger.warn(`[AdminLogin] 账户状态异常: ${dto.email}, status=${admin.status}`); + throw new UnauthorizedException('账户已被禁用'); + } + + // 验证密码 (使用 bcrypt) + const isPasswordValid = await bcrypt.compare(dto.password, admin.passwordHash); + + if (!isPasswordValid) { + this.logger.warn(`[AdminLogin] 密码错误: ${dto.email}`); + throw new UnauthorizedException('邮箱或密码错误'); + } + + // 更新最后登录时间 + await this.prisma.adminAccount.update({ + where: { id: admin.id }, + data: { lastLoginAt: new Date() }, + }); + + // 生成 JWT Token + const payload = { + sub: admin.id.toString(), + email: admin.email, + role: admin.role, + type: 'admin', + }; + + const accessToken = this.jwtService.sign(payload, { expiresIn: '24h' }); + const refreshToken = this.jwtService.sign(payload, { expiresIn: '7d' }); + + this.logger.log(`[AdminLogin] 登录成功: ${dto.email}`); + + return { + userId: admin.id.toString(), + email: admin.email, + nickname: admin.nickname, + role: admin.role, + accessToken, + refreshToken, + }; + } } diff --git a/backend/services/identity-service/src/api/dto/index.ts b/backend/services/identity-service/src/api/dto/index.ts index 43568eca..08cf2955 100644 --- a/backend/services/identity-service/src/api/dto/index.ts +++ b/backend/services/identity-service/src/api/dto/index.ts @@ -75,6 +75,38 @@ export class LoginDto { deviceId: string; } +export class AdminLoginDto { + @ApiProperty({ example: 'admin@example.com', description: '管理员邮箱' }) + @IsString() + @IsNotEmpty({ message: '邮箱不能为空' }) + email: string; + + @ApiProperty({ example: 'password123', description: '密码' }) + @IsString() + @IsNotEmpty({ message: '密码不能为空' }) + password: string; +} + +export class AdminLoginResponseDto { + @ApiProperty() + userId: string; + + @ApiProperty({ description: '管理员邮箱' }) + email: string; + + @ApiProperty({ description: '管理员昵称' }) + nickname: string; + + @ApiProperty({ description: '角色' }) + role: string; + + @ApiProperty() + accessToken: string; + + @ApiProperty() + refreshToken: string; +} + export class UpdateProfileDto { @ApiPropertyOptional() @IsOptional() diff --git a/frontend/admin-web/src/app/(auth)/login/page.tsx b/frontend/admin-web/src/app/(auth)/login/page.tsx index a8c43023..afbf1eae 100644 --- a/frontend/admin-web/src/app/(auth)/login/page.tsx +++ b/frontend/admin-web/src/app/(auth)/login/page.tsx @@ -74,30 +74,31 @@ export default function LoginPage() { console.log('[Login] 登录响应:', response); - // 根据后端返回的数据结构处理 - const { user, token, accessToken, permissions } = response.data || response; - const finalToken = token || accessToken; + // 后端返回格式: { userId, email, nickname, role, accessToken, refreshToken } + const data = response.data || response; + const { userId, email, nickname, role, accessToken, refreshToken } = data; - if (!finalToken) { - console.error('[Login] 响应中没有 token:', response); + if (!accessToken) { + console.error('[Login] 响应中没有 accessToken:', response); throw new Error('登录响应缺少 token'); } dispatch( setCredentials({ - user: user || { - id: '1', - email: formData.email, - username: formData.email.split('@')[0], - nickname: '管理员', + user: { + id: userId, + email: email, + username: email.split('@')[0], + nickname: nickname || '管理员', avatar: '', - role: 'super_admin', + role: role || 'super_admin', status: 'active', createdAt: new Date().toISOString(), lastLoginAt: new Date().toISOString(), }, - token: finalToken, - permissions: permissions || ['*'], + token: accessToken, + refreshToken: refreshToken, + permissions: ['*'], }) );