Summary: - Refactor timeline API to read from qc_field_status (SSOT) instead of qc_logs - Add field-issues paginated API with severity/dimension/recordId filters - Add LEFT JOIN field_metadata + qc_event_status for Chinese display names - Implement per-project ChatOrchestrator cache and SessionMemory isolation - Redesign admin IIT config tabs (REDCap -> Fields -> KB -> Rules -> Members) - Add AI-powered QC rule generation (D3 programmatic + D1/D5/D6 LLM-based) - Add clickable warning/critical detail Modal in ReportsPage - Auto-dispatch eQuery after batch QC via DailyQcOrchestrator - Update module status documentation to v3.2 Backend changes: - iitQcCockpitController: rewrite getTimeline from qc_field_status, add getFieldIssues - iitQcCockpitRoutes: add field-issues route - ChatOrchestrator: per-projectId cached instances - SessionMemory: keyed by userId::projectId - WechatCallbackController: resolve projectId from iitUserMapping - iitRuleSuggestionService: dimension-based suggest + generateD3Rules - iitBatchController: call DailyQcOrchestrator after batch QC Frontend changes: - AiStreamPage: adapt to new timeline structure with dimension tags - ReportsPage: clickable stats cards with issue detail Modal - IitProjectDetailPage: reorder tabs, add AI rule generation UI - iitProjectApi: add TimelineIssue, FieldIssueItem types and APIs Status: TypeScript compilation verified, no new lint errors Made-with: Cursor
493 lines
18 KiB
TypeScript
493 lines
18 KiB
TypeScript
import { PrismaClient, UserRole, TenantType, TenantStatus } from '@prisma/client';
|
||
import bcrypt from 'bcryptjs';
|
||
import { v4 as uuidv4 } from 'uuid';
|
||
|
||
const prisma = new PrismaClient();
|
||
|
||
// 默认密码
|
||
const DEFAULT_PASSWORD = '123456';
|
||
|
||
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: 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}`);
|
||
|
||
// 为内部租户开放所有模块(超级管理员完整权限)
|
||
const internalModules = ['AIA', 'ASL', 'PKB', 'DC', 'SSA', 'ST', 'RVW', 'IIT', 'RM', 'AIA_PROTOCOL'];
|
||
for (const moduleCode of internalModules) {
|
||
await prisma.tenant_modules.upsert({
|
||
where: { tenant_id_module_code: { tenant_id: internalTenant.id, module_code: moduleCode } },
|
||
update: {},
|
||
create: {
|
||
id: uuidv4(),
|
||
tenant_id: internalTenant.id,
|
||
module_code: moduleCode,
|
||
is_enabled: true,
|
||
},
|
||
});
|
||
}
|
||
console.log(` ✅ 内部租户模块订阅: ${internalModules.join(', ')}`);
|
||
|
||
// ============================================
|
||
// 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',
|
||
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(),
|
||
},
|
||
});
|
||
|
||
const neurology = await prisma.departments.upsert({
|
||
where: { id: 'dept-neurology' },
|
||
update: {},
|
||
create: {
|
||
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',
|
||
isTrial: false,
|
||
},
|
||
});
|
||
|
||
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}`);
|
||
|
||
// 创建租户成员关系
|
||
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 尚未创建)');
|
||
console.log('💡 提示:Protocol Agent 配置请运行独立脚本: npx tsx prisma/seed-protocol-agent.ts');
|
||
|
||
// ============================================
|
||
// 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()
|
||
.then(async () => {
|
||
await prisma.$disconnect();
|
||
})
|
||
.catch(async (e) => {
|
||
console.error('❌ 种子数据创建失败:', e);
|
||
await prisma.$disconnect();
|
||
process.exit(1);
|
||
});
|