feat(admin): Complete Phase 3.5.1-3.5.4 Prompt Management System (83%)

Summary:
- Implement Prompt management infrastructure and core services
- Build admin portal frontend with light theme
- Integrate CodeMirror 6 editor for non-technical users

Phase 3.5.1: Infrastructure Setup
- Create capability_schema for Prompt storage
- Add prompt_templates and prompt_versions tables
- Add prompt:view/edit/debug/publish permissions
- Migrate RVW prompts to database (RVW_EDITORIAL, RVW_METHODOLOGY)

Phase 3.5.2: PromptService Core
- Implement gray preview logic (DRAFT for debuggers, ACTIVE for users)
- Module-level debug control (setDebugMode)
- Handlebars template rendering
- Variable extraction and validation (extractVariables, validateVariables)
- Three-level disaster recovery (database -> cache -> hardcoded fallback)

Phase 3.5.3: Management API
- 8 RESTful endpoints (/api/admin/prompts/*)
- Permission control (PROMPT_ENGINEER can edit, SUPER_ADMIN can publish)

Phase 3.5.4: Frontend Management UI
- Build admin portal architecture (AdminLayout, OrgLayout)
- Add route system (/admin/*, /org/*)
- Implement PromptListPage (filter, search, debug switch)
- Implement PromptEditor (CodeMirror 6 simplified for clinical users)
- Implement PromptEditorPage (edit, save, publish, test, version history)

Technical Details:
- Backend: 6 files, ~2044 lines (prompt.service.ts 596 lines)
- Frontend: 9 files, ~1735 lines (PromptEditorPage.tsx 399 lines)
- CodeMirror 6: Line numbers, auto-wrap, variable highlight, search, undo/redo
- Chinese-friendly: 15px font, 1.8 line-height, system fonts

Next Step: Phase 3.5.5 - Integrate RVW module with PromptService

Tested: Backend API tests passed (8/8), Frontend pending user testing
Status: Ready for Phase 3.5.5 RVW integration
This commit is contained in:
2026-01-11 21:25:16 +08:00
parent cdfbc9927a
commit 5523ef36ea
297 changed files with 15914 additions and 1266 deletions

View File

@@ -0,0 +1,436 @@
/**
* 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[];
}
/**
* 登录响应
*/
export interface LoginResponse {
user: UserInfoResponse;
tokens: TokenResponse;
}
/**
* Auth Service 类
*/
export class AuthService {
/**
* 密码登录
*/
async loginWithPassword(request: PasswordLoginRequest): Promise<LoginResponse> {
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);
// 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: { updatedAt: new Date() },
});
logger.info('用户登录成功(密码方式)', {
userId: user.id,
phone: user.phone,
role: user.role,
tenantId: user.tenant_id,
});
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,
},
tokens,
};
}
/**
* 验证码登录
*/
async loginWithVerificationCode(request: VerificationCodeLoginRequest): Promise<LoginResponse> {
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);
// 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,
});
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,
},
tokens,
};
}
/**
* 获取当前用户信息
*/
async getCurrentUser(userId: string): Promise<UserInfoResponse> {
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<void> {
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<TokenResponse> {
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<string[]> {
const rolePermissions = await prisma.role_permissions.findMany({
where: { role: role as any },
include: { permissions: true },
});
return rolePermissions.map(rp => rp.permissions.code);
}
/**
* 根据用户ID获取JWT Payload用于刷新Token
*/
async getUserPayloadById(userId: string): Promise<JWTPayload | null> {
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();