import 'dotenv/config'; import { PrismaPg } from '@prisma/adapter-pg'; import { hash } from 'bcrypt'; import prismaClientPackage from '@prisma/client'; const { DictionaryType, DeviceStatus, PrismaClient, Role, TaskStatus } = prismaClientPackage; const connectionString = process.env.DATABASE_URL; if (!connectionString) { throw new Error('DATABASE_URL is required to run seed'); } const prisma = new PrismaClient({ adapter: new PrismaPg({ connectionString }), }); const SEED_PASSWORD_PLAIN = 'Seed@1234'; async function ensureHospital(name) { return ( (await prisma.hospital.findFirst({ where: { name } })) ?? prisma.hospital.create({ data: { name } }) ); } async function ensureDepartment(hospitalId, name) { return ( (await prisma.department.findFirst({ where: { hospitalId, name }, })) ?? prisma.department.create({ data: { hospitalId, name }, }) ); } async function ensureGroup(departmentId, name) { return ( (await prisma.group.findFirst({ where: { departmentId, name }, })) ?? prisma.group.create({ data: { departmentId, name }, }) ); } async function upsertUserByOpenId(openId, data) { return prisma.user.upsert({ where: { openId }, // 每次重置/补种子时推进失效时间,确保历史 token 无法继续访问。 update: { ...data, tokenValidAfter: new Date(), }, create: { ...data, openId, }, }); } async function ensurePatient({ hospitalId, doctorId, creatorId, name, inpatientNo = null, projectName = null, phone, idCard, }) { const existing = await prisma.patient.findFirst({ where: { hospitalId, phone, idCard, }, }); if (existing) { if ( existing.doctorId !== doctorId || existing.creatorId !== creatorId || existing.name !== name || existing.inpatientNo !== inpatientNo || existing.projectName !== projectName ) { return prisma.patient.update({ where: { id: existing.id }, data: { doctorId, creatorId, name, inpatientNo, projectName }, }); } return existing; } return prisma.patient.create({ data: { hospitalId, doctorId, creatorId, name, inpatientNo, projectName, phone, idCard, }, }); } async function ensureFamilyMiniAppAccount({ phone, openId = null, serviceUid = null, }) { const existing = await prisma.familyMiniAppAccount.findUnique({ where: { phone }, }); if (existing) { return prisma.familyMiniAppAccount.update({ where: { id: existing.id }, data: { openId, serviceUid, lastLoginAt: new Date(), }, }); } return prisma.familyMiniAppAccount.create({ data: { phone, openId, serviceUid, lastLoginAt: new Date(), }, }); } async function ensureImplantCatalog({ modelCode, manufacturer, name, pressureLevels = [], isPressureAdjustable = true, notes = null, }) { return prisma.implantCatalog.upsert({ where: { modelCode }, update: { manufacturer, name, pressureLevels, isPressureAdjustable, notes, }, create: { modelCode, manufacturer, name, pressureLevels, isPressureAdjustable, notes, }, }); } async function ensureDictionaryItem({ type, label, sortOrder = 0, enabled = true, }) { return prisma.dictionaryItem.upsert({ where: { type_label: { type, label, }, }, update: { sortOrder, enabled, }, create: { type, label, sortOrder, enabled, }, }); } async function ensurePatientSurgery({ patientId, surgeryDate, surgeryName, surgeonName, preOpPressure = null, primaryDisease, hydrocephalusTypes, previousShuntSurgeryDate = null, preOpMaterials = null, notes = null, }) { const normalizedSurgeryDate = new Date(surgeryDate); const normalizedPreviousDate = previousShuntSurgeryDate ? new Date(previousShuntSurgeryDate) : null; const existing = await prisma.patientSurgery.findFirst({ where: { patientId, surgeryDate: normalizedSurgeryDate, surgeryName, }, }); if (existing) { return prisma.patientSurgery.update({ where: { id: existing.id }, data: { surgeonName, preOpPressure, primaryDisease, hydrocephalusTypes, previousShuntSurgeryDate: normalizedPreviousDate, preOpMaterials, notes, }, }); } return prisma.patientSurgery.create({ data: { patientId, surgeryDate: normalizedSurgeryDate, surgeryName, surgeonName, preOpPressure, primaryDisease, hydrocephalusTypes, previousShuntSurgeryDate: normalizedPreviousDate, preOpMaterials, notes, }, }); } async function ensureDevice({ patientId, surgeryId, implantCatalogId, currentPressure, status, implantModel, implantManufacturer, implantName, isPressureAdjustable, isAbandoned, shuntMode, proximalPunctureAreas, valvePlacementSites, distalShuntDirection, initialPressure, implantNotes, labelImageUrl, }) { const existing = await prisma.device.findFirst({ where: { patientId, surgeryId, implantNotes, }, }); const data = { patientId, surgeryId, implantCatalogId, currentPressure, status, implantModel, implantManufacturer, implantName, isPressureAdjustable, isAbandoned, shuntMode, proximalPunctureAreas, valvePlacementSites, distalShuntDirection, initialPressure, implantNotes, labelImageUrl, }; if (existing) { return prisma.device.update({ where: { id: existing.id }, data, }); } return prisma.device.create({ data }); } async function main() { const seedPasswordHash = await hash(SEED_PASSWORD_PLAIN, 12); const hospitalA = await ensureHospital('Seed Hospital A'); const hospitalB = await ensureHospital('Seed Hospital B'); const departmentA1 = await ensureDepartment(hospitalA.id, 'Neurosurgery-A1'); const departmentA2 = await ensureDepartment(hospitalA.id, 'Cardiology-A2'); const departmentB1 = await ensureDepartment(hospitalB.id, 'Neurosurgery-B1'); const groupA1 = await ensureGroup(departmentA1.id, 'Shift-A1'); const groupA2 = await ensureGroup(departmentA2.id, 'Shift-A2'); const groupB1 = await ensureGroup(departmentB1.id, 'Shift-B1'); const systemAdmin = await upsertUserByOpenId('seed-system-admin-openid', { name: 'Seed System Admin', phone: '13800001000', passwordHash: seedPasswordHash, role: Role.SYSTEM_ADMIN, hospitalId: null, departmentId: null, groupId: null, }); const hospitalAdminA = await upsertUserByOpenId( 'seed-hospital-admin-a-openid', { name: 'Seed Hospital Admin A', phone: '13800001001', passwordHash: seedPasswordHash, role: Role.HOSPITAL_ADMIN, hospitalId: hospitalA.id, departmentId: null, groupId: null, }, ); await upsertUserByOpenId('seed-hospital-admin-b-openid', { name: 'Seed Hospital Admin B', phone: '13800001101', passwordHash: seedPasswordHash, role: Role.HOSPITAL_ADMIN, hospitalId: hospitalB.id, departmentId: null, groupId: null, }); const directorA = await upsertUserByOpenId('seed-director-a-openid', { name: 'Seed Director A', phone: '13800001002', passwordHash: seedPasswordHash, role: Role.DIRECTOR, hospitalId: hospitalA.id, departmentId: departmentA1.id, groupId: null, }); const leaderA = await upsertUserByOpenId('seed-leader-a-openid', { name: 'Seed Leader A', phone: '13800001003', passwordHash: seedPasswordHash, role: Role.LEADER, hospitalId: hospitalA.id, departmentId: departmentA1.id, groupId: groupA1.id, }); const doctorA = await upsertUserByOpenId('seed-doctor-a-openid', { name: 'Seed Doctor A', phone: '13800001004', passwordHash: seedPasswordHash, role: Role.DOCTOR, hospitalId: hospitalA.id, departmentId: departmentA1.id, groupId: groupA1.id, }); const doctorA2 = await upsertUserByOpenId('seed-doctor-a2-openid', { name: 'Seed Doctor A2', phone: '13800001204', passwordHash: seedPasswordHash, role: Role.DOCTOR, hospitalId: hospitalA.id, departmentId: departmentA1.id, groupId: groupA1.id, }); const doctorA3 = await upsertUserByOpenId('seed-doctor-a3-openid', { name: 'Seed Doctor A3', phone: '13800001304', passwordHash: seedPasswordHash, role: Role.DOCTOR, hospitalId: hospitalA.id, departmentId: departmentA2.id, groupId: groupA2.id, }); const doctorB = await upsertUserByOpenId('seed-doctor-b-openid', { name: 'Seed Doctor B', phone: '13800001104', passwordHash: seedPasswordHash, role: Role.DOCTOR, hospitalId: hospitalB.id, departmentId: departmentB1.id, groupId: groupB1.id, }); const engineerA = await upsertUserByOpenId('seed-engineer-a-openid', { name: 'Seed Engineer A', phone: '13800001005', passwordHash: seedPasswordHash, role: Role.ENGINEER, hospitalId: hospitalA.id, departmentId: null, groupId: null, }); const engineerB = await upsertUserByOpenId('seed-engineer-b-openid', { name: 'Seed Engineer B', phone: '13800001105', passwordHash: seedPasswordHash, role: Role.ENGINEER, hospitalId: hospitalB.id, departmentId: null, groupId: null, }); const dictionarySeeds = { [DictionaryType.PRIMARY_DISEASE]: [ '先天性脑积水', '梗阻性脑积水', '交通性脑积水', '出血后脑积水', '肿瘤相关脑积水', '外伤后脑积水', '感染后脑积水', '分流功能障碍', ], [DictionaryType.HYDROCEPHALUS_TYPE]: [ '交通性', '梗阻性', '高压性', '正常压力', '先天性', '继发性', ], [DictionaryType.SHUNT_MODE]: ['VPS', 'VPLS', 'LPS', '脑室心房分流'], [DictionaryType.PROXIMAL_PUNCTURE_AREA]: [ '额角', '枕角', '三角区', '腰穿', '后角', ], [DictionaryType.VALVE_PLACEMENT_SITE]: [ '耳后', '胸前', '锁骨下', '腹壁', '腰背部', ], [DictionaryType.DISTAL_SHUNT_DIRECTION]: ['腹腔', '胸腔', '心房', '腰大池'], }; await Promise.all( Object.entries(dictionarySeeds).flatMap(([type, labels]) => labels.map((label, index) => ensureDictionaryItem({ type, label, sortOrder: index * 10, }), ), ), ); const patientA1 = await ensurePatient({ hospitalId: hospitalA.id, doctorId: doctorA.id, creatorId: doctorA.id, name: 'Seed Patient A1', inpatientNo: 'ZYH-A-0001', projectName: '脑积水随访项目-A', phone: '13800002001', idCard: '110101199001010011', }); const patientA2 = await ensurePatient({ hospitalId: hospitalA.id, doctorId: doctorA2.id, creatorId: doctorA2.id, name: 'Seed Patient A2', inpatientNo: 'ZYH-A-0002', projectName: '脑积水随访项目-A', phone: '13800002002', idCard: '110101199002020022', }); const patientA3 = await ensurePatient({ hospitalId: hospitalA.id, doctorId: doctorA3.id, creatorId: doctorA3.id, name: 'Seed Patient A3', inpatientNo: 'ZYH-A-0003', projectName: '脑积水随访项目-A', phone: '13800002003', idCard: '110101199003030033', }); const patientB1 = await ensurePatient({ hospitalId: hospitalB.id, doctorId: doctorB.id, creatorId: doctorB.id, name: 'Seed Patient B1', inpatientNo: 'ZYH-B-0001', projectName: '脑积水随访项目-B', phone: '13800002001', idCard: '110101199001010011', }); await ensureFamilyMiniAppAccount({ phone: patientA2.phone, openId: 'seed-family-a2-openid', }); const adjustableCatalog = await ensureImplantCatalog({ modelCode: 'SEED-ADJUSTABLE-VALVE', manufacturer: 'Seed MedTech', name: 'Seed 可调压分流阀', pressureLevels: [80, 100, 120, 140, 160], isPressureAdjustable: true, notes: 'Seed 全局可调压目录样例', }); const fixedCatalog = await ensureImplantCatalog({ modelCode: 'SEED-FIXED-VALVE', manufacturer: 'Seed MedTech', name: 'Seed 固定压分流阀', pressureLevels: [], isPressureAdjustable: false, notes: 'Seed 固定压目录样例', }); const surgeryA1Old = await ensurePatientSurgery({ patientId: patientA1.id, surgeryDate: '2024-06-01T08:00:00.000Z', surgeryName: '首次脑室腹腔分流术', surgeonName: 'Seed Director A', preOpPressure: 24, primaryDisease: '先天性脑积水', hydrocephalusTypes: ['交通性'], notes: '首台手术', }); const surgeryA1New = await ensurePatientSurgery({ patientId: patientA1.id, surgeryDate: '2025-09-10T08:00:00.000Z', surgeryName: '分流系统翻修术', surgeonName: 'Seed Director A', preOpPressure: 18, primaryDisease: '分流功能障碍', hydrocephalusTypes: ['交通性', '高压性'], previousShuntSurgeryDate: '2024-06-01T08:00:00.000Z', preOpMaterials: [ { type: 'IMAGE', url: 'https://seed.example.com/a1-ct-preop.png', name: 'Seed A1 术前 CT', }, ], notes: '二次手术,保留原设备历史', }); const surgeryA2 = await ensurePatientSurgery({ patientId: patientA2.id, surgeryDate: '2025-12-15T08:00:00.000Z', surgeryName: '脑室腹腔分流术', surgeonName: 'Seed Doctor A2', preOpPressure: 20, primaryDisease: '肿瘤相关脑积水', hydrocephalusTypes: ['梗阻性'], }); const surgeryA3 = await ensurePatientSurgery({ patientId: patientA3.id, surgeryDate: '2025-11-20T08:00:00.000Z', surgeryName: '脑室腹腔分流术', surgeonName: 'Seed Doctor A3', preOpPressure: 21, primaryDisease: '外伤后脑积水', hydrocephalusTypes: ['交通性'], }); const surgeryB1 = await ensurePatientSurgery({ patientId: patientB1.id, surgeryDate: '2025-10-05T08:00:00.000Z', surgeryName: '脑室腹腔分流术', surgeonName: 'Seed Doctor B', preOpPressure: 23, primaryDisease: '出血后脑积水', hydrocephalusTypes: ['高压性'], }); const deviceA1 = await ensureDevice({ patientId: patientA1.id, surgeryId: surgeryA1New.id, implantCatalogId: adjustableCatalog.id, currentPressure: 118, status: DeviceStatus.ACTIVE, implantModel: adjustableCatalog.modelCode, implantManufacturer: adjustableCatalog.manufacturer, implantName: adjustableCatalog.name, isPressureAdjustable: adjustableCatalog.isPressureAdjustable, isAbandoned: false, shuntMode: 'VPS', proximalPunctureAreas: ['额角'], valvePlacementSites: ['耳后'], distalShuntDirection: '腹腔', initialPressure: 118, implantNotes: 'Seed A1 当前在用设备', labelImageUrl: 'https://seed.example.com/labels/a1-001.jpg', }); const deviceA2 = await ensureDevice({ patientId: patientA2.id, surgeryId: surgeryA2.id, implantCatalogId: adjustableCatalog.id, currentPressure: 112, status: DeviceStatus.ACTIVE, implantModel: adjustableCatalog.modelCode, implantManufacturer: adjustableCatalog.manufacturer, implantName: adjustableCatalog.name, isPressureAdjustable: adjustableCatalog.isPressureAdjustable, isAbandoned: false, shuntMode: 'VPS', proximalPunctureAreas: ['枕角'], valvePlacementSites: ['胸前'], distalShuntDirection: '腹腔', initialPressure: 112, implantNotes: 'Seed A2 当前在用设备', labelImageUrl: 'https://seed.example.com/labels/a2-002.jpg', }); await ensureDevice({ patientId: patientA3.id, surgeryId: surgeryA3.id, implantCatalogId: adjustableCatalog.id, currentPressure: 109, status: DeviceStatus.ACTIVE, implantModel: adjustableCatalog.modelCode, implantManufacturer: adjustableCatalog.manufacturer, implantName: adjustableCatalog.name, isPressureAdjustable: adjustableCatalog.isPressureAdjustable, isAbandoned: false, shuntMode: 'LPS', proximalPunctureAreas: ['腰穿'], valvePlacementSites: ['腰背部'], distalShuntDirection: '腹腔', initialPressure: 109, implantNotes: 'Seed A3 当前在用设备', labelImageUrl: 'https://seed.example.com/labels/a3-003.jpg', }); const deviceB1 = await ensureDevice({ patientId: patientB1.id, surgeryId: surgeryB1.id, implantCatalogId: adjustableCatalog.id, currentPressure: 121, status: DeviceStatus.ACTIVE, implantModel: adjustableCatalog.modelCode, implantManufacturer: adjustableCatalog.manufacturer, implantName: adjustableCatalog.name, isPressureAdjustable: adjustableCatalog.isPressureAdjustable, isAbandoned: false, shuntMode: 'VPS', proximalPunctureAreas: ['额角'], valvePlacementSites: ['耳后'], distalShuntDirection: '腹腔', initialPressure: 121, implantNotes: 'Seed B1 当前在用设备', labelImageUrl: 'https://seed.example.com/labels/b1-001.jpg', }); await ensureDevice({ patientId: patientA1.id, surgeryId: surgeryA1Old.id, implantCatalogId: adjustableCatalog.id, currentPressure: 130, status: DeviceStatus.INACTIVE, implantModel: adjustableCatalog.modelCode, implantManufacturer: adjustableCatalog.manufacturer, implantName: adjustableCatalog.name, isPressureAdjustable: adjustableCatalog.isPressureAdjustable, isAbandoned: true, shuntMode: 'VPS', proximalPunctureAreas: ['额角'], valvePlacementSites: ['耳后'], distalShuntDirection: '腹腔', initialPressure: 130, implantNotes: 'Seed A1 弃用历史设备', labelImageUrl: 'https://seed.example.com/labels/a1-004.jpg', }); // 清理与种子设备关联的历史任务,保证 seed 可重复执行且生命周期夹具稳定。 const seedTaskItems = await prisma.taskItem.findMany({ where: { deviceId: { in: [deviceA1.id, deviceB1.id], }, }, select: { taskId: true }, }); const seedTaskIds = Array.from( new Set(seedTaskItems.map((item) => item.taskId)), ); if (seedTaskIds.length > 0) { await prisma.task.deleteMany({ where: { id: { in: seedTaskIds, }, }, }); } const lifecycleTaskA = await prisma.task.create({ data: { status: TaskStatus.COMPLETED, creatorId: doctorA.id, engineerId: engineerA.id, hospitalId: hospitalA.id, items: { create: [ { deviceId: deviceA1.id, oldPressure: 118, targetPressure: 120, }, ], }, }, include: { items: true }, }); const lifecycleTaskB = await prisma.task.create({ data: { status: TaskStatus.PENDING, creatorId: doctorB.id, engineerId: engineerB.id, hospitalId: hospitalB.id, items: { create: [ { deviceId: deviceB1.id, oldPressure: 121, targetPressure: 119, }, ], }, }, include: { items: true }, }); console.log( JSON.stringify( { ok: true, seedPasswordPlain: SEED_PASSWORD_PLAIN, hospitals: { hospitalAId: hospitalA.id, hospitalBId: hospitalB.id, }, departments: { departmentA1Id: departmentA1.id, departmentA2Id: departmentA2.id, departmentB1Id: departmentB1.id, }, groups: { groupA1Id: groupA1.id, groupA2Id: groupA2.id, groupB1Id: groupB1.id, }, users: { systemAdminId: systemAdmin.id, hospitalAdminAId: hospitalAdminA.id, directorAId: directorA.id, leaderAId: leaderA.id, doctorAId: doctorA.id, doctorA2Id: doctorA2.id, doctorA3Id: doctorA3.id, doctorBId: doctorB.id, engineerAId: engineerA.id, engineerBId: engineerB.id, }, patients: { patientA1Id: patientA1.id, patientA2Id: patientA2.id, patientA3Id: patientA3.id, patientB1Id: patientB1.id, }, devices: { deviceA1Id: deviceA1.id, deviceA2Id: deviceA2.id, deviceB1Id: deviceB1.id, }, tasks: { lifecycleTaskAId: lifecycleTaskA.id, lifecycleTaskBId: lifecycleTaskB.id, }, }, null, 2, ), ); } main() .catch((error) => { console.error('Seed failed:', error); process.exitCode = 1; }) .finally(async () => { await prisma.$disconnect(); });