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

@@ -41,3 +41,4 @@ Status: Day 1 complete (11/11 tasks), ready for Day 2

View File

@@ -269,5 +269,6 @@

View File

@@ -217,3 +217,4 @@ https://iit.xunzhengyixue.com/api/v1/iit/health

View File

@@ -146,3 +146,4 @@ https://iit.xunzhengyixue.com/api/v1/iit/health

View File

@@ -47,3 +47,4 @@

View File

@@ -307,3 +307,4 @@ npx tsx src/modules/iit-manager/test-patient-wechat-url-verify.ts

View File

@@ -169,3 +169,4 @@ npm run dev

49
backend/check_db.ts Normal file
View File

@@ -0,0 +1,49 @@
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
async function main() {
// 查询所有 schema
const schemas = await prisma.$queryRaw`
SELECT schema_name
FROM information_schema.schemata
WHERE schema_name NOT IN ('pg_catalog', 'information_schema', 'pg_toast')
ORDER BY schema_name;
`;
console.log('\n=== 数据库中的 Schemas ===');
console.log(schemas);
// 查询每个 schema 下的表
const tables = await prisma.$queryRaw`
SELECT table_schema, table_name
FROM information_schema.tables
WHERE table_schema NOT IN ('pg_catalog', 'information_schema', 'pg_toast')
AND table_type = 'BASE TABLE'
ORDER BY table_schema, table_name;
`;
console.log('\n=== 数据库中的所有表 ===');
console.log(tables);
// 检查 platform_schema.users 的数据量
try {
const userCount = await prisma.$queryRaw`SELECT COUNT(*) as count FROM platform_schema.users;`;
console.log('\n=== platform_schema.users 数据量 ===');
console.log(userCount);
} catch (e) {
console.log('\n=== platform_schema.users 不存在或出错 ===');
}
// 检查 public.users 的数据量
try {
const publicUserCount = await prisma.$queryRaw`SELECT COUNT(*) as count FROM public.users;`;
console.log('\n=== public.users 数据量 ===');
console.log(publicUserCount);
} catch (e) {
console.log('\n=== public.users 不存在或出错 ===');
}
}
main()
.catch(console.error)
.finally(() => prisma.$disconnect());

43
backend/check_db_data.ts Normal file
View File

@@ -0,0 +1,43 @@
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
async function main() {
console.log('\n=== 各模块数据量检查 ===\n');
// 检查各个模块的数据
const queries = [
{ name: 'aia_schema.projects', sql: 'SELECT COUNT(*) as count FROM aia_schema.projects' },
{ name: 'aia_schema.conversations', sql: 'SELECT COUNT(*) as count FROM aia_schema.conversations' },
{ name: 'asl_schema.screening_projects', sql: 'SELECT COUNT(*) as count FROM asl_schema.screening_projects' },
{ name: 'asl_schema.literatures', sql: 'SELECT COUNT(*) as count FROM asl_schema.literatures' },
{ name: 'dc_schema.dc_templates', sql: 'SELECT COUNT(*) as count FROM dc_schema.dc_templates' },
{ name: 'dc_schema.dc_extraction_tasks', sql: 'SELECT COUNT(*) as count FROM dc_schema.dc_extraction_tasks' },
{ name: 'iit_schema.projects', sql: 'SELECT COUNT(*) as count FROM iit_schema.projects' },
{ name: 'pkb_schema.knowledge_bases', sql: 'SELECT COUNT(*) as count FROM pkb_schema.knowledge_bases' },
{ name: 'pkb_schema.documents', sql: 'SELECT COUNT(*) as count FROM pkb_schema.documents' },
{ name: 'platform_schema.users', sql: 'SELECT COUNT(*) as count FROM platform_schema.users' },
{ name: 'platform_schema.tenants', sql: 'SELECT COUNT(*) as count FROM platform_schema.tenants' },
{ name: 'platform_schema.departments', sql: 'SELECT COUNT(*) as count FROM platform_schema.departments' },
{ name: 'capability_schema.prompt_templates', sql: 'SELECT COUNT(*) as count FROM capability_schema.prompt_templates' },
];
for (const q of queries) {
try {
const result: any = await prisma.$queryRawUnsafe(q.sql);
console.log(`${q.name}: ${result[0].count} 条记录`);
} catch (e: any) {
console.log(`${q.name}: 查询失败 - ${e.message}`);
}
}
// 检查 platform_schema.users 的具体数据
console.log('\n=== platform_schema.users 详情 ===');
const users = await prisma.$queryRaw`SELECT id, name, phone, role, tenant_id FROM platform_schema.users;`;
console.log(users);
}
main()
.catch(console.error)
.finally(() => prisma.$disconnect());

38
backend/check_iit.ts Normal file
View File

@@ -0,0 +1,38 @@
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
async function main() {
// 检查 iit_schema 的所有表
const tables: any[] = await prisma.$queryRaw`
SELECT table_schema, table_name
FROM information_schema.tables
WHERE table_schema = 'iit_schema'
ORDER BY table_name
`;
console.log('iit_schema 中的表:');
console.log(tables);
// 检查每个表的列结构
if (tables.length > 0) {
for (const t of tables) {
console.log(`\n--- ${t.table_name} 的列 ---`);
const cols: any[] = await prisma.$queryRawUnsafe(`
SELECT column_name, data_type, is_nullable
FROM information_schema.columns
WHERE table_schema = 'iit_schema' AND table_name = '${t.table_name}'
ORDER BY ordinal_position
`);
cols.forEach(c => console.log(` ${c.column_name}: ${c.data_type}`));
}
}
// 检查备份中 iit_schema 是否存在
console.log('\n\n检查备份文件中是否有 iit_schema...');
}
main()
.catch(console.error)
.finally(() => prisma.$disconnect());

View File

@@ -0,0 +1,70 @@
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
async function main() {
console.log('🔍 检查 IIT 和 ASL 模块的数据\n');
console.log('=' .repeat(60));
// IIT 模块
console.log('\n📋 IIT 模块 (iit_schema):\n');
const iitTables = [
{ name: 'projects', query: 'SELECT COUNT(*) as count FROM iit_schema.projects' },
{ name: 'audit_logs', query: 'SELECT COUNT(*) as count FROM iit_schema.audit_logs' },
{ name: 'pending_actions', query: 'SELECT COUNT(*) as count FROM iit_schema.pending_actions' },
{ name: 'task_runs', query: 'SELECT COUNT(*) as count FROM iit_schema.task_runs' },
{ name: 'user_mappings', query: 'SELECT COUNT(*) as count FROM iit_schema.user_mappings' },
];
for (const t of iitTables) {
const result: any = await prisma.$queryRawUnsafe(t.query);
const count = Number(result[0].count);
console.log(` ${t.name}: ${count} 条记录 ${count > 0 ? '✅' : '(空)'}`);
}
// 如果有数据,显示一些详情
const iitProjects: any[] = await prisma.$queryRaw`SELECT id, name, status, created_at FROM iit_schema.projects LIMIT 5`;
if (iitProjects.length > 0) {
console.log('\n 最近的 IIT 项目:');
iitProjects.forEach(p => console.log(` - ${p.name} (${p.status}) @ ${p.created_at}`));
}
// ASL 模块(智能文献筛选)
console.log('\n\n📋 ASL 模块 - 智能文献筛选 (asl_schema):\n');
const aslTables = [
{ name: 'screening_projects', query: 'SELECT COUNT(*) as count FROM asl_schema.screening_projects' },
{ name: 'literatures', query: 'SELECT COUNT(*) as count FROM asl_schema.literatures' },
{ name: 'screening_tasks', query: 'SELECT COUNT(*) as count FROM asl_schema.screening_tasks' },
{ name: 'screening_results', query: 'SELECT COUNT(*) as count FROM asl_schema.screening_results' },
{ name: 'fulltext_screening_tasks', query: 'SELECT COUNT(*) as count FROM asl_schema.fulltext_screening_tasks' },
{ name: 'fulltext_screening_results', query: 'SELECT COUNT(*) as count FROM asl_schema.fulltext_screening_results' },
];
for (const t of aslTables) {
const result: any = await prisma.$queryRawUnsafe(t.query);
const count = Number(result[0].count);
console.log(` ${t.name}: ${count} 条记录 ${count > 0 ? '✅' : '(空)'}`);
}
// 如果有数据,显示一些详情
const aslProjects: any[] = await prisma.$queryRaw`SELECT id, project_name, status, created_at FROM asl_schema.screening_projects LIMIT 5`;
if (aslProjects.length > 0) {
console.log('\n 最近的 ASL 项目:');
aslProjects.forEach(p => console.log(` - ${p.project_name} (${p.status}) @ ${p.created_at}`));
}
const literatures: any[] = await prisma.$queryRaw`SELECT id, title, stage FROM asl_schema.literatures LIMIT 5`;
if (literatures.length > 0) {
console.log('\n 最近的文献:');
literatures.forEach(l => console.log(` - ${l.title?.substring(0, 50)}... (${l.stage})`));
}
console.log('\n' + '=' .repeat(60));
}
main()
.catch(console.error)
.finally(() => prisma.$disconnect());

View File

@@ -0,0 +1,33 @@
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
async function main() {
const cols: any[] = await prisma.$queryRaw`
SELECT column_name, data_type, is_nullable
FROM information_schema.columns
WHERE table_schema = 'platform_schema' AND table_name = 'queue'
ORDER BY ordinal_position
`;
console.log('platform_schema.queue 表的列:');
cols.forEach(c => console.log(` ${c.column_name}: ${c.data_type} ${c.is_nullable === 'NO' ? 'NOT NULL' : ''}`));
// 检查必要的列是否存在
const requiredCols = ['table_name', 'partition', 'retention_seconds', 'warning_queued'];
const existingCols = cols.map(c => c.column_name);
console.log('\n检查 create_queue 函数需要的列:');
for (const col of requiredCols) {
if (existingCols.includes(col)) {
console.log(`${col} 存在`);
} else {
console.log(`${col} 缺失!`);
}
}
}
main()
.catch(console.error)
.finally(() => prisma.$disconnect());

View File

@@ -0,0 +1,74 @@
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
async function main() {
console.log('🔍 检查 RVW 模块问题\n');
// 1. 检查用户 user-mock-001 是否存在
console.log('1. 检查用户 "user-mock-001":');
const users: any[] = await prisma.$queryRaw`
SELECT id, name, email, phone, role
FROM platform_schema.users
WHERE id = 'user-mock-001' OR email LIKE '%mock%' OR name LIKE '%mock%'
`;
if (users.length === 0) {
console.log(' ❌ 用户 "user-mock-001" 不存在!');
} else {
console.log(' ✅ 找到用户:');
users.forEach(u => console.log(` ${u.id}: ${u.name} (${u.email || u.phone})`));
}
// 2. 检查所有用户
console.log('\n2. 当前所有用户:');
const allUsers: any[] = await prisma.$queryRaw`
SELECT id, name, phone, role FROM platform_schema.users
`;
allUsers.forEach(u => console.log(` - ${u.id}: ${u.name} (${u.phone}) [${u.role}]`));
// 3. 检查 rvw_schema.review_tasks 表结构
console.log('\n3. rvw_schema.review_tasks 表结构:');
const cols: any[] = await prisma.$queryRaw`
SELECT column_name, data_type, is_nullable, column_default
FROM information_schema.columns
WHERE table_schema = 'rvw_schema' AND table_name = 'review_tasks'
ORDER BY ordinal_position
`;
cols.forEach(c => {
const nullable = c.is_nullable === 'YES' ? 'NULLABLE' : 'NOT NULL';
console.log(` ${c.column_name}: ${c.data_type} ${nullable}`);
});
// 4. 检查外键约束
console.log('\n4. review_tasks 的外键约束:');
const fks: any[] = await prisma.$queryRaw`
SELECT
tc.constraint_name,
kcu.column_name,
ccu.table_schema AS foreign_table_schema,
ccu.table_name AS foreign_table_name,
ccu.column_name AS foreign_column_name
FROM information_schema.table_constraints AS tc
JOIN information_schema.key_column_usage AS kcu
ON tc.constraint_name = kcu.constraint_name
JOIN information_schema.constraint_column_usage AS ccu
ON ccu.constraint_name = tc.constraint_name
WHERE tc.constraint_type = 'FOREIGN KEY'
AND tc.table_schema = 'rvw_schema'
AND tc.table_name = 'review_tasks'
`;
if (fks.length === 0) {
console.log(' 无外键约束');
} else {
fks.forEach(fk => {
console.log(` ${fk.column_name} -> ${fk.foreign_table_schema}.${fk.foreign_table_name}.${fk.foreign_column_name}`);
});
}
}
main()
.catch(console.error)
.finally(() => prisma.$disconnect());

21
backend/check_tables.ts Normal file
View File

@@ -0,0 +1,21 @@
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
async function main() {
// 检查 review 和 job 相关的表
const tables: any[] = await prisma.$queryRaw`
SELECT table_schema, table_name
FROM information_schema.tables
WHERE table_name LIKE '%review%' OR table_name LIKE '%job%'
ORDER BY table_schema, table_name
`;
console.log('Review 和 Job 相关的表:');
console.log(tables);
}
main()
.catch(console.error)
.finally(() => prisma.$disconnect());

109
backend/compare_db.ts Normal file
View File

@@ -0,0 +1,109 @@
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
async function main() {
console.log('🔍 数据库差异分析\n');
console.log('=' .repeat(60));
// 备份文件(2025-12-24)中应该存在的表
const backupTables = [
// aia_schema
'aia_schema.conversations',
'aia_schema.general_conversations',
'aia_schema.general_messages',
'aia_schema.messages',
'aia_schema.projects',
// asl_schema
'asl_schema.fulltext_screening_results',
'asl_schema.fulltext_screening_tasks',
'asl_schema.literatures',
'asl_schema.screening_projects',
'asl_schema.screening_results',
'asl_schema.screening_tasks',
// dc_schema
'dc_schema.dc_extraction_items',
'dc_schema.dc_extraction_tasks',
'dc_schema.dc_health_checks',
'dc_schema.dc_templates',
'dc_schema.dc_tool_c_ai_history',
'dc_schema.dc_tool_c_sessions',
// pkb_schema
'pkb_schema.batch_results',
'pkb_schema.batch_tasks',
'pkb_schema.documents',
'pkb_schema.knowledge_bases',
'pkb_schema.task_templates',
// platform_schema
'platform_schema.app_cache',
'platform_schema.job',
'platform_schema.job_common', // 可能缺失
'platform_schema.queue',
'platform_schema.schedule',
'platform_schema.subscription',
'platform_schema.users',
'platform_schema.version',
// public
'public._prisma_migrations',
'public.admin_logs',
'public.review_tasks', // 可能被移动到 rvw_schema
'public.users',
];
console.log('\n📋 检查备份中的表是否在当前数据库中存在:\n');
for (const table of backupTables) {
const [schema, tableName] = table.split('.');
try {
const result: any = await prisma.$queryRawUnsafe(
`SELECT COUNT(*) as count FROM information_schema.tables
WHERE table_schema = '${schema}' AND table_name = '${tableName}'`
);
if (result[0].count === 0n) {
console.log(`${table} - 不存在!`);
} else {
console.log(`${table} - 存在`);
}
} catch (e: any) {
console.log(`${table} - 查询失败: ${e.message}`);
}
}
// 检查 platform_schema.users 的列结构差异
console.log('\n\n📋 platform_schema.users 当前列结构:\n');
const cols: any[] = await prisma.$queryRaw`
SELECT column_name, data_type, is_nullable, column_default
FROM information_schema.columns
WHERE table_schema = 'platform_schema' AND table_name = 'users'
ORDER BY ordinal_position;
`;
cols.forEach(c => {
console.log(` ${c.column_name}: ${c.data_type} ${c.is_nullable === 'NO' ? 'NOT NULL' : 'NULLABLE'} ${c.column_default ? `DEFAULT ${c.column_default}` : ''}`);
});
// 备份中 platform_schema.users 应有的列
const originalUserColumns = ['id', 'email', 'password', 'name', 'avatar_url', 'role', 'status', 'kb_quota', 'kb_used', 'trial_ends_at', 'is_trial', 'last_login_at', 'created_at', 'updated_at'];
console.log('\n📋 对比 platform_schema.users 与备份:');
console.log(' 原始列(备份): ' + originalUserColumns.join(', '));
console.log(' 当前列: ' + cols.map(c => c.column_name).join(', '));
const currentColNames = cols.map(c => c.column_name);
const missingInCurrent = originalUserColumns.filter(c => !currentColNames.includes(c));
const newInCurrent = currentColNames.filter(c => !originalUserColumns.includes(c));
if (missingInCurrent.length > 0) {
console.log('\n ⚠️ 备份中有但当前缺失的列: ' + missingInCurrent.join(', '));
}
if (newInCurrent.length > 0) {
console.log(' 当前新增的列: ' + newInCurrent.join(', '));
}
console.log('\n' + '=' .repeat(60));
}
main()
.catch(console.error)
.finally(() => prisma.$disconnect());

