鉴权改为登录态回库校验,新增 tokenValidAfter 失效时间,支持密码变更与 seed 重置后旧 token 立即失效 患者字段由 idCardHash 统一迁移为 idCard,新增身份证标准化逻辑并同步 C 端生命周期查询参数 组织模块增加小组删除限制(有成员时返回 409)并补充中文错误消息 任务取消接口支持可选 reason 字段(先透传事件层) 补齐 Prisma 迁移、文档说明和 E2E 用例(含设备模块与 token 失效场景)
448 lines
11 KiB
JavaScript
448 lines
11 KiB
JavaScript
import 'dotenv/config';
|
|
import { PrismaPg } from '@prisma/adapter-pg';
|
|
import { hash } from 'bcrypt';
|
|
import prismaClientPackage from '@prisma/client';
|
|
|
|
const { 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, phone, idCard }) {
|
|
const existing = await prisma.patient.findFirst({
|
|
where: {
|
|
hospitalId,
|
|
phone,
|
|
idCard,
|
|
},
|
|
});
|
|
|
|
if (existing) {
|
|
if (existing.doctorId !== doctorId || existing.name !== name) {
|
|
return prisma.patient.update({
|
|
where: { id: existing.id },
|
|
data: { doctorId, name },
|
|
});
|
|
}
|
|
return existing;
|
|
}
|
|
|
|
return prisma.patient.create({
|
|
data: {
|
|
hospitalId,
|
|
doctorId,
|
|
name,
|
|
phone,
|
|
idCard,
|
|
},
|
|
});
|
|
}
|
|
|
|
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 patientA1 = await ensurePatient({
|
|
hospitalId: hospitalA.id,
|
|
doctorId: doctorA.id,
|
|
name: 'Seed Patient A1',
|
|
phone: '13800002001',
|
|
idCard: '110101199001010011',
|
|
});
|
|
|
|
const patientA2 = await ensurePatient({
|
|
hospitalId: hospitalA.id,
|
|
doctorId: doctorA2.id,
|
|
name: 'Seed Patient A2',
|
|
phone: '13800002002',
|
|
idCard: '110101199002020022',
|
|
});
|
|
|
|
const patientA3 = await ensurePatient({
|
|
hospitalId: hospitalA.id,
|
|
doctorId: doctorA3.id,
|
|
name: 'Seed Patient A3',
|
|
phone: '13800002003',
|
|
idCard: '110101199003030033',
|
|
});
|
|
|
|
const patientB1 = await ensurePatient({
|
|
hospitalId: hospitalB.id,
|
|
doctorId: doctorB.id,
|
|
name: 'Seed Patient B1',
|
|
phone: '13800002001',
|
|
idCard: '110101199001010011',
|
|
});
|
|
|
|
const deviceA1 = await prisma.device.upsert({
|
|
where: { snCode: 'SEED-SN-A-001' },
|
|
update: {
|
|
patientId: patientA1.id,
|
|
currentPressure: 118,
|
|
status: DeviceStatus.ACTIVE,
|
|
},
|
|
create: {
|
|
snCode: 'SEED-SN-A-001',
|
|
patientId: patientA1.id,
|
|
currentPressure: 118,
|
|
status: DeviceStatus.ACTIVE,
|
|
},
|
|
});
|
|
|
|
const deviceA2 = await prisma.device.upsert({
|
|
where: { snCode: 'SEED-SN-A-002' },
|
|
update: {
|
|
patientId: patientA2.id,
|
|
currentPressure: 112,
|
|
status: DeviceStatus.ACTIVE,
|
|
},
|
|
create: {
|
|
snCode: 'SEED-SN-A-002',
|
|
patientId: patientA2.id,
|
|
currentPressure: 112,
|
|
status: DeviceStatus.ACTIVE,
|
|
},
|
|
});
|
|
|
|
await prisma.device.upsert({
|
|
where: { snCode: 'SEED-SN-A-003' },
|
|
update: {
|
|
patientId: patientA3.id,
|
|
currentPressure: 109,
|
|
status: DeviceStatus.ACTIVE,
|
|
},
|
|
create: {
|
|
snCode: 'SEED-SN-A-003',
|
|
patientId: patientA3.id,
|
|
currentPressure: 109,
|
|
status: DeviceStatus.ACTIVE,
|
|
},
|
|
});
|
|
|
|
const deviceB1 = await prisma.device.upsert({
|
|
where: { snCode: 'SEED-SN-B-001' },
|
|
update: {
|
|
patientId: patientB1.id,
|
|
currentPressure: 121,
|
|
status: DeviceStatus.ACTIVE,
|
|
},
|
|
create: {
|
|
snCode: 'SEED-SN-B-001',
|
|
patientId: patientB1.id,
|
|
currentPressure: 121,
|
|
status: DeviceStatus.ACTIVE,
|
|
},
|
|
});
|
|
|
|
await prisma.device.upsert({
|
|
where: { snCode: 'SEED-SN-A-004' },
|
|
update: {
|
|
patientId: patientA1.id,
|
|
currentPressure: 130,
|
|
status: DeviceStatus.INACTIVE,
|
|
},
|
|
create: {
|
|
snCode: 'SEED-SN-A-004',
|
|
patientId: patientA1.id,
|
|
currentPressure: 130,
|
|
status: DeviceStatus.INACTIVE,
|
|
},
|
|
});
|
|
|
|
// 清理与种子设备关联的历史任务,保证 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();
|
|
});
|