tyt-api-nest/prisma/seed.mjs
EL 73082225f6 "1. 新增系统字典与全局植入目录相关表结构及迁移
2. 扩展患者手术与材料模型,更新种子数据
3. 新增字典模块,增强设备植入目录管理能力
4. 重构患者后台服务与表单链路,统一权限与参数校验
5. 管理台新增字典页面并改造患者/设备页面与路由权限
6. 补充字典及相关领域 e2e 测试并更新文档"
2026-03-19 20:42:17 +08:00

849 lines
23 KiB
JavaScript

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,
name,
inpatientNo = null,
projectName = null,
phone,
idCard,
}) {
const existing = await prisma.patient.findFirst({
where: {
hospitalId,
phone,
idCard,
},
});
if (existing) {
if (
existing.doctorId !== doctorId ||
existing.name !== name ||
existing.inpatientNo !== inpatientNo ||
existing.projectName !== projectName
) {
return prisma.patient.update({
where: { id: existing.id },
data: { doctorId, name, inpatientNo, projectName },
});
}
return existing;
}
return prisma.patient.create({
data: {
hospitalId,
doctorId,
name,
inpatientNo,
projectName,
phone,
idCard,
},
});
}
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 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,
name: 'Seed Patient A1',
inpatientNo: 'ZYH-A-0001',
projectName: '脑积水随访项目-A',
phone: '13800002001',
idCard: '110101199001010011',
});
const patientA2 = await ensurePatient({
hospitalId: hospitalA.id,
doctorId: 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,
name: 'Seed Patient A3',
inpatientNo: 'ZYH-A-0003',
projectName: '脑积水随访项目-A',
phone: '13800002003',
idCard: '110101199003030033',
});
const patientB1 = await ensurePatient({
hospitalId: hospitalB.id,
doctorId: doctorB.id,
name: 'Seed Patient B1',
inpatientNo: 'ZYH-B-0001',
projectName: '脑积水随访项目-B',
phone: '13800002001',
idCard: '110101199001010011',
});
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 prisma.device.upsert({
where: { snCode: 'SEED-SN-A-001' },
update: {
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',
},
create: {
snCode: 'SEED-SN-A-001',
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 prisma.device.upsert({
where: { snCode: 'SEED-SN-A-002' },
update: {
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',
},
create: {
snCode: 'SEED-SN-A-002',
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 prisma.device.upsert({
where: { snCode: 'SEED-SN-A-003' },
update: {
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',
},
create: {
snCode: 'SEED-SN-A-003',
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 prisma.device.upsert({
where: { snCode: 'SEED-SN-B-001' },
update: {
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',
},
create: {
snCode: 'SEED-SN-B-001',
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 prisma.device.upsert({
where: { snCode: 'SEED-SN-A-004' },
update: {
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',
},
create: {
snCode: 'SEED-SN-A-004',
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();
});