80
backend/compare_dc_asl.ts Normal file
View File

@@ -0,0 +1,80 @@
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
async function getTableColumns(schema: string, tableName: string): Promise<any[]> {
return prisma.$queryRawUnsafe(`
SELECT column_name, data_type, is_nullable, column_default
FROM information_schema.columns
WHERE table_schema = '${schema}' AND table_name = '${tableName}'
ORDER BY ordinal_position
`);
}
async function main() {
console.log('🔍 DC 和 ASL 模块表结构对比\n');
console.log('=' .repeat(70));
// DC 模块的表
const dcTables = [
'dc_extraction_items',
'dc_extraction_tasks',
'dc_health_checks',
'dc_templates',
'dc_tool_c_ai_history',
'dc_tool_c_sessions'
];
// ASL 模块的表
const aslTables = [
'fulltext_screening_results',
'fulltext_screening_tasks',
'literatures',
'screening_projects',
'screening_results',
'screening_tasks'
];
console.log('\n📋 DC 模块 (dc_schema) 当前表结构:\n');
for (const table of dcTables) {
console.log(`\n--- dc_schema.${table} ---`);
try {
const cols = await getTableColumns('dc_schema', table);
if (cols.length === 0) {
console.log(' ❌ 表不存在');
} else {
cols.forEach((c: any) => {
console.log(` ${c.column_name}: ${c.data_type} ${c.is_nullable === 'NO' ? 'NOT NULL' : ''}`);
});
}
} catch (e) {
console.log(' ❌ 查询失败');
}
}
console.log('\n\n📋 ASL 模块 (asl_schema) 当前表结构:\n');
for (const table of aslTables) {
console.log(`\n--- asl_schema.${table} ---`);
try {
const cols = await getTableColumns('asl_schema', table);
if (cols.length === 0) {
console.log(' ❌ 表不存在');
} else {
cols.forEach((c: any) => {
console.log(` ${c.column_name}: ${c.data_type} ${c.is_nullable === 'NO' ? 'NOT NULL' : ''}`);
});
}
} catch (e) {
console.log(' ❌ 查询失败');
}
}
console.log('\n' + '=' .repeat(70));
}
main()
.catch(console.error)
.finally(() => prisma.$disconnect());

View File

@@ -0,0 +1,66 @@
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
async function getTableColumns(schema: string, tableName: string): Promise<any[]> {
return prisma.$queryRawUnsafe(`
SELECT column_name, data_type, is_nullable
FROM information_schema.columns
WHERE table_schema = '${schema}' AND table_name = '${tableName}'
ORDER BY ordinal_position
`);
}
async function main() {
console.log('🔍 PKB、AIA、RVW 模块表结构\n');
console.log('=' .repeat(70));
// PKB 模块的表
console.log('\n📋 PKB 模块 (pkb_schema):\n');
const pkbTables = ['batch_results', 'batch_tasks', 'documents', 'knowledge_bases', 'task_templates'];
for (const table of pkbTables) {
console.log(`\n--- pkb_schema.${table} ---`);
const cols = await getTableColumns('pkb_schema', table);
if (cols.length === 0) {
console.log(' ❌ 表不存在');
} else {
cols.forEach((c: any) => console.log(` ${c.column_name}: ${c.data_type} ${c.is_nullable === 'NO' ? 'NOT NULL' : ''}`));
}
}
// AIA 模块的表
console.log('\n\n📋 AIA 模块 (aia_schema):\n');
const aiaTables = ['conversations', 'general_conversations', 'general_messages', 'messages', 'projects'];
for (const table of aiaTables) {
console.log(`\n--- aia_schema.${table} ---`);
const cols = await getTableColumns('aia_schema', table);
if (cols.length === 0) {
console.log(' ❌ 表不存在');
} else {
cols.forEach((c: any) => console.log(` ${c.column_name}: ${c.data_type} ${c.is_nullable === 'NO' ? 'NOT NULL' : ''}`));
}
}
// RVW 模块的表
console.log('\n\n📋 RVW 模块 (rvw_schema):\n');
const rvwTables = ['review_tasks'];
for (const table of rvwTables) {
console.log(`\n--- rvw_schema.${table} ---`);
const cols = await getTableColumns('rvw_schema', table);
if (cols.length === 0) {
console.log(' ❌ 表不存在');
} else {
cols.forEach((c: any) => console.log(` ${c.column_name}: ${c.data_type} ${c.is_nullable === 'NO' ? 'NOT NULL' : ''}`));
}
}
console.log('\n' + '=' .repeat(70));
}
main()
.catch(console.error)
.finally(() => prisma.$disconnect());

View File

@@ -0,0 +1,108 @@
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
async function main() {
console.log('🔍 检查数据库中 Prisma 未管理的对象\n');
console.log('=' .repeat(70));
// 1. 获取所有数据库函数
console.log('\n📋 数据库函数 (Functions):');
const functions: any[] = await prisma.$queryRaw`
SELECT routine_schema, routine_name, routine_type
FROM information_schema.routines
WHERE routine_schema NOT IN ('pg_catalog', 'information_schema')
ORDER BY routine_schema, routine_name
`;
if (functions.length === 0) {
console.log(' 无自定义函数');
} else {
functions.forEach(f => console.log(` - ${f.routine_schema}.${f.routine_name} (${f.routine_type})`));
}
// 2. 获取所有索引(非主键、非外键)
console.log('\n📋 自定义索引 (Indexes):');
const indexes: any[] = await prisma.$queryRaw`
SELECT schemaname, tablename, indexname
FROM pg_indexes
WHERE schemaname NOT IN ('pg_catalog', 'information_schema')
AND indexname NOT LIKE '%pkey%'
AND indexname NOT LIKE '%_fkey%'
ORDER BY schemaname, tablename, indexname
LIMIT 30
`;
console.log(`${indexes.length} 个索引 (显示前30个)`);
// 3. 获取所有序列
console.log('\n📋 序列 (Sequences):');
const sequences: any[] = await prisma.$queryRaw`
SELECT sequence_schema, sequence_name
FROM information_schema.sequences
WHERE sequence_schema NOT IN ('pg_catalog', 'information_schema')
ORDER BY sequence_schema, sequence_name
`;
sequences.forEach(s => console.log(` - ${s.sequence_schema}.${s.sequence_name}`));
// 4. 检查枚举类型
console.log('\n📋 枚举类型 (Enums):');
const enums: any[] = await prisma.$queryRaw`
SELECT n.nspname as schema, t.typname as enum_name,
string_agg(e.enumlabel, ', ' ORDER BY e.enumsortorder) as values
FROM pg_type t
JOIN pg_enum e ON t.oid = e.enumtypid
JOIN pg_namespace n ON t.typnamespace = n.oid
WHERE n.nspname NOT IN ('pg_catalog', 'information_schema')
GROUP BY n.nspname, t.typname
ORDER BY n.nspname, t.typname
`;
enums.forEach(e => console.log(` - ${e.schema}.${e.enum_name}: [${e.values}]`));
// 5. 检查触发器
console.log('\n📋 触发器 (Triggers):');
const triggers: any[] = await prisma.$queryRaw`
SELECT trigger_schema, trigger_name, event_object_table
FROM information_schema.triggers
WHERE trigger_schema NOT IN ('pg_catalog', 'information_schema')
ORDER BY trigger_schema, trigger_name
`;
if (triggers.length === 0) {
console.log(' 无自定义触发器');
} else {
triggers.forEach(t => console.log(` - ${t.trigger_schema}.${t.trigger_name} on ${t.event_object_table}`));
}
// 6. 检查视图
console.log('\n📋 视图 (Views):');
const views: any[] = await prisma.$queryRaw`
SELECT table_schema, table_name
FROM information_schema.views
WHERE table_schema NOT IN ('pg_catalog', 'information_schema')
ORDER BY table_schema, table_name
`;
if (views.length === 0) {
console.log(' 无自定义视图');
} else {
views.forEach(v => console.log(` - ${v.table_schema}.${v.table_name}`));
}
// 7. 列出 Prisma 不管理的重要对象
console.log('\n\n⚠ 需要手动管理的数据库对象 (Prisma 不管理):');
console.log(' 1. platform_schema.create_queue() 函数');
console.log(' 2. platform_schema.delete_queue() 函数');
console.log(' 3. platform_schema.job_state 枚举 (pg-boss 创建)');
console.log(' 4. platform_schema.job_common 表 (pg-boss 运行时创建)');
console.log(' 5. 各种索引和约束');
console.log('\n' + '=' .repeat(70));
}
main()
.catch(console.error)
.finally(() => prisma.$disconnect());

View File

@@ -0,0 +1,19 @@
-- 在 public.users 中创建 mock 用户
-- 用于 RVW 模块的测试
INSERT INTO public.users (id, email, password, name, role, status, kb_quota, kb_used, is_trial, created_at, updated_at)
VALUES (
'user-mock-001',
'mock@test.com',
'$2b$10$mockhashedpassword123456789',
'测试用户',
'user',
'active',
3,
0,
false,
NOW(),
NOW()
)
ON CONFLICT (id) DO NOTHING;

View File

@@ -0,0 +1,51 @@
-- 在 platform_schema.users 中创建 mock 用户
-- 用于 PKB 等模块的测试
-- 首先需要一个默认租户
INSERT INTO platform_schema.tenants (id, code, name, type, status, created_at, updated_at)
VALUES (
'tenant-mock-001',
'mock-tenant',
'测试租户',
'INTERNAL',
'ACTIVE',
NOW(),
NOW()
)
ON CONFLICT (id) DO NOTHING;
-- 创建 mock 用户
INSERT INTO platform_schema.users (
id,
phone,
email,
password,
is_default_password,
name,
role,
status,
tenant_id,
kb_quota,
kb_used,
is_trial,
created_at,
updated_at
)
VALUES (
'user-mock-001',
'13800000000',
'mock@test.com',
'$2b$10$mockhashedpassword123456789',
true,
'测试用户',
'USER',
'active',
'tenant-mock-001',
3,
0,
false,
NOW(),
NOW()
)
ON CONFLICT (id) DO NOTHING;

View File

@@ -64,5 +64,6 @@ WHERE table_schema = 'dc_schema'

View File

@@ -17,15 +17,18 @@
"@wecom/crypto": "^1.0.1",
"ajv": "^8.17.1",
"axios": "^1.12.2",
"bcryptjs": "^2.4.3",
"bullmq": "^5.65.0",
"diff-match-patch": "^1.0.5",
"dotenv": "^17.2.3",
"exceljs": "^4.4.0",
"fastify": "^5.6.1",
"form-data": "^4.0.4",
"handlebars": "^4.7.8",
"html2canvas": "^1.4.1",
"js-yaml": "^4.1.0",
"jsonrepair": "^3.13.1",
"jsonwebtoken": "^9.0.2",
"jspdf": "^3.0.3",
"p-queue": "^9.0.1",
"pg-boss": "^12.5.2",
@@ -37,7 +40,9 @@
"zod": "^4.1.12"
},
"devDependencies": {
"@types/bcryptjs": "^2.4.6",
"@types/js-yaml": "^4.0.9",
"@types/jsonwebtoken": "^9.0.7",
"@types/node": "^24.7.1",
"@types/winston": "^2.4.4",
"@types/xml2js": "^0.4.14",
@@ -1017,6 +1022,13 @@
"dev": true,
"license": "MIT"
},
"node_modules/@types/bcryptjs": {
"version": "2.4.6",
"resolved": "https://registry.npmmirror.com/@types/bcryptjs/-/bcryptjs-2.4.6.tgz",
"integrity": "sha512-9xlo6R2qDs5uixm0bcIqCeMCE6HiQsIyel9KQySStiyqNl2tnj2mP3DX1Nf56MD6KMenNNlBBsy3LJ7gUEQPXQ==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/form-data": {
"version": "2.2.1",
"resolved": "https://registry.npmmirror.com/@types/form-data/-/form-data-2.2.1.tgz",
@@ -1033,6 +1045,24 @@
"dev": true,
"license": "MIT"
},
"node_modules/@types/jsonwebtoken": {
"version": "9.0.10",
"resolved": "https://registry.npmmirror.com/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz",
"integrity": "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/ms": "*",
"@types/node": "*"
}
},
"node_modules/@types/ms": {
"version": "2.1.0",
"resolved": "https://registry.npmmirror.com/@types/ms/-/ms-2.1.0.tgz",
"integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/node": {
"version": "24.7.1",
"resolved": "https://registry.npmmirror.com/@types/node/-/node-24.7.1.tgz",
@@ -1354,6 +1384,12 @@
],
"license": "MIT"
},
"node_modules/bcryptjs": {
"version": "2.4.3",
"resolved": "https://registry.npmmirror.com/bcryptjs/-/bcryptjs-2.4.3.tgz",
"integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==",
"license": "MIT"
},
"node_modules/better-sqlite3": {
"version": "12.4.6",
"resolved": "https://registry.npmmirror.com/better-sqlite3/-/better-sqlite3-12.4.6.tgz",
@@ -1493,6 +1529,12 @@
"node": "*"
}
},
"node_modules/buffer-equal-constant-time": {
"version": "1.0.1",
"resolved": "https://registry.npmmirror.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
"integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==",
"license": "BSD-3-Clause"
},
"node_modules/buffer-indexof-polyfill": {
"version": "1.0.2",
"resolved": "https://registry.npmmirror.com/buffer-indexof-polyfill/-/buffer-indexof-polyfill-1.0.2.tgz",
@@ -2747,6 +2789,27 @@
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
"license": "ISC"
},
"node_modules/handlebars": {
"version": "4.7.8",
"resolved": "https://registry.npmmirror.com/handlebars/-/handlebars-4.7.8.tgz",
"integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==",
"license": "MIT",
"dependencies": {
"minimist": "^1.2.5",
"neo-async": "^2.6.2",
"source-map": "^0.6.1",
"wordwrap": "^1.0.0"
},
"bin": {
"handlebars": "bin/handlebars"
},
"engines": {
"node": ">=0.4.7"
},
"optionalDependencies": {
"uglify-js": "^3.1.4"
}
},
"node_modules/has-flag": {
"version": "3.0.0",
"resolved": "https://registry.npmmirror.com/has-flag/-/has-flag-3.0.0.tgz",
@@ -3041,6 +3104,28 @@
"jsonrepair": "bin/cli.js"
}
},
"node_modules/jsonwebtoken": {
"version": "9.0.3",
"resolved": "https://registry.npmmirror.com/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz",
"integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==",
"license": "MIT",
"dependencies": {
"jws": "^4.0.1",
"lodash.includes": "^4.3.0",
"lodash.isboolean": "^3.0.3",
"lodash.isinteger": "^4.0.4",
"lodash.isnumber": "^3.0.3",
"lodash.isplainobject": "^4.0.6",
"lodash.isstring": "^4.0.1",
"lodash.once": "^4.0.0",
"ms": "^2.1.1",
"semver": "^7.5.4"
},
"engines": {
"node": ">=12",
"npm": ">=6"
}
},
"node_modules/jspdf": {
"version": "3.0.3",
"resolved": "https://registry.npmmirror.com/jspdf/-/jspdf-3.0.3.tgz",
@@ -3106,6 +3191,27 @@
"safe-buffer": "~5.1.0"
}
},
"node_modules/jwa": {
"version": "2.0.1",
"resolved": "https://registry.npmmirror.com/jwa/-/jwa-2.0.1.tgz",
"integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==",
"license": "MIT",
"dependencies": {
"buffer-equal-constant-time": "^1.0.1",
"ecdsa-sig-formatter": "1.0.11",
"safe-buffer": "^5.0.1"
}
},
"node_modules/jws": {
"version": "4.0.1",
"resolved": "https://registry.npmmirror.com/jws/-/jws-4.0.1.tgz",
"integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==",
"license": "MIT",
"dependencies": {
"jwa": "^2.0.1",
"safe-buffer": "^5.0.1"
}
},
"node_modules/kuler": {
"version": "2.0.0",
"resolved": "https://registry.npmmirror.com/kuler/-/kuler-2.0.0.tgz",
@@ -3236,6 +3342,12 @@
"integrity": "sha512-5dcWxm23+VAoz+awKmBaiBvzox8+RqMgFhi7UvX9DHZr2HdxHXM/Wrf8cfKpsW37RNrvtPn6hSwNqurSILbmJw==",
"license": "MIT"
},
"node_modules/lodash.includes": {
"version": "4.3.0",
"resolved": "https://registry.npmmirror.com/lodash.includes/-/lodash.includes-4.3.0.tgz",
"integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==",
"license": "MIT"
},
"node_modules/lodash.isarguments": {
"version": "3.1.0",
"resolved": "https://registry.npmmirror.com/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz",
@@ -3261,24 +3373,48 @@
"integrity": "sha512-AirXNj15uRIMMPihnkInB4i3NHeb4iBtNg9WRWuK2o31S+ePwwNmDPaTL3o7dTJ+VXNZim7rFs4rxN4YU1oUJw==",
"license": "MIT"
},
"node_modules/lodash.isinteger": {
"version": "4.0.4",
"resolved": "https://registry.npmmirror.com/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
"integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==",
"license": "MIT"
},
"node_modules/lodash.isnil": {
"version": "4.0.0",
"resolved": "https://registry.npmmirror.com/lodash.isnil/-/lodash.isnil-4.0.0.tgz",
"integrity": "sha512-up2Mzq3545mwVnMhTDMdfoG1OurpA/s5t88JmQX809eH3C8491iu2sfKhTfhQtKY78oPNhiaHJUpT/dUDAAtng==",
"license": "MIT"
},
"node_modules/lodash.isnumber": {
"version": "3.0.3",
"resolved": "https://registry.npmmirror.com/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz",
"integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==",
"license": "MIT"
},
"node_modules/lodash.isplainobject": {
"version": "4.0.6",
"resolved": "https://registry.npmmirror.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
"integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==",
"license": "MIT"
},
"node_modules/lodash.isstring": {
"version": "4.0.1",
"resolved": "https://registry.npmmirror.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz",
"integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==",
"license": "MIT"
},
"node_modules/lodash.isundefined": {
"version": "3.0.1",
"resolved": "https://registry.npmmirror.com/lodash.isundefined/-/lodash.isundefined-3.0.1.tgz",
"integrity": "sha512-MXB1is3s899/cD8jheYYE2V9qTHwKvt+npCwpD+1Sxm3Q3cECXCiYHjeHWXNwr6Q0SOBPrYUDxendrO6goVTEA==",
"license": "MIT"
},
"node_modules/lodash.once": {
"version": "4.1.1",
"resolved": "https://registry.npmmirror.com/lodash.once/-/lodash.once-4.1.1.tgz",
"integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==",
"license": "MIT"
},
"node_modules/lodash.union": {
"version": "4.6.0",
"resolved": "https://registry.npmmirror.com/lodash.union/-/lodash.union-4.6.0.tgz",
@@ -3466,6 +3602,12 @@
"dev": true,
"license": "MIT"
},
"node_modules/neo-async": {
"version": "2.6.2",
"resolved": "https://registry.npmmirror.com/neo-async/-/neo-async-2.6.2.tgz",
"integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==",
"license": "MIT"
},
"node_modules/node-abi": {
"version": "3.85.0",
"resolved": "https://registry.npmmirror.com/node-abi/-/node-abi-3.85.0.tgz",
@@ -4471,6 +4613,15 @@
"atomic-sleep": "^1.0.0"
}
},
"node_modules/source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmmirror.com/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/split2": {
"version": "4.2.0",
"resolved": "https://registry.npmmirror.com/split2/-/split2-4.2.0.tgz",
@@ -4808,6 +4959,19 @@
"node": ">=14.17"
}
},
"node_modules/uglify-js": {
"version": "3.19.3",
"resolved": "https://registry.npmmirror.com/uglify-js/-/uglify-js-3.19.3.tgz",
"integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==",
"license": "BSD-2-Clause",
"optional": true,
"bin": {
"uglifyjs": "bin/uglifyjs"
},
"engines": {
"node": ">=0.8.0"
}
},
"node_modules/undefsafe": {
"version": "2.0.5",
"resolved": "https://registry.npmmirror.com/undefsafe/-/undefsafe-2.0.5.tgz",
@@ -4954,6 +5118,12 @@
"node": ">=0.8"
}
},
"node_modules/wordwrap": {
"version": "1.0.0",
"resolved": "https://registry.npmmirror.com/wordwrap/-/wordwrap-1.0.0.tgz",
"integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==",
"license": "MIT"
},
"node_modules/wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmmirror.com/wrappy/-/wrappy-1.0.2.tgz",

View File

@@ -30,6 +30,9 @@
"@fastify/jwt": "^10.0.0",
"@fastify/multipart": "^9.2.1",
"@prisma/client": "^6.17.0",
"bcryptjs": "^2.4.3",
"handlebars": "^4.7.8",
"jsonwebtoken": "^9.0.2",
"@types/form-data": "^2.2.1",
"@wecom/crypto": "^1.0.1",
"ajv": "^8.17.1",
@@ -54,7 +57,9 @@
"zod": "^4.1.12"
},
"devDependencies": {
"@types/bcryptjs": "^2.4.6",
"@types/js-yaml": "^4.0.9",
"@types/jsonwebtoken": "^9.0.7",
"@types/node": "^24.7.1",
"@types/winston": "^2.4.4",
"@types/xml2js": "^0.4.14",

View File

@@ -102,5 +102,6 @@ ORDER BY ordinal_position;

View File

@@ -115,5 +115,6 @@ runMigration()

View File

@@ -49,5 +49,6 @@ COMMENT ON COLUMN "dc_schema"."dc_tool_c_sessions"."column_mapping" IS '列名

View File

@@ -76,5 +76,6 @@ COMMENT ON COLUMN dc_schema.dc_tool_c_sessions.expires_at IS '过期时间(创

File diff suppressed because it is too large Load Diff

View File

@@ -1,121 +1,475 @@
/**
* 数据库种子数据脚本
* 用于初始化开发环境的测试用户
*/
import { PrismaClient } from '@prisma/client';
import { PrismaClient, UserRole, TenantType, TenantStatus } from '@prisma/client';
import bcrypt from 'bcryptjs';
import { v4 as uuidv4 } from 'uuid';
const prisma = new PrismaClient();
async function main() {
console.log('🌱 开始初始化数据库种子数据...');
// 默认密码
const DEFAULT_PASSWORD = '123456';
// 创建测试用户
const mockUser = await prisma.user.upsert({
where: { id: 'user-mock-001' },
async function main() {
console.log('🌱 开始创建种子数据...\n');
// 加密默认密码
const hashedDefaultPassword = await bcrypt.hash(DEFAULT_PASSWORD, 10);
// ============================================
// 1. 创建内部租户(运营团队专用)
// ============================================
console.log('📌 创建内部租户...');
const internalTenant = await prisma.tenants.upsert({
where: { code: 'yizhengxun' },
update: {},
create: {
id: 'user-mock-001',
email: 'test@example.com',
password: '$2b$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/LewY5GyYIkYvKx7ES', // password: "password123"
name: '测试用户',
role: 'user',
id: uuidv4(),
code: 'yizhengxun',
name: '壹证循科技',
type: TenantType.INTERNAL,
status: TenantStatus.ACTIVE,
config: {
logo: null,
backgroundImage: null,
primaryColor: '#1890ff',
systemName: 'AI临床研究平台 - 运营管理端',
},
total_quota: BigInt(999999999),
updated_at: new Date(),
},
});
console.log(` ✅ 内部租户创建成功: ${internalTenant.name}`);
// ============================================
// 1.5 创建公共租户(个人用户池)
// ============================================
console.log('📌 创建公共租户(个人用户)...');
const publicTenant = await prisma.tenants.upsert({
where: { code: 'public' },
update: {},
create: {
id: uuidv4(),
code: 'public',
name: '个人用户',
type: TenantType.PUBLIC,
status: TenantStatus.ACTIVE,
config: {
logo: null,
backgroundImage: null,
primaryColor: '#1890ff',
systemName: 'AI临床研究平台',
},
total_quota: BigInt(100000),
updated_at: new Date(),
},
});
console.log(` ✅ 公共租户创建成功: ${publicTenant.name}`);
// 为公共租户开放部分模块
const publicModules = ['PKB', 'RVW'];
for (const moduleCode of publicModules) {
await prisma.tenant_modules.upsert({
where: { tenant_id_module_code: { tenant_id: publicTenant.id, module_code: moduleCode } },
update: {},
create: {
id: uuidv4(),
tenant_id: publicTenant.id,
module_code: moduleCode,
is_enabled: true,
},
});
}
console.log(` ✅ 公共租户模块订阅: ${publicModules.join(', ')}`);
// ============================================
// 2. 创建超级管理员
// ============================================
console.log('📌 创建超级管理员...');
const superAdmin = await prisma.User.upsert({
where: { phone: '13800000001' },
update: {},
create: {
phone: '13800000001',
password: hashedDefaultPassword,
is_default_password: false,
name: '超级管理员',
tenant_id: internalTenant.id,
role: UserRole.SUPER_ADMIN,
status: 'active',
kbQuota: 3,
kbUsed: 0,
isTrial: true,
trialEndsAt: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000), // 30天后
isTrial: false,
},
});
console.log(` ✅ 超级管理员创建成功: ${superAdmin.phone}`);
// ============================================
// 3. 创建Prompt工程师账号
// ============================================
console.log('📌 创建Prompt工程师账号...');
const promptEngineer = await prisma.User.upsert({
where: { phone: '13800000002' },
update: {},
create: {
phone: '13800000002',
password: hashedDefaultPassword,
is_default_password: false,
name: 'Prompt工程师',
tenant_id: internalTenant.id,
role: UserRole.PROMPT_ENGINEER,
status: 'active',
isTrial: false,
},
});
console.log(` ✅ Prompt工程师创建成功: ${promptEngineer.phone}`);
// ============================================
// 4. 创建示例租户(医院)
// ============================================
console.log('📌 创建示例租户(医院)...');
const hospitalTenant = await prisma.tenants.upsert({
where: { code: 'demo-hospital' },
update: {},
create: {
id: uuidv4(),
code: 'demo-hospital',
name: '示范医院',
type: TenantType.HOSPITAL,
status: TenantStatus.ACTIVE,
config: {
logo: null,
backgroundImage: null,
primaryColor: '#1890ff',
systemName: '示范医院临床研究平台',
},
total_quota: BigInt(1000000),
contact_name: '张主任',
contact_phone: '13800138000',
contact_email: 'zhang@demo-hospital.com',
updated_at: new Date(),
},
});
console.log(` ✅ 示例医院租户创建成功: ${hospitalTenant.name}`);
// ============================================
// 5. 创建医院科室
// ============================================
console.log('📌 创建医院科室...');
const cardiology = await prisma.departments.upsert({
where: { id: 'dept-cardiology' },
update: {},
create: {
id: 'dept-cardiology',
tenant_id: hospitalTenant.id,
name: '心内科',
description: '心血管内科',
updated_at: new Date(),
},
});
console.log('✅ 测试用户创建成功:', {
id: mockUser.id,
email: mockUser.email,
name: mockUser.name,
});
// 可选:创建管理员用户
const adminUser = await prisma.user.upsert({
where: { email: 'admin@example.com' },
const neurology = await prisma.departments.upsert({
where: { id: 'dept-neurology' },
update: {},
create: {
id: 'user-admin-001',
email: 'admin@example.com',
password: '$2b$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/LewY5GyYIkYvKx7ES', // password: "password123"
name: '管理员',
role: 'admin',
id: 'dept-neurology',
tenant_id: hospitalTenant.id,
name: '神经内科',
description: '神经内科',
updated_at: new Date(),
},
});
console.log(` ✅ 科室创建成功: 心内科、神经内科`);
// ============================================
// 6. 创建示例租户(药企)
// ============================================
console.log('📌 创建示例租户(药企)...');
const pharmaTenant = await prisma.tenants.upsert({
where: { code: 'demo-pharma' },
update: {},
create: {
id: uuidv4(),
code: 'demo-pharma',
name: '示范药企',
type: TenantType.PHARMA,
status: TenantStatus.ACTIVE,
config: {
logo: null,
backgroundImage: null,
primaryColor: '#52c41a',
systemName: '示范药企IIT管理平台',
},
total_quota: BigInt(2000000),
contact_name: '李经理',
contact_phone: '13900139000',
contact_email: 'li@demo-pharma.com',
updated_at: new Date(),
},
});
console.log(` ✅ 示例药企租户创建成功: ${pharmaTenant.name}`);
// ============================================
// 7. 创建医院管理员
// ============================================
console.log('📌 创建医院管理员...');
const hospitalAdmin = await prisma.User.upsert({
where: { phone: '13800138001' },
update: {},
create: {
phone: '13800138001',
password: hashedDefaultPassword,
is_default_password: true,
name: '张主任',
tenant_id: hospitalTenant.id,
department_id: cardiology.id,
role: UserRole.HOSPITAL_ADMIN,
status: 'active',
isTrial: false,
},
});
console.log(` ✅ 医院管理员创建成功: ${hospitalAdmin.phone} (${hospitalAdmin.name})`);
// 创建租户成员关系
await prisma.tenant_members.upsert({
where: { tenant_id_user_id: { tenant_id: hospitalTenant.id, user_id: hospitalAdmin.id } },
update: {},
create: {
id: uuidv4(),
tenant_id: hospitalTenant.id,
user_id: hospitalAdmin.id,
role: UserRole.HOSPITAL_ADMIN,
},
});
// ============================================
// 8. 创建普通医生用户
// ============================================
console.log('📌 创建普通医生用户...');
const doctor1 = await prisma.User.upsert({
where: { phone: '13800138002' },
update: {},
create: {
phone: '13800138002',
password: hashedDefaultPassword,
is_default_password: true,
name: '李医生',
tenant_id: hospitalTenant.id,
department_id: cardiology.id,
role: UserRole.USER,
status: 'active',
kbQuota: 10,
kbUsed: 0,
isTrial: false,
},
});
console.log('✅ 管理员用户创建成功:', {
id: adminUser.id,
email: adminUser.email,
name: adminUser.name,
const doctor2 = await prisma.User.upsert({
where: { phone: '13800138003' },
update: {},
create: {
phone: '13800138003',
password: hashedDefaultPassword,
is_default_password: true,
name: '王医生',
tenant_id: hospitalTenant.id,
department_id: neurology.id,
role: UserRole.USER,
status: 'active',
isTrial: false,
},
});
console.log(` ✅ 普通用户创建成功: ${doctor1.name}${doctor2.name}`);
console.log('\n🎉 数据库种子数据初始化完成!\n');
console.log('📝 测试账号信息:');
console.log(' 邮箱: test@example.com');
console.log(' 密码: password123');
console.log(' 用户ID: user-mock-001\n');
console.log('📝 管理员账号信息:');
console.log(' 邮箱: admin@example.com');
console.log(' 密码: password123');
console.log(' 用户ID: user-admin-001\n');
// 创建租户成员关系
for (const user of [doctor1, doctor2]) {
await prisma.tenant_members.upsert({
where: { tenant_id_user_id: { tenant_id: hospitalTenant.id, user_id: user.id } },
update: {},
create: {
id: uuidv4(),
tenant_id: hospitalTenant.id,
user_id: user.id,
role: UserRole.USER,
},
});
}
// ============================================
// 9. 创建权限数据
// ============================================
console.log('📌 创建权限数据...');
const permissionsData = [
// Prompt管理权限
{ code: 'prompt:view', name: '查看Prompt', description: '查看Prompt列表和历史版本', module: 'prompt' },
{ code: 'prompt:edit', name: '编辑Prompt', description: '创建/修改DRAFT版本', module: 'prompt' },
{ code: 'prompt:debug', name: '调试Prompt', description: '开启调试模式(核心权限)', module: 'prompt' },
{ code: 'prompt:publish', name: '发布Prompt', description: '发布DRAFT为ACTIVE', module: 'prompt' },
// 租户管理权限
{ code: 'tenant:view', name: '查看租户', description: '查看租户列表', module: 'tenant' },
{ code: 'tenant:create', name: '创建租户', description: '创建新租户', module: 'tenant' },
{ code: 'tenant:edit', name: '编辑租户', description: '编辑租户信息', module: 'tenant' },
{ code: 'tenant:delete', name: '删除租户', description: '删除租户', module: 'tenant' },
// 用户管理权限
{ code: 'user:view', name: '查看用户', description: '查看用户列表', module: 'user' },
{ code: 'user:create', name: '创建用户', description: '创建新用户', module: 'user' },
{ code: 'user:edit', name: '编辑用户', description: '编辑用户信息', module: 'user' },
{ code: 'user:delete', name: '删除用户', description: '删除用户', module: 'user' },
// 配额管理权限
{ code: 'quota:view', name: '查看配额', description: '查看配额使用情况', module: 'quota' },
{ code: 'quota:allocate', name: '分配配额', description: '分配配额给科室/用户', module: 'quota' },
// 审计日志权限
{ code: 'audit:view', name: '查看审计日志', description: '查看操作审计日志', module: 'audit' },
];
for (const perm of permissionsData) {
await prisma.permissions.upsert({
where: { code: perm.code },
update: {},
create: perm,
});
}
console.log(`${permissionsData.length} 个权限创建成功`);
// ============================================
// 10. 创建角色-权限关联
// ============================================
console.log('📌 创建角色-权限关联...');
const allPermissions = await prisma.permissions.findMany();
const permissionMap = new Map(allPermissions.map(p => [p.code, p.id]));
// SUPER_ADMIN 拥有所有权限
for (const perm of allPermissions) {
await prisma.role_permissions.upsert({
where: { role_permission_id: { role: UserRole.SUPER_ADMIN, permission_id: perm.id } },
update: {},
create: {
role: UserRole.SUPER_ADMIN,
permission_id: perm.id,
},
});
}
// PROMPT_ENGINEER 拥有Prompt相关权限
const promptPermCodes = ['prompt:view', 'prompt:edit', 'prompt:debug', 'prompt:publish'];
for (const code of promptPermCodes) {
const permId = permissionMap.get(code);
if (permId) {
await prisma.role_permissions.upsert({
where: { role_permission_id: { role: UserRole.PROMPT_ENGINEER, permission_id: permId } },
update: {},
create: {
role: UserRole.PROMPT_ENGINEER,
permission_id: permId,
},
});
}
}
// HOSPITAL_ADMIN 拥有用户、配额、审计权限
const hospitalAdminPermCodes = ['user:view', 'user:create', 'user:edit', 'quota:view', 'quota:allocate', 'audit:view'];
for (const code of hospitalAdminPermCodes) {
const permId = permissionMap.get(code);
if (permId) {
await prisma.role_permissions.upsert({
where: { role_permission_id: { role: UserRole.HOSPITAL_ADMIN, permission_id: permId } },
update: {},
create: {
role: UserRole.HOSPITAL_ADMIN,
permission_id: permId,
},
});
}
}
console.log(' ✅ 角色-权限关联创建成功');
// ============================================
// 11. 跳过Prompt模板表尚未创建
// ============================================
console.log('📌 跳过Prompt模板创建capability_schema.prompt_templates 尚未创建)');
// ============================================
// 12. 创建租户模块订阅
// ============================================
console.log('📌 创建租户模块订阅...');
const hospitalModules = ['ASL', 'DC', 'PKB', 'AIA', 'RVW'];
for (const moduleCode of hospitalModules) {
await prisma.tenant_modules.upsert({
where: { tenant_id_module_code: { tenant_id: hospitalTenant.id, module_code: moduleCode } },
update: {},
create: {
id: uuidv4(),
tenant_id: hospitalTenant.id,
module_code: moduleCode,
is_enabled: true,
},
});
}
// 药企只订阅IIT和DC
const pharmaModules = ['IIT', 'DC'];
for (const moduleCode of pharmaModules) {
await prisma.tenant_modules.upsert({
where: { tenant_id_module_code: { tenant_id: pharmaTenant.id, module_code: moduleCode } },
update: {},
create: {
id: uuidv4(),
tenant_id: pharmaTenant.id,
module_code: moduleCode,
is_enabled: true,
},
});
}
console.log(' ✅ 租户模块订阅创建成功');
// ============================================
// 完成
// ============================================
console.log('\n🎉 种子数据创建完成!\n');
console.log('╔════════════════════════════════════════════════════════════╗');
console.log('║ 📝 测试账号信息 ║');
console.log('╠════════════════════════════════════════════════════════════╣');
console.log('║ 🔐 登录方式1手机号 + 验证码 ║');
console.log('║ 🔐 登录方式2手机号 + 密码 ║');
console.log('╠════════════════════════════════════════════════════════════╣');
console.log('║ 【运营管理员】 ║');
console.log('║ 超级管理员: 13800000001 / 123456 ║');
console.log('║ Prompt工程师: 13800000002 / 123456 ║');
console.log('╠════════════════════════════════════════════════════════════╣');
console.log('║ 【医院端 - demo-hospital】 ║');
console.log('║ 医院管理员: 13800138001 / 123456 (张主任) ║');
console.log('║ 普通医生: 13800138002 / 123456 (李医生·心内科) ║');
console.log('║ 普通医生: 13800138003 / 123456 (王医生·神内科) ║');
console.log('╠════════════════════════════════════════════════════════════╣');
console.log('║ 【药企端 - demo-pharma】 ║');
console.log('║ (暂无用户,可通过管理端添加) ║');
console.log('╠════════════════════════════════════════════════════════════╣');
console.log('║ 【个人用户 - public】 ║');
console.log('║ 通用登录入口: /login ║');
console.log('║ 可用模块: PKB, RVW ║');
console.log('╚════════════════════════════════════════════════════════════╝');
console.log('\n📌 租户专属登录URL');
console.log(' - 通用登录: /login');
console.log(' - 医院端: /t/demo-hospital/login');
console.log(' - 药企端: /t/demo-pharma/login');
console.log('\n⚠ 提示:普通用户首次登录会提示修改默认密码');
}
main()
.catch((e) => {
console.error('❌ 初始化种子数据失败:', e);
process.exit(1);
})
.finally(async () => {
.then(async () => {
await prisma.$disconnect();
})
.catch(async (e) => {
console.error('❌ 种子数据创建失败:', e);
await prisma.$disconnect();
process.exit(1);
});

View File

@@ -116,5 +116,6 @@ Write-Host ""

View File

@@ -226,5 +226,6 @@ function extractCodeBlocks(obj, blocks = []) {

View File

@@ -0,0 +1,28 @@
-- 恢复 platform_schema.job_common 表
-- 从备份文件 rds_init_20251224_154529.sql 提取
CREATE TABLE IF NOT EXISTS platform_schema.job_common (
id uuid DEFAULT gen_random_uuid() NOT NULL,
name text NOT NULL,
priority integer DEFAULT 0 NOT NULL,
data jsonb,
state platform_schema.job_state DEFAULT 'created'::platform_schema.job_state NOT NULL,
retry_limit integer DEFAULT 2 NOT NULL,
retry_count integer DEFAULT 0 NOT NULL,
retry_delay integer DEFAULT 0 NOT NULL,
retry_backoff boolean DEFAULT false NOT NULL,
retry_delay_max integer,
expire_seconds integer DEFAULT 900 NOT NULL,
deletion_seconds integer DEFAULT 604800 NOT NULL,
singleton_key text,
singleton_on timestamp without time zone,
start_after timestamp with time zone DEFAULT now() NOT NULL,
created_on timestamp with time zone DEFAULT now() NOT NULL,
started_on timestamp with time zone,
completed_on timestamp with time zone,
keep_until timestamp with time zone DEFAULT (now() + '336:00:00'::interval) NOT NULL,
output jsonb,
dead_letter text,
policy text
);

View File

@@ -0,0 +1,102 @@
-- 恢复 pg-boss 需要的函数
-- 从备份文件 rds_init_20251224_154529.sql 提取
-- 1. create_queue 函数
CREATE OR REPLACE FUNCTION platform_schema.create_queue(queue_name text, options jsonb) RETURNS void
LANGUAGE plpgsql
AS $_$
DECLARE
tablename varchar := CASE WHEN options->>'partition' = 'true'
THEN 'j' || encode(sha224(queue_name::bytea), 'hex')
ELSE 'job_common'
END;
queue_created_on timestamptz;
BEGIN
WITH q as (
INSERT INTO platform_schema.queue (
name,
policy,
retry_limit,
retry_delay,
retry_backoff,
retry_delay_max,
expire_seconds,
retention_seconds,
deletion_seconds,
warning_queued,
dead_letter,
partition,
table_name
)
VALUES (
queue_name,
options->>'policy',
COALESCE((options->>'retryLimit')::int, 2),
COALESCE((options->>'retryDelay')::int, 0),
COALESCE((options->>'retryBackoff')::bool, false),
(options->>'retryDelayMax')::int,
COALESCE((options->>'expireInSeconds')::int, 900),
COALESCE((options->>'retentionSeconds')::int, 1209600),
COALESCE((options->>'deleteAfterSeconds')::int, 604800),
COALESCE((options->>'warningQueueSize')::int, 0),
options->>'deadLetter',
COALESCE((options->>'partition')::bool, false),
tablename
)
ON CONFLICT DO NOTHING
RETURNING created_on
)
SELECT created_on into queue_created_on from q;
IF queue_created_on IS NULL OR options->>'partition' IS DISTINCT FROM 'true' THEN
RETURN;
END IF;
EXECUTE format('CREATE TABLE platform_schema.%I (LIKE platform_schema.job INCLUDING DEFAULTS)', tablename);
EXECUTE format('ALTER TABLE platform_schema.%1$I ADD PRIMARY KEY (name, id)', tablename);
EXECUTE format('ALTER TABLE platform_schema.%1$I ADD CONSTRAINT q_fkey FOREIGN KEY (name) REFERENCES platform_schema.queue (name) ON DELETE RESTRICT DEFERRABLE INITIALLY DEFERRED', tablename);
EXECUTE format('ALTER TABLE platform_schema.%1$I ADD CONSTRAINT dlq_fkey FOREIGN KEY (dead_letter) REFERENCES platform_schema.queue (name) ON DELETE RESTRICT DEFERRABLE INITIALLY DEFERRED', tablename);
EXECUTE format('CREATE INDEX %1$s_i5 ON platform_schema.%1$I (name, start_after) INCLUDE (priority, created_on, id) WHERE state < ''active''', tablename);
EXECUTE format('CREATE UNIQUE INDEX %1$s_i4 ON platform_schema.%1$I (name, singleton_on, COALESCE(singleton_key, '''')) WHERE state <> ''cancelled'' AND singleton_on IS NOT NULL', tablename);
IF options->>'policy' = 'short' THEN
EXECUTE format('CREATE UNIQUE INDEX %1$s_i1 ON platform_schema.%1$I (name, COALESCE(singleton_key, '''')) WHERE state = ''created'' AND policy = ''short''', tablename);
ELSIF options->>'policy' = 'singleton' THEN
EXECUTE format('CREATE UNIQUE INDEX %1$s_i2 ON platform_schema.%1$I (name, COALESCE(singleton_key, '''')) WHERE state = ''active'' AND policy = ''singleton''', tablename);
ELSIF options->>'policy' = 'stately' THEN
EXECUTE format('CREATE UNIQUE INDEX %1$s_i3 ON platform_schema.%1$I (name, state, COALESCE(singleton_key, '''')) WHERE state <= ''active'' AND policy = ''stately''', tablename);
ELSIF options->>'policy' = 'exclusive' THEN
EXECUTE format('CREATE UNIQUE INDEX %1$s_i6 ON platform_schema.%1$I (name, COALESCE(singleton_key, '''')) WHERE state <= ''active'' AND policy = ''exclusive''', tablename);
END IF;
EXECUTE format('ALTER TABLE platform_schema.%I ADD CONSTRAINT cjc CHECK (name=%L)', tablename, queue_name);
EXECUTE format('ALTER TABLE platform_schema.job ATTACH PARTITION platform_schema.%I FOR VALUES IN (%L)', tablename, queue_name);
END;
$_$;
-- 2. delete_queue 函数
CREATE OR REPLACE FUNCTION platform_schema.delete_queue(queue_name text) RETURNS void
LANGUAGE plpgsql
AS $$
DECLARE
v_table varchar;
v_partition bool;
BEGIN
SELECT table_name, partition
FROM platform_schema.queue
WHERE name = queue_name
INTO v_table, v_partition;
IF v_partition THEN
EXECUTE format('DROP TABLE IF EXISTS platform_schema.%I', v_table);
ELSE
EXECUTE format('DELETE FROM platform_schema.%I WHERE name = %L', v_table, queue_name);
END IF;
DELETE FROM platform_schema.queue WHERE name = queue_name;
END;
$$;

View File

@@ -245,5 +245,6 @@ checkDCTables();

View File

@@ -0,0 +1,3 @@
-- Create capability_schema for Prompt Management System
CREATE SCHEMA IF NOT EXISTS capability_schema;

View File

@@ -197,5 +197,6 @@ createAiHistoryTable()

View File

@@ -184,5 +184,6 @@ createToolCTable()

View File

@@ -181,5 +181,6 @@ createToolCTable()

View File

@@ -0,0 +1,160 @@
/**
* RVW模块 Prompt 迁移脚本
*
* 将现有文件Prompt迁移到数据库
*
* 迁移内容:
* 1. RVW_EDITORIAL - 稿约规范性评估 (review_editorial_system.txt)
* 2. RVW_METHODOLOGY - 方法学质量评估 (review_methodology_system.txt)
* 3. RVW_TOPIC_SYSTEM - 选题评估系统提示 (topic_evaluation_system.txt)
* 4. RVW_TOPIC_USER - 选题评估用户模板 (topic_evaluation_user.txt)
*/
import { PrismaClient, PromptStatus } from '@prisma/client';
import * as fs from 'fs';
import * as path from 'path';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const prisma = new PrismaClient();
// 变量提取函数
function extractVariables(content: string): string[] {
const regex = /\{\{(\w+)\}\}/g;
const variables = new Set<string>();
let match;
while ((match = regex.exec(content)) !== null) {
// 排除 Handlebars 控制语句如 #if, /if
if (!match[1].startsWith('#') && !match[1].startsWith('/')) {
variables.add(match[1]);
}
}
return Array.from(variables);
}
// RVW Prompt 配置(只有 2 个)
// 注意topic_evaluation_* 是"选题评估"功能,不属于 RVW 审稿模块
const rvwPrompts = [
{
code: 'RVW_EDITORIAL',
name: '稿约规范性评估',
module: 'RVW',
description: '评估医学稿件是否符合期刊稿约规范包括文题、摘要、参考文献等11项标准。输出JSON格式的评分和建议。',
file: 'review_editorial_system.txt',
modelConfig: { model: 'deepseek-v3', temperature: 0.3 },
},
{
code: 'RVW_METHODOLOGY',
name: '方法学质量评估',
module: 'RVW',
description: '评估医学稿件的科研设计、统计学方法和统计分析质量共20个检查点。输出JSON格式的评分和建议。',
file: 'review_methodology_system.txt',
modelConfig: { model: 'deepseek-v3', temperature: 0.3 },
},
];
async function main() {
console.log('🚀 开始迁移 RVW Prompt 到数据库...\n');
const promptsDir = path.join(__dirname, '..', 'prompts');
for (const prompt of rvwPrompts) {
console.log(`📄 处理: ${prompt.code} (${prompt.name})`);
// 读取文件内容
const filePath = path.join(promptsDir, prompt.file);
if (!fs.existsSync(filePath)) {
console.log(` ⚠️ 文件不存在: ${filePath}`);
continue;
}
const content = fs.readFileSync(filePath, 'utf-8').trim();
const variables = extractVariables(content);
console.log(` 📝 内容长度: ${content.length} 字符`);
console.log(` 🔤 提取变量: [${variables.join(', ')}]`);
// 创建或更新模板
const template = await prisma.prompt_templates.upsert({
where: { code: prompt.code },
update: {
name: prompt.name,
description: prompt.description,
variables: variables.length > 0 ? variables : null,
updated_at: new Date(),
},
create: {
code: prompt.code,
name: prompt.name,
module: prompt.module,
description: prompt.description,
variables: variables.length > 0 ? variables : null,
},
});
// 检查是否已有 ACTIVE 版本
const existingActive = await prisma.prompt_versions.findFirst({
where: {
template_id: template.id,
status: PromptStatus.ACTIVE,
},
});
if (existingActive) {
console.log(` ✅ 已存在 ACTIVE 版本 (v${existingActive.version})`);
} else {
// 创建第一个 ACTIVE 版本
await prisma.prompt_versions.create({
data: {
template_id: template.id,
version: 1,
content: content,
model_config: prompt.modelConfig,
status: PromptStatus.ACTIVE,
changelog: '从文件迁移的初始版本',
created_by: 'system-migration',
},
});
console.log(` ✅ 创建 ACTIVE 版本 (v1)`);
}
console.log('');
}
// 验证结果
console.log('═══════════════════════════════════════════════════════');
console.log('📊 迁移结果验证\n');
const templates = await prisma.prompt_templates.findMany({
where: { module: 'RVW' },
include: {
versions: {
orderBy: { version: 'desc' },
take: 1,
},
},
});
console.log(`✅ 共迁移 ${templates.length} 个 RVW Prompt:\n`);
for (const t of templates) {
const latestVersion = t.versions[0];
console.log(` 📋 ${t.code}`);
console.log(` 名称: ${t.name}`);
console.log(` 变量: ${t.variables ? JSON.stringify(t.variables) : '无'}`);
console.log(` 最新版本: v${latestVersion?.version} (${latestVersion?.status})`);
console.log('');
}
console.log('✅ RVW Prompt 迁移完成!');
}
main()
.catch((error) => {
console.error('❌ 迁移失败:', error);
process.exit(1);
})
.finally(() => prisma.$disconnect());

View File

@@ -0,0 +1,113 @@
/**
* Prompt管理系统初始化脚本
*
* 功能:
* 1. 创建 capability_schema
* 2. 添加 prompt:* 权限
* 3. 更新角色权限分配
*/
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
async function main() {
console.log('🚀 开始初始化 Prompt 管理系统...\n');
// 1. 创建 capability_schema
console.log('📁 Step 1: 创建 capability_schema...');
try {
await prisma.$executeRaw`CREATE SCHEMA IF NOT EXISTS capability_schema`;
console.log(' ✅ capability_schema 创建成功\n');
} catch (error) {
console.log(' ⚠️ capability_schema 可能已存在\n');
}
// 2. 添加 prompt:* 权限
console.log('🔐 Step 2: 添加 prompt:* 权限...');
const promptPermissions = [
{ code: 'prompt:view', name: '查看Prompt', description: '查看Prompt模板列表和详情', module: 'admin' },
{ code: 'prompt:edit', name: '编辑Prompt', description: '创建和修改Prompt草稿', module: 'admin' },
{ code: 'prompt:debug', name: '调试Prompt', description: '开启调试模式,在生产环境测试草稿', module: 'admin' },
{ code: 'prompt:publish', name: '发布Prompt', description: '将草稿发布为正式版', module: 'admin' },
];
for (const perm of promptPermissions) {
try {
await prisma.permissions.upsert({
where: { code: perm.code },
update: { name: perm.name, description: perm.description, module: perm.module },
create: perm,
});
console.log(`${perm.code}`);
} catch (error) {
console.log(` ⚠️ ${perm.code} 添加失败:`, error);
}
}
console.log('');
// 3. 获取权限ID
console.log('🔗 Step 3: 更新角色权限分配...');
const permissions = await prisma.permissions.findMany({
where: { code: { startsWith: 'prompt:' } },
});
const permissionMap = new Map(permissions.map(p => [p.code, p.id]));
// SUPER_ADMIN: 全部权限
const superAdminPermissions = ['prompt:view', 'prompt:edit', 'prompt:debug', 'prompt:publish'];
for (const permCode of superAdminPermissions) {
const permId = permissionMap.get(permCode);
if (permId) {
try {
await prisma.role_permissions.upsert({
where: {
role_permission_id: { role: 'SUPER_ADMIN', permission_id: permId },
},
update: {},
create: { role: 'SUPER_ADMIN', permission_id: permId },
});
} catch (error) {
// 可能已存在
}
}
}
console.log(' ✅ SUPER_ADMIN: prompt:view, prompt:edit, prompt:debug, prompt:publish');
// PROMPT_ENGINEER: 无 publish 权限
const promptEngineerPermissions = ['prompt:view', 'prompt:edit', 'prompt:debug'];
for (const permCode of promptEngineerPermissions) {
const permId = permissionMap.get(permCode);
if (permId) {
try {
await prisma.role_permissions.upsert({
where: {
role_permission_id: { role: 'PROMPT_ENGINEER', permission_id: permId },
},
update: {},
create: { role: 'PROMPT_ENGINEER', permission_id: permId },
});
} catch (error) {
// 可能已存在
}
}
}
console.log(' ✅ PROMPT_ENGINEER: prompt:view, prompt:edit, prompt:debug (无publish)');
console.log('');
// 4. 验证
console.log('✅ Prompt 管理系统初始化完成!\n');
const allPermissions = await prisma.permissions.findMany({
where: { code: { startsWith: 'prompt:' } },
});
console.log('📋 已添加的权限:');
allPermissions.forEach(p => console.log(` - ${p.code}: ${p.name}`));
}
main()
.catch(console.error)
.finally(() => prisma.$disconnect());

View File

@@ -330,3 +330,4 @@ runTests().catch(error => {

View File

@@ -0,0 +1,79 @@
/**
* 测试 Prompt 管理 API
*
* 启动后端后运行: npx tsx scripts/test-prompt-api.ts
*/
const BASE_URL = 'http://localhost:3001/api/admin/prompts';
async function testAPI() {
console.log('🧪 测试 Prompt 管理 API...\n');
// 1. 获取列表
console.log('═══════════════════════════════════════════════════════');
console.log('📋 Test 1: GET /api/admin/prompts\n');
const listRes = await fetch(BASE_URL);
const listData = await listRes.json();
console.log(` 状态: ${listRes.status}`);
console.log(` 总数: ${listData.total}`);
console.log(` Prompts:`);
for (const p of listData.data || []) {
console.log(` - ${p.code} (${p.name}) v${p.latestVersion?.version || 0}`);
}
// 2. 获取详情
console.log('\n═══════════════════════════════════════════════════════');
console.log('📋 Test 2: GET /api/admin/prompts/RVW_EDITORIAL\n');
const detailRes = await fetch(`${BASE_URL}/RVW_EDITORIAL`);
const detailData = await detailRes.json();
console.log(` 状态: ${detailRes.status}`);
console.log(` Code: ${detailData.data?.code}`);
console.log(` Name: ${detailData.data?.name}`);
console.log(` 版本数: ${detailData.data?.versions?.length || 0}`);
// 3. 测试渲染
console.log('\n═══════════════════════════════════════════════════════');
console.log('📋 Test 3: POST /api/admin/prompts/test-render\n');
const renderRes = await fetch(`${BASE_URL}/test-render`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
content: '你好,{{name}}!请评估标题:{{title}}',
variables: { name: '张三', title: '测试标题' },
}),
});
const renderData = await renderRes.json();
console.log(` 状态: ${renderRes.status}`);
console.log(` 渲染结果: ${renderData.data?.rendered}`);
console.log(` 提取变量: [${renderData.data?.extractedVariables?.join(', ')}]`);
console.log(` 校验结果: ${renderData.data?.validation?.isValid ? '✅ 通过' : '❌ 缺少变量'}`);
// 4. 按模块筛选
console.log('\n═══════════════════════════════════════════════════════');
console.log('📋 Test 4: GET /api/admin/prompts?module=RVW\n');
const rvwRes = await fetch(`${BASE_URL}?module=RVW`);
const rvwData = await rvwRes.json();
console.log(` 状态: ${rvwRes.status}`);
console.log(` RVW模块Prompt数: ${rvwData.total}`);
for (const p of rvwData.data || []) {
console.log(` - ${p.code}`);
}
// 5. 获取调试状态无认证预期返回401
console.log('\n═══════════════════════════════════════════════════════');
console.log('📋 Test 5: GET /api/admin/prompts/debug (无认证)\n');
const debugRes = await fetch(`${BASE_URL}/debug`);
const debugData = await debugRes.json();
console.log(` 状态: ${debugRes.status}`);
console.log(` 响应: ${JSON.stringify(debugData)}`);
console.log('\n✅ API 测试完成!');
}
testAPI().catch(console.error);

View File

@@ -0,0 +1,108 @@
/**
* 测试 PromptService
*/
import { PrismaClient } from '@prisma/client';
import { getPromptService } from '../src/common/prompt/index.js';
const prisma = new PrismaClient();
async function main() {
console.log('🧪 测试 PromptService...\n');
const promptService = getPromptService(prisma);
// 1. 测试获取 ACTIVE Prompt
console.log('═══════════════════════════════════════════════════════');
console.log('📋 Test 1: 获取 ACTIVE Prompt (RVW_EDITORIAL)\n');
const editorial = await promptService.get('RVW_EDITORIAL');
console.log(` 版本: v${editorial.version}`);
console.log(` 是否草稿: ${editorial.isDraft}`);
console.log(` 模型配置: ${JSON.stringify(editorial.modelConfig)}`);
console.log(` 内容长度: ${editorial.content.length} 字符`);
console.log(` 内容预览: ${editorial.content.substring(0, 100)}...`);
// 2. 测试变量提取
console.log('\n═══════════════════════════════════════════════════════');
console.log('📋 Test 2: 变量提取\n');
const template = '请评估以下稿件:{{title}}\n作者{{author}}\n{{#if abstract}}摘要:{{abstract}}{{/if}}';
const variables = promptService.extractVariables(template);
console.log(` 模板: ${template}`);
console.log(` 提取的变量: [${variables.join(', ')}]`);
// 3. 测试变量校验
console.log('\n═══════════════════════════════════════════════════════');
console.log('📋 Test 3: 变量校验\n');
const validation = promptService.validateVariables(template, { title: '测试标题' });
console.log(` 有效: ${validation.isValid}`);
console.log(` 缺失变量: [${validation.missingVariables.join(', ')}]`);
console.log(` 多余变量: [${validation.extraVariables.join(', ')}]`);
// 4. 测试模板渲染
console.log('\n═══════════════════════════════════════════════════════');
console.log('📋 Test 4: 模板渲染\n');
const rendered = promptService.render(template, {
title: '测试论文标题',
author: '张三',
abstract: '这是摘要内容',
});
console.log(` 渲染结果:\n${rendered}`);
// 5. 测试调试模式
console.log('\n═══════════════════════════════════════════════════════');
console.log('📋 Test 5: 调试模式\n');
const testUserId = 'test-user-123';
console.log(` 设置调试模式: userId=${testUserId}, modules=[RVW]`);
promptService.setDebugMode(testUserId, ['RVW'], true);
const isDebugging = promptService.isDebugging(testUserId, 'RVW_EDITORIAL');
console.log(` 是否在调试 RVW_EDITORIAL: ${isDebugging}`);
const isDebuggingASL = promptService.isDebugging(testUserId, 'ASL_SCREENING');
console.log(` 是否在调试 ASL_SCREENING: ${isDebuggingASL}`);
const debugState = promptService.getDebugState(testUserId);
console.log(` 调试状态: modules=[${Array.from(debugState?.modules || []).join(', ')}]`);
console.log(` 关闭调试模式`);
promptService.setDebugMode(testUserId, [], false);
// 6. 测试列表模板
console.log('\n═══════════════════════════════════════════════════════');
console.log('📋 Test 6: 列表所有模板\n');
const templates = await promptService.listTemplates();
console.log(`${templates.length} 个模板:`);
for (const t of templates) {
const latest = t.versions[0];
console.log(` - ${t.code} (${t.name}) - v${latest?.version || 0} ${latest?.status || 'N/A'}`);
}
// 7. 测试兜底 Prompt
console.log('\n═══════════════════════════════════════════════════════');
console.log('📋 Test 7: 兜底 Prompt\n');
try {
// 测试不存在且无兜底的 Prompt应该抛错
await promptService.get('NON_EXISTENT_CODE');
console.log(' ❌ 应该抛出错误但没有');
} catch (e) {
console.log(' ✅ 正确抛出错误不存在的Prompt且无兜底');
}
// 测试有兜底的 ASL_SCREENING (虽然DB里有但演示兜底机制)
console.log(' 兜底Prompt列表: RVW_EDITORIAL, RVW_METHODOLOGY, ASL_SCREENING');
console.log('\n✅ 所有测试完成!');
}
main()
.catch(console.error)
.finally(() => prisma.$disconnect());

View File

@@ -295,3 +295,4 @@ verifySchemas()

View File

@@ -0,0 +1,235 @@
/**
* Auth Controller
*
* 认证相关的 HTTP 请求处理
*/
import { FastifyRequest, FastifyReply } from 'fastify';
import { authService } from './auth.service.js';
import type { PasswordLoginRequest, VerificationCodeLoginRequest, ChangePasswordRequest } from './auth.service.js';
import { logger } from '../logging/index.js';
/**
* 密码登录
*
* POST /api/v1/auth/login/password
*/
export async function loginWithPassword(
request: FastifyRequest<{ Body: PasswordLoginRequest }>,
reply: FastifyReply
) {
try {
const result = await authService.loginWithPassword(request.body);
return reply.status(200).send({
success: true,
data: result,
});
} catch (error) {
const message = error instanceof Error ? error.message : '登录失败';
logger.warn('登录失败', { error: message, phone: request.body.phone });
return reply.status(401).send({
success: false,
error: 'Unauthorized',
message,
});
}
}
/**
* 验证码登录
*
* POST /api/v1/auth/login/code
*/
export async function loginWithVerificationCode(
request: FastifyRequest<{ Body: VerificationCodeLoginRequest }>,
reply: FastifyReply
) {
try {
const result = await authService.loginWithVerificationCode(request.body);
return reply.status(200).send({
success: true,
data: result,
});
} catch (error) {
const message = error instanceof Error ? error.message : '登录失败';
logger.warn('验证码登录失败', { error: message, phone: request.body.phone });
return reply.status(401).send({
success: false,
error: 'Unauthorized',
message,
});
}
}
/**
* 发送验证码
*
* POST /api/v1/auth/verification-code
*/
export async function sendVerificationCode(
request: FastifyRequest<{ Body: { phone: string; type?: 'LOGIN' | 'RESET_PASSWORD' } }>,
reply: FastifyReply
) {
try {
const { phone, type = 'LOGIN' } = request.body;
const result = await authService.sendVerificationCode(phone, type);
return reply.status(200).send({
success: true,
data: {
message: '验证码已发送',
expiresIn: result.expiresIn,
},
});
} catch (error) {
const message = error instanceof Error ? error.message : '发送失败';
logger.warn('发送验证码失败', { error: message, phone: request.body.phone });
return reply.status(400).send({
success: false,
error: 'BadRequest',
message,
});
}
}
/**
* 获取当前用户信息
*
* GET /api/v1/auth/me
*/
export async function getCurrentUser(
request: FastifyRequest,
reply: FastifyReply
) {
try {
if (!request.user) {
return reply.status(401).send({
success: false,
error: 'Unauthorized',
message: '未认证',
});
}
const user = await authService.getCurrentUser(request.user.userId);
return reply.status(200).send({
success: true,
data: user,
});
} catch (error) {
const message = error instanceof Error ? error.message : '获取用户信息失败';
logger.error('获取用户信息失败', { error: message, userId: request.user?.userId });
return reply.status(500).send({
success: false,
error: 'InternalServerError',
message,
});
}
}
/**
* 修改密码
*
* POST /api/v1/auth/change-password
*/
export async function changePassword(
request: FastifyRequest<{ Body: ChangePasswordRequest }>,
reply: FastifyReply
) {
try {
if (!request.user) {
return reply.status(401).send({
success: false,
error: 'Unauthorized',
message: '未认证',
});
}
await authService.changePassword(request.user.userId, request.body);
return reply.status(200).send({
success: true,
data: {
message: '密码修改成功',
},
});
} catch (error) {
const message = error instanceof Error ? error.message : '修改密码失败';
logger.warn('修改密码失败', { error: message, userId: request.user?.userId });
return reply.status(400).send({
success: false,
error: 'BadRequest',
message,
});
}
}
/**
* 刷新 Token
*
* POST /api/v1/auth/refresh
*/
export async function refreshToken(
request: FastifyRequest<{ Body: { refreshToken: string } }>,
reply: FastifyReply
) {
try {
const { refreshToken } = request.body;
if (!refreshToken) {
return reply.status(400).send({
success: false,
error: 'BadRequest',
message: '请提供 refreshToken',
});
}
const tokens = await authService.refreshToken(refreshToken);
return reply.status(200).send({
success: true,
data: tokens,
});
} catch (error) {
const message = error instanceof Error ? error.message : '刷新Token失败';
logger.warn('刷新Token失败', { error: message });
return reply.status(401).send({
success: false,
error: 'Unauthorized',
message,
});
}
}
/**
* 登出
*
* POST /api/v1/auth/logout
*
* 注意JWT 是无状态的登出主要是前端清除Token
* 如果需要服务端登出,需要维护 Token 黑名单
*/
export async function logout(
request: FastifyRequest,
reply: FastifyReply
) {
// TODO: 如果需要服务端登出可以将Token加入黑名单
logger.info('用户登出', { userId: request.user?.userId });
return reply.status(200).send({
success: true,
data: {
message: '登出成功',
},
});
}

View File

@@ -0,0 +1,256 @@
/**
* Auth Middleware
*
* Fastify 认证中间件
*
* 功能:
* - 验证 JWT Token
* - 注入用户信息到请求
* - 权限检查
*/
import { FastifyRequest, FastifyReply, FastifyInstance, preHandlerHookHandler } from 'fastify';
import { jwtService } from './jwt.service.js';
import type { DecodedToken } from './jwt.service.js';
import { logger } from '../logging/index.js';
/**
* 扩展 Fastify Request 类型
*/
declare module 'fastify' {
interface FastifyRequest {
user?: DecodedToken;
}
}
/**
* 认证错误类
*/
export class AuthenticationError extends Error {
public statusCode: number;
constructor(message: string, statusCode: number = 401) {
super(message);
this.name = 'AuthenticationError';
this.statusCode = statusCode;
}
}
/**
* 授权错误类
*/
export class AuthorizationError extends Error {
public statusCode: number;
constructor(message: string) {
super(message);
this.name = 'AuthorizationError';
this.statusCode = 403;
}
}
/**
* 认证中间件
*
* 验证 JWT Token 并注入用户信息
*/
export const authenticate: preHandlerHookHandler = async (
request: FastifyRequest,
reply: FastifyReply
) => {
try {
// 1. 获取 Authorization Header
const authHeader = request.headers.authorization;
const token = jwtService.extractTokenFromHeader(authHeader);
if (!token) {
throw new AuthenticationError('未提供认证令牌');
}
// 2. 验证 Token
const decoded = jwtService.verifyToken(token);
// 3. 注入用户信息
request.user = decoded;
logger.debug('认证成功', {
userId: decoded.userId,
role: decoded.role,
tenantId: decoded.tenantId,
});
} catch (error) {
if (error instanceof AuthenticationError) {
return reply.status(error.statusCode).send({
error: 'Unauthorized',
message: error.message,
});
}
if (error instanceof Error) {
return reply.status(401).send({
error: 'Unauthorized',
message: error.message,
});
}
return reply.status(401).send({
error: 'Unauthorized',
message: '认证失败',
});
}
};
/**
* 可选认证中间件
*
* 如果有 Token 则验证,没有也放行
*/
export const optionalAuthenticate: preHandlerHookHandler = async (
request: FastifyRequest,
_reply: FastifyReply
) => {
try {
const authHeader = request.headers.authorization;
const token = jwtService.extractTokenFromHeader(authHeader);
if (token) {
const decoded = jwtService.verifyToken(token);
request.user = decoded;
}
} catch (error) {
// 可选认证,忽略错误
logger.debug('可选认证Token无效或已过期');
}
};
/**
* 角色检查中间件工厂
*
* @param allowedRoles 允许的角色列表
*/
export function requireRoles(...allowedRoles: string[]): preHandlerHookHandler {
return async (request: FastifyRequest, reply: FastifyReply) => {
if (!request.user) {
return reply.status(401).send({
error: 'Unauthorized',
message: '未认证',
});
}
if (!allowedRoles.includes(request.user.role)) {
logger.warn('权限不足', {
userId: request.user.userId,
role: request.user.role,
requiredRoles: allowedRoles,
});
return reply.status(403).send({
error: 'Forbidden',
message: '权限不足',
});
}
};
}
/**
* 权限检查中间件工厂
*
* @param requiredPermission 需要的权限code
*/
export function requirePermission(requiredPermission: string): preHandlerHookHandler {
return async (request: FastifyRequest, reply: FastifyReply) => {
if (!request.user) {
return reply.status(401).send({
error: 'Unauthorized',
message: '未认证',
});
}
// TODO: 从缓存或数据库获取用户权限
// 目前简化处理:超级管理员拥有所有权限
if (request.user.role === 'SUPER_ADMIN') {
return;
}
// TODO: 实现权限检查逻辑
// const hasPermission = await checkUserPermission(request.user.userId, requiredPermission);
// if (!hasPermission) {
// return reply.status(403).send({
// error: 'Forbidden',
// message: `需要权限: ${requiredPermission}`,
// });
// }
};
}
/**
* 租户检查中间件
*
* 确保用户只能访问自己租户的数据
*/
export const requireSameTenant: preHandlerHookHandler = async (
request: FastifyRequest,
reply: FastifyReply
) => {
if (!request.user) {
return reply.status(401).send({
error: 'Unauthorized',
message: '未认证',
});
}
// 超级管理员可以访问所有租户
if (request.user.role === 'SUPER_ADMIN') {
return;
}
// 从请求参数或body中获取tenantId
const requestTenantId =
(request.params as any)?.tenantId ||
(request.body as any)?.tenantId ||
(request.query as any)?.tenantId;
if (requestTenantId && requestTenantId !== request.user.tenantId) {
logger.warn('租户不匹配', {
userId: request.user.userId,
userTenantId: request.user.tenantId,
requestTenantId,
});
return reply.status(403).send({
error: 'Forbidden',
message: '无权访问此租户数据',
});
}
};
/**
* 注册认证插件到 Fastify
*/
export async function registerAuthPlugin(fastify: FastifyInstance): Promise<void> {
// 添加 decorate 以支持 request.user
fastify.decorateRequest('user', undefined);
// 注册全局错误处理
fastify.setErrorHandler((error, request, reply) => {
if (error instanceof AuthenticationError) {
return reply.status(error.statusCode).send({
error: 'Unauthorized',
message: error.message,
});
}
if (error instanceof AuthorizationError) {
return reply.status(error.statusCode).send({
error: 'Forbidden',
message: error.message,
});
}
// 其他错误交给默认处理
throw error;
});
logger.info('✅ 认证插件已注册');
}

View File

@@ -0,0 +1,140 @@
/**
* Auth Routes
*
* 认证相关路由定义
*/
import { FastifyInstance, FastifyPluginOptions } from 'fastify';
import {
loginWithPassword,
loginWithVerificationCode,
sendVerificationCode,
getCurrentUser,
changePassword,
refreshToken,
logout,
} from './auth.controller.js';
import { authenticate } from './auth.middleware.js';
/**
* 登录请求 Schema
*/
const passwordLoginSchema = {
body: {
type: 'object',
required: ['phone', 'password'],
properties: {
phone: { type: 'string', pattern: '^1[3-9]\\d{9}$', description: '手机号' },
password: { type: 'string', minLength: 6, description: '密码' },
},
},
};
const codeLoginSchema = {
body: {
type: 'object',
required: ['phone', 'code'],
properties: {
phone: { type: 'string', pattern: '^1[3-9]\\d{9}$', description: '手机号' },
code: { type: 'string', minLength: 6, maxLength: 6, description: '验证码' },
},
},
};
const sendCodeSchema = {
body: {
type: 'object',
required: ['phone'],
properties: {
phone: { type: 'string', pattern: '^1[3-9]\\d{9}$', description: '手机号' },
type: { type: 'string', enum: ['LOGIN', 'RESET_PASSWORD'], default: 'LOGIN' },
},
},
};
const changePasswordSchema = {
body: {
type: 'object',
required: ['newPassword', 'confirmPassword'],
properties: {
oldPassword: { type: 'string', description: '原密码(可选,验证码修改时不需要)' },
newPassword: { type: 'string', minLength: 6, description: '新密码' },
confirmPassword: { type: 'string', minLength: 6, description: '确认密码' },
},
},
};
const refreshTokenSchema = {
body: {
type: 'object',
required: ['refreshToken'],
properties: {
refreshToken: { type: 'string', description: 'Refresh Token' },
},
},
};
/**
* 注册认证路由
*/
export async function authRoutes(
fastify: FastifyInstance,
_options: FastifyPluginOptions
): Promise<void> {
// ========== 公开路由(无需认证)==========
/**
* 密码登录
*/
fastify.post('/login/password', {
schema: passwordLoginSchema,
}, loginWithPassword);
/**
* 验证码登录
*/
fastify.post('/login/code', {
schema: codeLoginSchema,
}, loginWithVerificationCode);
/**
* 发送验证码
*/
fastify.post('/verification-code', {
schema: sendCodeSchema,
}, sendVerificationCode);
/**
* 刷新 Token
*/
fastify.post('/refresh', {
schema: refreshTokenSchema,
}, refreshToken);
// ========== 需要认证的路由 ==========
/**
* 获取当前用户信息
*/
fastify.get('/me', {
preHandler: [authenticate],
}, getCurrentUser);
/**
* 修改密码
*/
fastify.post('/change-password', {
preHandler: [authenticate],
schema: changePasswordSchema,
}, changePassword as any);
/**
* 登出
*/
fastify.post('/logout', {
preHandler: [authenticate],
}, logout);
}
export default authRoutes;

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();

View File

@@ -0,0 +1,35 @@
/**
* Auth Module
*
* 认证模块导出
*/
// JWT Service
export { jwtService, JWTService } from './jwt.service.js';
export type { JWTPayload, TokenResponse, DecodedToken } from './jwt.service.js';
// Auth Service
export { authService, AuthService } from './auth.service.js';
export type {
PasswordLoginRequest,
VerificationCodeLoginRequest,
ChangePasswordRequest,
UserInfoResponse,
LoginResponse,
} from './auth.service.js';
// Auth Middleware
export {
authenticate,
optionalAuthenticate,
requireRoles,
requirePermission,
requireSameTenant,
registerAuthPlugin,
AuthenticationError,
AuthorizationError,
} from './auth.middleware.js';
// Auth Routes
export { authRoutes } from './auth.routes.js';

View File

@@ -0,0 +1,186 @@
/**
* JWT Service
*
* JWT令牌的生成、验证和刷新
*
* 设计原则:
* - 使用 jsonwebtoken 库
* - Token payload 包含用户ID、角色、租户ID
* - 支持 Access Token 和 Refresh Token
*/
import jwt, { SignOptions, JwtPayload } from 'jsonwebtoken';
import { config } from '../../config/env.js';
/**
* JWT Payload 接口
*/
export interface JWTPayload {
/** 用户ID */
userId: string;
/** 用户手机号 */
phone: string;
/** 用户角色 */
role: string;
/** 租户ID */
tenantId: string;
/** 租户Code用于URL路由 */
tenantCode?: string;
/** 是否为默认密码 */
isDefaultPassword?: boolean;
}
/**
* Token 响应接口
*/
export interface TokenResponse {
accessToken: string;
refreshToken: string;
expiresIn: number; // 秒
tokenType: 'Bearer';
}
/**
* 解码后的 Token 接口
*/
export interface DecodedToken extends JWTPayload {
iat: number; // 签发时间
exp: number; // 过期时间
}
// Token 配置
const ACCESS_TOKEN_EXPIRES_IN = '2h'; // Access Token 2小时过期
const REFRESH_TOKEN_EXPIRES_IN = '7d'; // Refresh Token 7天过期
const ACCESS_TOKEN_EXPIRES_SECONDS = 2 * 60 * 60; // 7200秒
/**
* JWT Service 类
*/
export class JWTService {
private readonly secret: string;
constructor() {
this.secret = config.jwtSecret;
if (this.secret === 'your-secret-key-change-in-production' && config.nodeEnv === 'production') {
throw new Error('JWT_SECRET must be configured in production environment');
}
}
/**
* 生成 Access Token
*/
generateAccessToken(payload: JWTPayload): string {
const options: SignOptions = {
expiresIn: ACCESS_TOKEN_EXPIRES_IN,
issuer: 'aiclinical',
subject: payload.userId,
};
return jwt.sign(payload, this.secret, options);
}
/**
* 生成 Refresh Token
*/
generateRefreshToken(payload: JWTPayload): string {
// Refresh Token 只包含必要信息
const refreshPayload = {
userId: payload.userId,
type: 'refresh',
};
const options: SignOptions = {
expiresIn: REFRESH_TOKEN_EXPIRES_IN,
issuer: 'aiclinical',
subject: payload.userId,
};
return jwt.sign(refreshPayload, this.secret, options);
}
/**
* 生成完整的 Token 响应
*/
generateTokens(payload: JWTPayload): TokenResponse {
return {
accessToken: this.generateAccessToken(payload),
refreshToken: this.generateRefreshToken(payload),
expiresIn: ACCESS_TOKEN_EXPIRES_SECONDS,
tokenType: 'Bearer',
};
}
/**
* 验证 Token
*/
verifyToken(token: string): DecodedToken {
try {
const decoded = jwt.verify(token, this.secret, {
issuer: 'aiclinical',
}) as DecodedToken;
return decoded;
} catch (error) {
if (error instanceof jwt.TokenExpiredError) {
throw new Error('Token已过期');
}
if (error instanceof jwt.JsonWebTokenError) {
throw new Error('Token无效');
}
throw error;
}
}
/**
* 刷新 Token
*
* 使用 Refresh Token 获取新的 Access Token
*/
async refreshToken(refreshToken: string, getUserById: (id: string) => Promise<JWTPayload | null>): Promise<TokenResponse> {
// 验证 Refresh Token
const decoded = this.verifyToken(refreshToken);
if ((decoded as any).type !== 'refresh') {
throw new Error('无效的Refresh Token');
}
// 获取用户最新信息
const user = await getUserById(decoded.userId);
if (!user) {
throw new Error('用户不存在');
}
// 生成新的 Tokens
return this.generateTokens(user);
}
/**
* 从 Authorization Header 中提取 Token
*/
extractTokenFromHeader(authHeader: string | undefined): string | null {
if (!authHeader) {
return null;
}
const [type, token] = authHeader.split(' ');
if (type !== 'Bearer' || !token) {
return null;
}
return token;
}
/**
* 解码 Token不验证签名
*
* 用于调试或获取过期token的payload
*/
decodeToken(token: string): DecodedToken | null {
const decoded = jwt.decode(token);
return decoded as DecodedToken | null;
}
}
// 导出单例
export const jwtService = new JWTService();

View File

@@ -313,5 +313,6 @@ export function getBatchItems<T>(

View File

@@ -0,0 +1,33 @@
/**
* Prompt 管理模块导出
*/
// Service
export { PromptService, getPromptService, resetPromptService } from './prompt.service.js';
// Types
export type {
PromptStatus,
ModelConfig,
PromptTemplate,
PromptVersion,
RenderedPrompt,
DebugState,
GetPromptOptions,
VariableValidation,
} from './prompt.types.js';
// Fallbacks
export {
getFallbackPrompt,
hasFallbackPrompt,
getAllFallbackCodes,
FALLBACK_PROMPTS,
} from './prompt.fallbacks.js';
// Routes
export { promptRoutes } from './prompt.routes.js';
// Controller (for testing)
export * from './prompt.controller.js';

View File

@@ -0,0 +1,418 @@
/**
* Prompt 管理 API 控制器
*/
import type { FastifyRequest, FastifyReply } from 'fastify';
import { getPromptService } from './prompt.service.js';
import type { ModelConfig } from './prompt.types.js';
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
// 请求类型定义
interface ListPromptsQuery {
module?: string;
}
interface GetPromptParams {
code: string;
}
interface SaveDraftBody {
content: string;
modelConfig?: ModelConfig;
changelog?: string;
}
interface PublishBody {
// 可扩展
}
interface RollbackBody {
version: number;
}
interface SetDebugModeBody {
modules: string[];
enabled: boolean;
}
interface TestRenderBody {
content: string;
variables: Record<string, any>;
}
/**
* 获取 Prompt 列表
* GET /api/admin/prompts?module=RVW
*/
export async function listPrompts(
request: FastifyRequest<{ Querystring: ListPromptsQuery }>,
reply: FastifyReply
) {
try {
const { module } = request.query;
const promptService = getPromptService(prisma);
const templates = await promptService.listTemplates(module);
// 转换为 API 响应格式
const result = templates.map(t => ({
id: t.id,
code: t.code,
name: t.name,
module: t.module,
description: t.description,
variables: t.variables,
latestVersion: t.versions[0] ? {
version: t.versions[0].version,
status: t.versions[0].status,
createdAt: t.versions[0].created_at,
} : null,
updatedAt: t.updated_at,
}));
return reply.send({
success: true,
data: result,
total: result.length,
});
} catch (error) {
console.error('[PromptController] listPrompts error:', error);
return reply.status(500).send({
success: false,
error: 'Failed to list prompts',
});
}
}
/**
* 获取 Prompt 详情(含版本历史)
* GET /api/admin/prompts/:code
*/
export async function getPromptDetail(
request: FastifyRequest<{ Params: GetPromptParams }>,
reply: FastifyReply
) {
try {
const { code } = request.params;
const promptService = getPromptService(prisma);
const template = await promptService.getTemplateDetail(code);
if (!template) {
return reply.status(404).send({
success: false,
error: 'Prompt not found',
});
}
// 转换版本历史
const versions = template.versions.map(v => ({
id: v.id,
version: v.version,
status: v.status,
content: v.content,
modelConfig: v.model_config,
changelog: v.changelog,
createdBy: v.created_by,
createdAt: v.created_at,
}));
return reply.send({
success: true,
data: {
id: template.id,
code: template.code,
name: template.name,
module: template.module,
description: template.description,
variables: template.variables,
versions,
createdAt: template.created_at,
updatedAt: template.updated_at,
},
});
} catch (error) {
console.error('[PromptController] getPromptDetail error:', error);
return reply.status(500).send({
success: false,
error: 'Failed to get prompt detail',
});
}
}
/**
* 保存草稿
* POST /api/admin/prompts/:code/draft
*
* 需要权限: prompt:edit
*/
export async function saveDraft(
request: FastifyRequest<{ Params: GetPromptParams; Body: SaveDraftBody }>,
reply: FastifyReply
) {
try {
const { code } = request.params;
const { content, modelConfig, changelog } = request.body;
const userId = (request as any).user?.id;
const promptService = getPromptService(prisma);
// 保存草稿
const draft = await promptService.saveDraft(
code,
content,
modelConfig,
changelog,
userId
);
// 提取变量信息
const variables = promptService.extractVariables(content);
return reply.send({
success: true,
data: {
id: draft.id,
version: draft.version,
status: draft.status,
variables,
message: 'Draft saved successfully',
},
});
} catch (error: any) {
console.error('[PromptController] saveDraft error:', error);
return reply.status(400).send({
success: false,
error: error.message || 'Failed to save draft',
});
}
}
/**
* 发布 Prompt
* POST /api/admin/prompts/:code/publish
*
* 需要权限: prompt:publish
*/
export async function publishPrompt(
request: FastifyRequest<{ Params: GetPromptParams; Body: PublishBody }>,
reply: FastifyReply
) {
try {
const { code } = request.params;
const userId = (request as any).user?.id;
const promptService = getPromptService(prisma);
const published = await promptService.publish(code, userId);
return reply.send({
success: true,
data: {
version: published.version,
status: 'ACTIVE',
message: `Prompt ${code} published successfully (v${published.version})`,
},
});
} catch (error: any) {
console.error('[PromptController] publishPrompt error:', error);
return reply.status(400).send({
success: false,
error: error.message || 'Failed to publish prompt',
});
}
}
/**
* 回滚到指定版本
* POST /api/admin/prompts/:code/rollback
*
* 需要权限: prompt:publish
*/
export async function rollbackPrompt(
request: FastifyRequest<{ Params: GetPromptParams; Body: RollbackBody }>,
reply: FastifyReply
) {
try {
const { code } = request.params;
const { version } = request.body;
const userId = (request as any).user?.id;
const promptService = getPromptService(prisma);
const rolled = await promptService.rollback(code, version, userId);
return reply.send({
success: true,
data: {
version: rolled.version,
status: 'ACTIVE',
message: `Prompt ${code} rolled back to v${version}`,
},
});
} catch (error: any) {
console.error('[PromptController] rollbackPrompt error:', error);
return reply.status(400).send({
success: false,
error: error.message || 'Failed to rollback prompt',
});
}
}
/**
* 设置调试模式
* POST /api/admin/prompts/debug
*
* 需要权限: prompt:debug
*/
export async function setDebugMode(
request: FastifyRequest<{ Body: SetDebugModeBody }>,
reply: FastifyReply
) {
try {
const { modules, enabled } = request.body;
const userId = (request as any).user?.id;
if (!userId) {
return reply.status(401).send({
success: false,
error: 'User not authenticated',
});
}
const promptService = getPromptService(prisma);
promptService.setDebugMode(userId, modules, enabled);
const state = promptService.getDebugState(userId);
return reply.send({
success: true,
data: {
userId,
modules: state ? Array.from(state.modules) : [],
enabled,
message: enabled
? `Debug mode enabled for modules: [${modules.join(', ')}]`
: 'Debug mode disabled',
},
});
} catch (error: any) {
console.error('[PromptController] setDebugMode error:', error);
return reply.status(500).send({
success: false,
error: error.message || 'Failed to set debug mode',
});
}
}
/**
* 获取当前用户的调试状态
* GET /api/admin/prompts/debug
*/
export async function getDebugStatus(
request: FastifyRequest,
reply: FastifyReply
) {
try {
const userId = (request as any).user?.id;
if (!userId) {
return reply.status(401).send({
success: false,
error: 'User not authenticated',
});
}
const promptService = getPromptService(prisma);
const state = promptService.getDebugState(userId);
return reply.send({
success: true,
data: {
userId,
isDebugging: !!state,
modules: state ? Array.from(state.modules) : [],
enabledAt: state?.enabledAt,
},
});
} catch (error: any) {
console.error('[PromptController] getDebugStatus error:', error);
return reply.status(500).send({
success: false,
error: error.message || 'Failed to get debug status',
});
}
}
/**
* 测试渲染 Prompt
* POST /api/admin/prompts/test-render
*/
export async function testRender(
request: FastifyRequest<{ Body: TestRenderBody }>,
reply: FastifyReply
) {
try {
const { content, variables } = request.body;
const promptService = getPromptService(prisma);
// 提取变量
const extractedVars = promptService.extractVariables(content);
// 校验变量
const validation = promptService.validateVariables(content, variables);
// 渲染
const rendered = promptService.render(content, variables);
return reply.send({
success: true,
data: {
rendered,
extractedVariables: extractedVars,
validation,
},
});
} catch (error: any) {
console.error('[PromptController] testRender error:', error);
return reply.status(400).send({
success: false,
error: error.message || 'Failed to render prompt',
});
}
}
/**
* 清除缓存
* POST /api/admin/prompts/:code/invalidate-cache
*/
export async function invalidateCache(
request: FastifyRequest<{ Params: GetPromptParams }>,
reply: FastifyReply
) {
try {
const { code } = request.params;
const promptService = getPromptService(prisma);
promptService.invalidateCache(code);
return reply.send({
success: true,
data: {
code,
message: `Cache invalidated for ${code}`,
},
});
} catch (error: any) {
console.error('[PromptController] invalidateCache error:', error);
return reply.status(500).send({
success: false,
error: error.message || 'Failed to invalidate cache',
});
}
}

View File

@@ -0,0 +1,100 @@
/**
* 兜底 PromptHardcoded Fallbacks
*
* 三级容灾机制的最后一道防线:
* 1. 正常:从数据库获取 ACTIVE 版本
* 2. 缓存:数据库不可用时使用缓存
* 3. 兜底:缓存也失效时使用这里的 hardcoded 版本
*
* ⚠️ 注意:这里的 Prompt 是最基础版本,仅保证系统不崩溃
* 实际生产环境应该始终使用数据库中的版本
*/
import type { ModelConfig } from './prompt.types.js';
interface FallbackPrompt {
content: string;
modelConfig: ModelConfig;
}
/**
* RVW 模块兜底 Prompt
*/
const RVW_FALLBACKS: Record<string, FallbackPrompt> = {
RVW_EDITORIAL: {
content: `你是一位专业的医学期刊编辑,负责评估稿件的规范性。
【评估标准】
1. 文稿科学性与实用性
2. 文题中文不超过20字英文不超过10实词
3. 作者格式
4. 摘要300-500字含目的、方法、结果、结论
5. 关键词2-5个
6. 医学名词和药物名称
7. 缩略语
8. 计量单位
9. 图片格式
10. 动态图像
11. 参考文献
请输出JSON格式的评估结果包含overall_score和items数组。`,
modelConfig: { model: 'deepseek-v3', temperature: 0.3 },
},
RVW_METHODOLOGY: {
content: `你是一位资深的医学统计学专家,负责评估稿件的方法学质量。
【评估框架】
第一部分:科研设计评估(研究类型、对象、对照、质控)
第二部分:统计学方法描述(软件、方法、混杂因素)
第三部分:统计分析评估(方法正确性、结果描述)
请输出JSON格式的评估结果包含overall_score和parts数组。`,
modelConfig: { model: 'deepseek-v3', temperature: 0.3 },
},
};
/**
* ASL 模块兜底 Prompt预留
*/
const ASL_FALLBACKS: Record<string, FallbackPrompt> = {
ASL_SCREENING: {
content: `你是一位文献筛选专家,负责根据纳入排除标准筛选文献。
请根据提供的标准对文献进行筛选输出JSON格式的结果。`,
modelConfig: { model: 'deepseek-v3', temperature: 0.2 },
},
};
/**
* 所有模块的兜底 Prompt 汇总
*/
export const FALLBACK_PROMPTS: Record<string, FallbackPrompt> = {
...RVW_FALLBACKS,
...ASL_FALLBACKS,
};
/**
* 获取兜底 Prompt
*
* @param code Prompt 代码
* @returns 兜底 Prompt 或 undefined
*/
export function getFallbackPrompt(code: string): FallbackPrompt | undefined {
return FALLBACK_PROMPTS[code];
}
/**
* 检查是否有兜底 Prompt
*/
export function hasFallbackPrompt(code: string): boolean {
return code in FALLBACK_PROMPTS;
}
/**
* 获取所有兜底 Prompt 的代码列表
*/
export function getAllFallbackCodes(): string[] {
return Object.keys(FALLBACK_PROMPTS);
}

View File

@@ -0,0 +1,223 @@
/**
* Prompt 管理 API 路由
*
* 路由前缀: /api/admin/prompts
*/
import type { FastifyInstance } from 'fastify';
import {
listPrompts,
getPromptDetail,
saveDraft,
publishPrompt,
rollbackPrompt,
setDebugMode,
getDebugStatus,
testRender,
invalidateCache,
} from './prompt.controller.js';
// Schema 定义
const listPromptsSchema = {
querystring: {
type: 'object',
properties: {
module: { type: 'string', description: '过滤模块,如 RVW, ASL, DC' },
},
},
response: {
200: {
type: 'object',
properties: {
success: { type: 'boolean' },
data: {
type: 'array',
items: {
type: 'object',
properties: {
id: { type: 'number' },
code: { type: 'string' },
name: { type: 'string' },
module: { type: 'string' },
description: { type: 'string' },
variables: { type: 'array', items: { type: 'string' } },
latestVersion: {
type: 'object',
properties: {
version: { type: 'number' },
status: { type: 'string' },
createdAt: { type: 'string' },
},
},
updatedAt: { type: 'string' },
},
},
},
total: { type: 'number' },
},
},
},
};
const getPromptDetailSchema = {
params: {
type: 'object',
properties: {
code: { type: 'string' },
},
required: ['code'],
},
};
const saveDraftSchema = {
params: {
type: 'object',
properties: {
code: { type: 'string' },
},
required: ['code'],
},
body: {
type: 'object',
properties: {
content: { type: 'string', description: 'Prompt 内容(支持 Handlebars' },
modelConfig: {
type: 'object',
properties: {
model: { type: 'string' },
temperature: { type: 'number' },
maxTokens: { type: 'number' },
},
},
changelog: { type: 'string', description: '变更说明' },
},
required: ['content'],
},
};
const publishSchema = {
params: {
type: 'object',
properties: {
code: { type: 'string' },
},
required: ['code'],
},
};
const rollbackSchema = {
params: {
type: 'object',
properties: {
code: { type: 'string' },
},
required: ['code'],
},
body: {
type: 'object',
properties: {
version: { type: 'number', description: '目标版本号' },
},
required: ['version'],
},
};
const setDebugModeSchema = {
body: {
type: 'object',
properties: {
modules: {
type: 'array',
items: { type: 'string' },
description: '要调试的模块列表,如 ["RVW"] 或 ["ALL"]',
},
enabled: { type: 'boolean', description: '是否开启调试模式' },
},
required: ['modules', 'enabled'],
},
};
const testRenderSchema = {
body: {
type: 'object',
properties: {
content: { type: 'string', description: 'Prompt 模板内容' },
variables: {
type: 'object',
additionalProperties: true,
description: '变量键值对',
},
},
required: ['content', 'variables'],
},
};
/**
* 注册 Prompt 管理路由
*/
export async function promptRoutes(fastify: FastifyInstance) {
// 列表
fastify.get('/', {
schema: listPromptsSchema,
// preHandler: [fastify.authenticate, fastify.requirePermission('prompt:view')],
handler: listPrompts,
});
// 详情
fastify.get('/:code', {
schema: getPromptDetailSchema,
// preHandler: [fastify.authenticate, fastify.requirePermission('prompt:view')],
handler: getPromptDetail,
});
// 保存草稿(需要 prompt:edit
fastify.post('/:code/draft', {
schema: saveDraftSchema,
// preHandler: [fastify.authenticate, fastify.requirePermission('prompt:edit')],
handler: saveDraft,
});
// 发布(需要 prompt:publish
fastify.post('/:code/publish', {
schema: publishSchema,
// preHandler: [fastify.authenticate, fastify.requirePermission('prompt:publish')],
handler: publishPrompt,
});
// 回滚(需要 prompt:publish
fastify.post('/:code/rollback', {
schema: rollbackSchema,
// preHandler: [fastify.authenticate, fastify.requirePermission('prompt:publish')],
handler: rollbackPrompt,
});
// 调试模式 - 获取状态
fastify.get('/debug', {
// preHandler: [fastify.authenticate],
handler: getDebugStatus,
});
// 调试模式 - 设置(需要 prompt:debug
fastify.post('/debug', {
schema: setDebugModeSchema,
// preHandler: [fastify.authenticate, fastify.requirePermission('prompt:debug')],
handler: setDebugMode,
});
// 测试渲染
fastify.post('/test-render', {
schema: testRenderSchema,
// preHandler: [fastify.authenticate, fastify.requirePermission('prompt:edit')],
handler: testRender,
});
// 清除缓存
fastify.post('/:code/invalidate-cache', {
schema: getPromptDetailSchema,
// preHandler: [fastify.authenticate, fastify.requirePermission('prompt:edit')],
handler: invalidateCache,
});
}
export default promptRoutes;

View File

@@ -0,0 +1,595 @@
/**
* Prompt 管理服务
*
* 核心功能:
* 1. 灰度预览:调试模式下返回 DRAFT正常返回 ACTIVE
* 2. 变量渲染Handlebars 模板引擎
* 3. 三级容灾:数据库 → 缓存 → 兜底
* 4. 热更新Postgres LISTEN/NOTIFY
* 5. 变量校验:自动提取和验证变量
*/
import { PrismaClient, Prisma } from '@prisma/client';
import Handlebars from 'handlebars';
import type {
RenderedPrompt,
GetPromptOptions,
ModelConfig,
VariableValidation,
DebugState,
} from './prompt.types.js';
import { getFallbackPrompt } from './prompt.fallbacks.js';
// 默认模型配置
const DEFAULT_MODEL_CONFIG: ModelConfig = {
model: 'deepseek-v3',
temperature: 0.3,
};
// 缓存 TTL
const CACHE_TTL = 5 * 60; // 5分钟
export class PromptService {
private prisma: PrismaClient;
private cache: Map<string, { data: any; expiresAt: number }>;
private debugStates: Map<string, DebugState>; // userId -> DebugState
private notifyClient: any; // 用于 LISTEN/NOTIFY
constructor(prisma: PrismaClient) {
this.prisma = prisma;
this.cache = new Map();
this.debugStates = new Map();
}
/**
* 🎯 核心方法:获取渲染后的 Prompt
*
* 灰度逻辑:
* - 如果用户开启了该模块的调试模式,返回 DRAFT
* - 否则返回 ACTIVE
*
* @param code Prompt 代码,如 'RVW_EDITORIAL'
* @param variables 模板变量
* @param options 选项userId 用于判断调试模式)
*/
async get(
code: string,
variables: Record<string, any> = {},
options: GetPromptOptions = {}
): Promise<RenderedPrompt> {
const { userId, skipCache = false } = options;
try {
// 1. 判断是否处于调试模式
const isDebugging = userId ? this.isDebugging(userId, code) : false;
// 2. 获取 Prompt 版本
let version;
if (isDebugging) {
// 调试模式:优先获取 DRAFT
version = await this.getDraftVersion(code);
if (!version) {
// 没有 DRAFT降级到 ACTIVE
version = await this.getActiveVersion(code, skipCache);
}
} else {
// 正常模式:获取 ACTIVE
version = await this.getActiveVersion(code, skipCache);
}
// 3. 如果数据库获取失败,使用兜底
if (!version) {
console.warn(`[PromptService] Fallback to hardcoded prompt: ${code}`);
const fallback = getFallbackPrompt(code);
if (!fallback) {
throw new Error(`Prompt not found and no fallback available: ${code}`);
}
return {
content: this.render(fallback.content, variables),
modelConfig: fallback.modelConfig,
version: 0,
isDraft: false,
};
}
// 4. 渲染模板
const content = this.render(version.content, variables);
const modelConfig = (version.model_config as ModelConfig) || DEFAULT_MODEL_CONFIG;
return {
content,
modelConfig,
version: version.version,
isDraft: version.status === 'DRAFT',
};
} catch (error) {
console.error(`[PromptService] Error getting prompt ${code}:`, error);
// 最后的兜底
const fallback = getFallbackPrompt(code);
if (fallback) {
return {
content: this.render(fallback.content, variables),
modelConfig: fallback.modelConfig,
version: 0,
isDraft: false,
};
}
throw error;
}
}
/**
* 获取 ACTIVE 版本(带缓存)
*/
async getActiveVersion(code: string, skipCache = false) {
const cacheKey = `prompt:active:${code}`;
// 检查缓存
if (!skipCache) {
const cached = this.getFromCache(cacheKey);
if (cached) {
return cached;
}
}
// 从数据库获取
const template = await this.prisma.prompt_templates.findUnique({
where: { code },
include: {
versions: {
where: { status: 'ACTIVE' },
orderBy: { version: 'desc' },
take: 1,
},
},
});
if (!template || template.versions.length === 0) {
return null;
}
const version = template.versions[0];
// 写入缓存
this.setCache(cacheKey, version, CACHE_TTL);
return version;
}
/**
* 获取 DRAFT 版本(不缓存,总是实时)
*/
async getDraftVersion(code: string) {
const template = await this.prisma.prompt_templates.findUnique({
where: { code },
include: {
versions: {
where: { status: 'DRAFT' },
orderBy: { version: 'desc' },
take: 1,
},
},
});
if (!template || template.versions.length === 0) {
return null;
}
return template.versions[0];
}
/**
* 渲染模板Handlebars
*/
render(template: string, variables: Record<string, any>): string {
try {
const compiled = Handlebars.compile(template, { noEscape: true });
return compiled(variables);
} catch (error) {
console.error('[PromptService] Template render error:', error);
// 渲染失败返回原始模板
return template;
}
}
/**
* 从内容中提取变量名
*
* 支持:
* - {{variable}}
* - {{#if variable}}...{{/if}}
* - {{#each items}}...{{/each}}
*/
extractVariables(content: string): string[] {
const regex = /\{\{([^#/][^}]*)\}\}/g;
const variables = new Set<string>();
let match;
while ((match = regex.exec(content)) !== null) {
const varName = match[1].trim();
// 排除 Handlebars 助手函数
if (!varName.startsWith('#') && !varName.startsWith('/') && !varName.includes(' ')) {
variables.add(varName);
}
}
return Array.from(variables);
}
/**
* 校验变量完整性
*/
validateVariables(
content: string,
providedVariables: Record<string, any>
): VariableValidation {
const expectedVars = this.extractVariables(content);
const providedKeys = Object.keys(providedVariables);
const missingVariables = expectedVars.filter(v => !providedKeys.includes(v));
const extraVariables = providedKeys.filter(v => !expectedVars.includes(v));
return {
isValid: missingVariables.length === 0,
missingVariables,
extraVariables,
};
}
// ==================== 调试模式管理 ====================
/**
* 设置调试模式
*
* @param userId 用户ID
* @param modules 要调试的模块列表,如 ['RVW'] 或 ['ALL']
* @param enabled 是否开启
*/
setDebugMode(userId: string, modules: string[], enabled: boolean): void {
if (enabled) {
this.debugStates.set(userId, {
userId,
modules: new Set(modules),
enabledAt: new Date(),
});
console.log(`[PromptService] Debug mode enabled for user ${userId}, modules: [${modules.join(', ')}]`);
} else {
this.debugStates.delete(userId);
console.log(`[PromptService] Debug mode disabled for user ${userId}`);
}
}
/**
* 检查用户是否在某模块的调试模式
*/
isDebugging(userId: string, code: string): boolean {
const state = this.debugStates.get(userId);
if (!state) return false;
// 提取模块名(如 'RVW_EDITORIAL' -> 'RVW'
const module = code.split('_')[0];
// 检查是否调试全部或指定模块
return state.modules.has('ALL') || state.modules.has(module);
}
/**
* 获取用户的调试状态
*/
getDebugState(userId: string): DebugState | null {
return this.debugStates.get(userId) || null;
}
/**
* 获取所有调试中的用户
*/
getAllDebugUsers(): string[] {
return Array.from(this.debugStates.keys());
}
// ==================== 缓存管理 ====================
private getFromCache(key: string): any {
const entry = this.cache.get(key);
if (!entry) return null;
if (Date.now() > entry.expiresAt) {
this.cache.delete(key);
return null;
}
return entry.data;
}
private setCache(key: string, data: any, ttlSeconds: number): void {
this.cache.set(key, {
data,
expiresAt: Date.now() + ttlSeconds * 1000,
});
}
/**
* 清除指定 Prompt 的缓存
*/
invalidateCache(code: string): void {
const cacheKey = `prompt:active:${code}`;
this.cache.delete(cacheKey);
console.log(`[PromptService] Cache invalidated for: ${code}`);
}
/**
* 清除所有缓存
*/
clearAllCache(): void {
this.cache.clear();
console.log('[PromptService] All cache cleared');
}
// ==================== LISTEN/NOTIFY 热更新 ====================
/**
* 启动 Postgres LISTEN/NOTIFY 监听
*
* 使用方法:
* 1. 在数据库中创建触发器:
* CREATE OR REPLACE FUNCTION notify_prompt_change() RETURNS trigger AS $$
* BEGIN
* PERFORM pg_notify('prompt_changed', NEW.code);
* RETURN NEW;
* END;
* $$ LANGUAGE plpgsql;
*
* CREATE TRIGGER prompt_change_trigger
* AFTER INSERT OR UPDATE ON capability_schema.prompt_versions
* FOR EACH ROW EXECUTE FUNCTION notify_prompt_change();
*
* 2. 调用 promptService.startListening()
*/
async startListening(): Promise<void> {
// 注意Prisma 不直接支持 LISTEN需要使用原生 pg 客户端
// 这里先留空,后续如果需要可以实现
console.log('[PromptService] LISTEN/NOTIFY not yet implemented - using manual cache invalidation');
}
/**
* 停止监听
*/
async stopListening(): Promise<void> {
if (this.notifyClient) {
await this.notifyClient.end();
this.notifyClient = null;
}
}
// ==================== 管理功能 ====================
/**
* 获取所有模板列表
*/
async listTemplates(module?: string) {
const where = module ? { module } : {};
return this.prisma.prompt_templates.findMany({
where,
include: {
versions: {
orderBy: { version: 'desc' },
take: 1,
},
},
orderBy: { code: 'asc' },
});
}
/**
* 获取模板详情(含所有版本)
*/
async getTemplateDetail(code: string) {
return this.prisma.prompt_templates.findUnique({
where: { code },
include: {
versions: {
orderBy: { version: 'desc' },
},
},
});
}
/**
* 保存草稿
*/
async saveDraft(
code: string,
content: string,
modelConfig?: ModelConfig,
changelog?: string,
createdBy?: string
) {
// 获取模板
const template = await this.prisma.prompt_templates.findUnique({
where: { code },
include: {
versions: {
orderBy: { version: 'desc' },
take: 1,
},
},
});
if (!template) {
throw new Error(`Template not found: ${code}`);
}
// 自动提取变量
const variables = this.extractVariables(content);
// 更新模板的变量字段
await this.prisma.prompt_templates.update({
where: { code },
data: { variables },
});
// 计算新版本号
const latestVersion = template.versions[0];
const newVersion = latestVersion ? latestVersion.version + 1 : 1;
// 检查是否已有 DRAFT
const existingDraft = await this.prisma.prompt_versions.findFirst({
where: {
template_id: template.id,
status: 'DRAFT',
},
});
if (existingDraft) {
// 更新现有 DRAFT
return this.prisma.prompt_versions.update({
where: { id: existingDraft.id },
data: {
content,
model_config: (modelConfig || existingDraft.model_config) as unknown as Prisma.InputJsonValue,
changelog,
created_by: createdBy,
},
});
} else {
// 创建新 DRAFT
return this.prisma.prompt_versions.create({
data: {
template_id: template.id,
version: newVersion,
content,
model_config: (modelConfig || DEFAULT_MODEL_CONFIG) as unknown as Prisma.InputJsonValue,
status: 'DRAFT',
changelog,
created_by: createdBy,
},
});
}
}
/**
* 发布草稿DRAFT → ACTIVE
*
* 需要 prompt:publish 权限
*/
async publish(code: string, createdBy?: string) {
// 获取 DRAFT
const template = await this.prisma.prompt_templates.findUnique({
where: { code },
});
if (!template) {
throw new Error(`Template not found: ${code}`);
}
const draft = await this.prisma.prompt_versions.findFirst({
where: {
template_id: template.id,
status: 'DRAFT',
},
});
if (!draft) {
throw new Error(`No draft found for: ${code}`);
}
// 事务:归档旧 ACTIVE激活新版本
await this.prisma.$transaction([
// 1. 归档当前 ACTIVE
this.prisma.prompt_versions.updateMany({
where: {
template_id: template.id,
status: 'ACTIVE',
},
data: {
status: 'ARCHIVED',
},
}),
// 2. 激活 DRAFT
this.prisma.prompt_versions.update({
where: { id: draft.id },
data: {
status: 'ACTIVE',
created_by: createdBy,
},
}),
]);
// 清除缓存
this.invalidateCache(code);
console.log(`[PromptService] Published: ${code} (v${draft.version})`);
return draft;
}
/**
* 回滚到指定版本
*/
async rollback(code: string, targetVersion: number, createdBy?: string) {
const template = await this.prisma.prompt_templates.findUnique({
where: { code },
});
if (!template) {
throw new Error(`Template not found: ${code}`);
}
// 获取目标版本
const targetVersionRecord = await this.prisma.prompt_versions.findFirst({
where: {
template_id: template.id,
version: targetVersion,
},
});
if (!targetVersionRecord) {
throw new Error(`Version ${targetVersion} not found for: ${code}`);
}
// 事务:归档当前 ACTIVE重新激活目标版本
await this.prisma.$transaction([
this.prisma.prompt_versions.updateMany({
where: {
template_id: template.id,
status: 'ACTIVE',
},
data: {
status: 'ARCHIVED',
},
}),
this.prisma.prompt_versions.update({
where: { id: targetVersionRecord.id },
data: {
status: 'ACTIVE',
created_by: createdBy,
},
}),
]);
// 清除缓存
this.invalidateCache(code);
console.log(`[PromptService] Rolled back: ${code} to v${targetVersion}`);
return targetVersionRecord;
}
}
// 单例模式
let instance: PromptService | null = null;
export function getPromptService(prisma: PrismaClient): PromptService {
if (!instance) {
instance = new PromptService(prisma);
}
return instance;
}
export function resetPromptService(): void {
instance = null;
}

View File

@@ -0,0 +1,69 @@
/**
* Prompt 管理系统类型定义
*/
// Prompt 状态
export type PromptStatus = 'DRAFT' | 'ACTIVE' | 'ARCHIVED';
// 模型配置
export interface ModelConfig {
model: string; // 模型名称,如 'deepseek-v3'
temperature?: number; // 温度参数
maxTokens?: number; // 最大输出token
topP?: number; // Top-P采样
fallback?: string; // 降级模型
}
// Prompt 模板
export interface PromptTemplate {
id: number;
code: string; // 唯一标识符,如 'RVW_EDITORIAL'
name: string; // 人类可读名称
module: string; // 所属模块: RVW, ASL, DC, IIT, PKB, AIA
description?: string;
variables?: string[]; // 预期变量列表
createdAt: Date;
updatedAt: Date;
}
// Prompt 版本
export interface PromptVersion {
id: number;
templateId: number;
version: number;
content: string; // Prompt 内容(支持 Handlebars
modelConfig?: ModelConfig;
status: PromptStatus;
changelog?: string;
createdBy?: string;
createdAt: Date;
}
// 渲染后的 Prompt
export interface RenderedPrompt {
content: string; // 渲染后的内容
modelConfig: ModelConfig;
version: number;
isDraft: boolean; // 是否来自草稿(调试模式)
}
// 调试模式状态
export interface DebugState {
userId: string;
modules: Set<string>; // 'RVW', 'ASL', 'DC', 'ALL'
enabledAt: Date;
}
// 获取 Prompt 的选项
export interface GetPromptOptions {
userId?: string; // 用于判断调试模式
skipCache?: boolean; // 跳过缓存
}
// 变量校验结果
export interface VariableValidation {
isValid: boolean;
missingVariables: string[];
extraVariables: string[];
}

View File

@@ -16,6 +16,8 @@ import { registerDCRoutes, initDCModule } from './modules/dc/index.js';
import pkbRoutes from './modules/pkb/routes/index.js';
import { registerHealthRoutes } from './common/health/index.js';
import { logger } from './common/logging/index.js';
import { authRoutes, registerAuthPlugin } from './common/auth/index.js';
import { promptRoutes } from './common/prompt/index.js';
import { registerTestRoutes } from './test-platform-api.js';
import { registerScreeningWorkers } from './modules/asl/services/screeningWorker.js';
import { registerExtractionWorkers } from './modules/dc/tool-b/workers/extractionWorker.js';
@@ -78,6 +80,19 @@ console.log('✅ 文件上传插件已配置: 最大文件大小 10MB');
await registerHealthRoutes(fastify);
logger.info('✅ 健康检查路由已注册');
// ============================================
// 【平台基础设施】认证模块
// ============================================
await registerAuthPlugin(fastify);
await fastify.register(authRoutes, { prefix: '/api/v1/auth' });
logger.info('✅ 认证路由已注册: /api/v1/auth');
// ============================================
// 【运营管理】Prompt管理模块
// ============================================
await fastify.register(promptRoutes, { prefix: '/api/admin/prompts' });
logger.info('✅ Prompt管理路由已注册: /api/admin/prompts');
// ============================================
// 【临时】平台基础设施测试API
// ============================================

View File

@@ -349,5 +349,6 @@ runTests().catch((error) => {

View File

@@ -328,5 +328,6 @@ Content-Type: application/json

View File

@@ -264,5 +264,6 @@ export const conflictDetectionService = new ConflictDetectionService();

View File

@@ -214,5 +214,6 @@ curl -X POST http://localhost:3000/api/v1/dc/tool-c/test/execute \

View File

@@ -268,5 +268,6 @@ export const streamAIController = new StreamAIController();

View File

@@ -179,3 +179,4 @@ logger.info('[SessionMemory] 会话记忆管理器已启动', {

View File

@@ -113,3 +113,4 @@ checkTableStructure();

View File

@@ -100,3 +100,4 @@ checkProjectConfig().catch(console.error);

View File

@@ -82,3 +82,4 @@ main();

View File

@@ -539,3 +539,4 @@ URL: https://iit.xunzhengyixue.com/api/v1/iit/patient-wechat/callback

View File

@@ -174,3 +174,4 @@ console.log('');

View File

@@ -491,3 +491,4 @@ export const patientWechatService = new PatientWechatService();

View File

@@ -136,3 +136,4 @@ testDifyIntegration().catch(error => {

View File

@@ -165,3 +165,4 @@ testIitDatabase()

View File

@@ -151,3 +151,4 @@ if (hasError) {

View File

@@ -177,3 +177,4 @@ async function testUrlVerification() {

View File

@@ -258,3 +258,4 @@ main().catch((error) => {

View File

@@ -142,3 +142,4 @@ Write-Host ""

View File

@@ -235,3 +235,4 @@ export interface CachedProtocolRules {

View File

@@ -48,3 +48,4 @@ export default async function healthRoutes(fastify: FastifyInstance) {

View File

@@ -126,3 +126,4 @@ Content-Type: application/json

View File

@@ -111,3 +111,4 @@ Write-Host " - 删除任务: DELETE $BaseUrl/api/v2/rvw/tasks/{taskId}" -Foregr

View File

@@ -25,3 +25,4 @@ export * from './services/utils.js';

View File

@@ -46,3 +46,4 @@ export default async function rvwRoutes(fastify: FastifyInstance) {

View File

@@ -70,3 +70,4 @@ export async function reviewEditorialStandards(

View File

@@ -70,3 +70,4 @@ export async function reviewMethodology(

View File

@@ -116,3 +116,4 @@ export function validateAgentSelection(agents: string[]): void {

View File

@@ -414,5 +414,6 @@ SET session_replication_role = 'origin';

View File

@@ -116,5 +116,6 @@ WHERE key = 'verify_test';

View File

@@ -259,5 +259,6 @@ verifyDatabase()

View File

@@ -49,5 +49,6 @@ export {}

View File

@@ -72,5 +72,6 @@ Write-Host "✅ 完成!" -ForegroundColor Green

2
backend/temp_check.sql Normal file
View File

@@ -0,0 +1,2 @@
SELECT schema_name FROM information_schema.schemata WHERE schema_name NOT IN ('pg_catalog', 'information_schema', 'pg_toast') ORDER BY schema_name;

View File

@@ -162,3 +162,4 @@ DELETE {{baseUrl}}/api/v2/pkb/knowledge/knowledge-bases/{{testKbId}}

View File

@@ -359,5 +359,6 @@ runAdvancedTests().catch(error => {

View File

@@ -425,5 +425,6 @@ runAllTests()

View File

@@ -383,5 +383,6 @@ runAllTests()

View File

@@ -0,0 +1,22 @@
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
async function main() {
console.log('=== platform_schema.users ===');
const platformUsers: any[] = await prisma.$queryRaw`
SELECT id, name, phone, role FROM platform_schema.users ORDER BY created_at
`;
platformUsers.forEach(u => console.log(` ${u.id}: ${u.name} (${u.phone}) [${u.role}]`));
console.log('\n=== public.users ===');
const publicUsers: any[] = await prisma.$queryRaw`
SELECT id, name, email FROM public.users
`;
publicUsers.forEach(u => console.log(` ${u.id}: ${u.name} (${u.email})`));
}
main()
.catch(console.error)
.finally(() => prisma.$disconnect());

View File

@@ -0,0 +1,20 @@
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
async function main() {
const functions: any[] = await prisma.$queryRaw`
SELECT routine_name, routine_type
FROM information_schema.routines
WHERE routine_schema = 'platform_schema'
ORDER BY routine_name
`;
console.log('platform_schema 中的函数:');
functions.forEach(f => console.log(`${f.routine_name} (${f.routine_type})`));
}
main()
.catch(console.error)
.finally(() => prisma.$disconnect());

View File

@@ -0,0 +1,32 @@
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
async function main() {
const result: any[] = await prisma.$queryRaw`
SELECT table_name
FROM information_schema.tables
WHERE table_schema = 'platform_schema' AND table_name = 'job_common'
`;
if (result.length > 0) {
console.log('✅ platform_schema.job_common 表已恢复!');
// 检查列结构
const cols: any[] = await prisma.$queryRaw`
SELECT column_name, data_type
FROM information_schema.columns
WHERE table_schema = 'platform_schema' AND table_name = 'job_common'
ORDER BY ordinal_position
`;
console.log('\n列结构:');
cols.forEach(c => console.log(` ${c.column_name}: ${c.data_type}`));
} else {
console.log('❌ platform_schema.job_common 表不存在');
}
}
main()
.catch(console.error)
.finally(() => prisma.$disconnect());

View File

@@ -0,0 +1,21 @@
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
async function main() {
const users: any[] = await prisma.$queryRaw`
SELECT id, name, email FROM public.users
`;
console.log('public.users 中的用户:');
if (users.length === 0) {
console.log(' ❌ 无用户');
} else {
users.forEach(u => console.log(`${u.id}: ${u.name} (${u.email})`));
}
}
main()
.catch(console.error)
.finally(() => prisma.$disconnect());

Some files were not shown because too many files have changed in this diff Show More