558 lines
20 KiB
TypeScript
558 lines
20 KiB
TypeScript
import { Prisma, PrismaClient } from '@prisma/client';
|
||
import bcrypt from 'bcryptjs';
|
||
import { TENANT_ADMIN_ROLE_CODE } from '../src/common/constants.js';
|
||
import { initTenant } from '../src/common/tenant-template.js';
|
||
|
||
/**
|
||
* 测试机构(TY20261015 投云养老服务机构)演示数据 seed。
|
||
* 幂等:可重复执行(tenant/user/userRole/staff/elder/svc_org 按业务键 upsert,plan/order/record/abnormal/qc 用固定 id)。
|
||
*
|
||
* 运行:pnpm exec tsx prisma/seed-demo.ts
|
||
*/
|
||
const prisma = new PrismaClient();
|
||
|
||
const TENANT_CODE = 'TY20261015';
|
||
const TENANT_NAME = '投云养老服务机构';
|
||
const ADMIN_USERNAME = 'admin01';
|
||
const PASSWORD = 'Touyun@123';
|
||
const BIZ_LINE = 'ltci';
|
||
const LOC_CODE = '530100';
|
||
|
||
// 固定业务 id(幂等锚点)
|
||
const IDS = {
|
||
staffLeader: 'demo-staff-leader-0001',
|
||
staffNurseZhang: 'demo-staff-nurse-0001',
|
||
staffNurseLi: 'demo-staff-nurse-0002',
|
||
staffNurseWang: 'demo-staff-nurse-0003',
|
||
planZhang: 'demo-plan-zhang-0001',
|
||
orderZhang: 'demo-order-zhang-0001',
|
||
recordZhang: 'demo-record-zhang-0001',
|
||
abnormalZhang: 'demo-abnormal-zhang-0001',
|
||
qcZhang: 'demo-qc-zhang-0001',
|
||
} as const;
|
||
|
||
// L1 目录样例(10 项)+ L4 拍照分档/配价(photoTier:沐浴/压疮/鼻饲/导管=must,擦浴=optional,其余 free)
|
||
const SERVICES: { natCode: string; natName: string; natCategory: string; isMedical: boolean; photoTier: string; price: string }[] = [
|
||
{ natCode: 'L0000106010020001', natName: '协助翻身', natCategory: 'life', isMedical: false, photoTier: 'free', price: '25.00' },
|
||
{ natCode: 'L0000106010020002', natName: '协助进食', natCategory: 'life', isMedical: false, photoTier: 'free', price: '25.00' },
|
||
{ natCode: 'L0000106010020003', natName: '口腔清洁', natCategory: 'life', isMedical: false, photoTier: 'free', price: '20.00' },
|
||
{ natCode: 'L0000106010020004', natName: '沐浴', natCategory: 'life', isMedical: false, photoTier: 'must', price: '50.00' },
|
||
{ natCode: 'L0000106010020005', natName: '压疮护理', natCategory: 'medical', isMedical: true, photoTier: 'must', price: '60.00' },
|
||
{ natCode: 'L0000106010020006', natName: '擦浴', natCategory: 'life', isMedical: false, photoTier: 'optional', price: '40.00' },
|
||
{ natCode: 'L0000106010020007', natName: '鼻饲', natCategory: 'medical', isMedical: true, photoTier: 'must', price: '45.00' },
|
||
{ natCode: 'L0000106010020008', natName: '导管护理', natCategory: 'medical', isMedical: true, photoTier: 'must', price: '55.00' },
|
||
{ natCode: 'L0000106010020009', natName: '血糖监测', natCategory: 'medical', isMedical: true, photoTier: 'free', price: '15.00' },
|
||
{ natCode: 'L0000106010020010', natName: '关节活动', natCategory: 'life', isMedical: false, photoTier: 'free', price: '30.00' },
|
||
];
|
||
|
||
// 业务角色账号(admin01 由 initTenant 创建并绑 tenant_super_admin)
|
||
const ACCOUNTS = [
|
||
{ username: 'director01', roleCode: 'director', nickname: '院长' },
|
||
{ username: 'qc01', roleCode: 'qc', nickname: '质控员' },
|
||
{ username: 'leader01', roleCode: 'leader', nickname: '组长' },
|
||
{ username: 'nurse01', roleCode: 'nurse', nickname: '护理员' },
|
||
];
|
||
|
||
// ---- 日期工具(@db.Date 用 UTC 零点,datetime 用本地墙上时间) ----
|
||
|
||
function pad(n: number): string {
|
||
return String(n).padStart(2, '0');
|
||
}
|
||
|
||
/** YYYY-MM-DD → @db.Date(UTC 零点)。 */
|
||
function dateOnly(s: string): Date {
|
||
return new Date(`${s}T00:00:00Z`);
|
||
}
|
||
|
||
/** 今天(本地)YYYY-MM-DD。 */
|
||
function todayStr(): string {
|
||
const d = new Date();
|
||
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
|
||
}
|
||
|
||
/** 距今天 N 天的 YYYY-MM-DD(本地)。 */
|
||
function daysFromToday(days: number): string {
|
||
const d = new Date();
|
||
d.setDate(d.getDate() + days);
|
||
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
|
||
}
|
||
|
||
/** 本月最后一天 YYYY-MM-DD。 */
|
||
function lastDayOfMonth(): string {
|
||
const d = new Date();
|
||
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(new Date(d.getFullYear(), d.getMonth() + 1, 0).getDate())}`;
|
||
}
|
||
|
||
/** 今天本地时间 HH:mm 的 Date。 */
|
||
function todayAt(hour: number, minute: number): Date {
|
||
const d = new Date();
|
||
d.setHours(hour, minute, 0, 0);
|
||
return d;
|
||
}
|
||
|
||
// ---- GB11643 校验位(生成合法 18 位身份证) ----
|
||
|
||
const ID_WEIGHTS = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2];
|
||
const ID_CHECK_CODES = ['1', '0', 'X', '9', '8', '7', '6', '5', '4', '3', '2'];
|
||
|
||
function idCard(prefix17: string): string {
|
||
const sum = ID_WEIGHTS.reduce((acc, w, i) => acc + w * Number(prefix17[i]), 0);
|
||
return prefix17 + ID_CHECK_CODES[sum % 11];
|
||
}
|
||
|
||
// 关键老人身份证(张三用任务给定值,李四与 staff 用校验位生成)
|
||
const ID_ZHANG = '530102199001011231';
|
||
const ID_LI = idCard('53010219900303342');
|
||
|
||
// ---- 机构项目快照结构 ----
|
||
|
||
interface SvcRecord {
|
||
svcId: string;
|
||
svcName: string;
|
||
svcCode: string;
|
||
photoTier: string;
|
||
isMedical: boolean;
|
||
needCert: boolean;
|
||
}
|
||
|
||
function itemSnapshot(s: SvcRecord): Prisma.InputJsonObject {
|
||
return {
|
||
svcId: s.svcId,
|
||
svcName: s.svcName,
|
||
svcCode: s.svcCode,
|
||
freqType: null,
|
||
freqValue: null,
|
||
durationMin: null,
|
||
photoTier: s.photoTier,
|
||
needCert: s.needCert,
|
||
isMedical: s.isMedical,
|
||
};
|
||
}
|
||
|
||
async function main(): Promise<void> {
|
||
// 1. 机构(租户)+ 平台化初始化(admin01 + 资源 + 内置字典 + business roles 授权)
|
||
await prisma.tenant.upsert({
|
||
where: { code: TENANT_CODE },
|
||
update: { name: TENANT_NAME, status: 'enabled' },
|
||
create: { code: TENANT_CODE, name: TENANT_NAME, status: 'enabled' },
|
||
});
|
||
await initTenant({
|
||
prisma,
|
||
tenantCode: TENANT_CODE,
|
||
tenantName: TENANT_NAME,
|
||
adminRoleCode: TENANT_ADMIN_ROLE_CODE,
|
||
adminRoleName: '机构管理员',
|
||
adminUsername: ADMIN_USERNAME,
|
||
adminPassword: PASSWORD,
|
||
includePlatformPerms: false,
|
||
forcePasswordChange: false,
|
||
});
|
||
|
||
// 2. 角色账号 + 绑角色
|
||
const passwordHash = await bcrypt.hash(PASSWORD, 10);
|
||
const userIds: Record<string, string> = {};
|
||
for (const acc of ACCOUNTS) {
|
||
const user = await prisma.user.upsert({
|
||
where: { tenantCode_username: { tenantCode: TENANT_CODE, username: acc.username } },
|
||
update: { nickname: acc.nickname, status: 'enabled' },
|
||
create: {
|
||
tenantCode: TENANT_CODE,
|
||
username: acc.username,
|
||
passwordHash,
|
||
nickname: acc.nickname,
|
||
status: 'enabled',
|
||
orgCode: TENANT_CODE,
|
||
},
|
||
});
|
||
userIds[acc.username] = user.id;
|
||
await prisma.userRole.upsert({
|
||
where: { tenantCode_userId_roleCode: { tenantCode: TENANT_CODE, userId: user.id, roleCode: acc.roleCode } },
|
||
update: {},
|
||
create: { tenantCode: TENANT_CODE, userId: user.id, roleCode: acc.roleCode },
|
||
});
|
||
}
|
||
|
||
// 3. staff:1 组长(刘组长,绑 leader01)+ 3 护理员(张/李/王,张护理绑 nurse01)
|
||
const leaderId = IDS.staffLeader;
|
||
await prisma.staff.upsert({
|
||
where: { id: leaderId },
|
||
update: {
|
||
userId: userIds.leader01,
|
||
name: '刘组长',
|
||
idCardNo: idCard('53010219850101004'),
|
||
jobType: 'leader',
|
||
workType: 'fulltime',
|
||
employStatus: 'on_job',
|
||
groupLeaderId: null,
|
||
},
|
||
create: {
|
||
id: leaderId,
|
||
tenantCode: TENANT_CODE,
|
||
userId: userIds.leader01,
|
||
name: '刘组长',
|
||
idCardNo: idCard('53010219850101004'),
|
||
phone: '13800000004',
|
||
jobType: 'leader',
|
||
workType: 'fulltime',
|
||
entryDate: dateOnly('2024-01-01'),
|
||
employStatus: 'on_job',
|
||
groupLeaderId: null,
|
||
},
|
||
});
|
||
|
||
const nurseDefs = [
|
||
{ id: IDS.staffNurseZhang, userId: userIds.nurse01, name: '张护理', idPrefix: '53010219880515001', phone: '13800000001' },
|
||
{ id: IDS.staffNurseLi, userId: null, name: '李护理', idPrefix: '53010219920310002', phone: '13800000002' },
|
||
{ id: IDS.staffNurseWang, userId: null, name: '王护理', idPrefix: '53010219941025003', phone: '13800000003' },
|
||
];
|
||
const nurseStaffIds: Record<string, string> = {};
|
||
for (const nd of nurseDefs) {
|
||
await prisma.staff.upsert({
|
||
where: { id: nd.id },
|
||
update: {
|
||
userId: nd.userId,
|
||
name: nd.name,
|
||
idCardNo: idCard(nd.idPrefix),
|
||
jobType: 'nurse',
|
||
workType: 'fulltime',
|
||
employStatus: 'on_job',
|
||
groupLeaderId: leaderId,
|
||
},
|
||
create: {
|
||
id: nd.id,
|
||
tenantCode: TENANT_CODE,
|
||
userId: nd.userId,
|
||
name: nd.name,
|
||
idCardNo: idCard(nd.idPrefix),
|
||
phone: nd.phone,
|
||
jobType: 'nurse',
|
||
workType: 'fulltime',
|
||
entryDate: dateOnly('2024-06-01'),
|
||
employStatus: 'on_job',
|
||
groupLeaderId: leaderId,
|
||
},
|
||
});
|
||
nurseStaffIds[nd.name] = nd.id;
|
||
}
|
||
const zhangStaffId = nurseStaffIds['张护理'];
|
||
|
||
// 4. L1 国家目录 + L4 机构项目库(srcId = svc_national.id,与 plan/record 解析逻辑一致)
|
||
const svcs: SvcRecord[] = [];
|
||
for (const svc of SERVICES) {
|
||
const national = await prisma.svcNational.upsert({
|
||
where: { natCode: svc.natCode },
|
||
update: { natName: svc.natName, natCategory: svc.natCategory, isMedical: svc.isMedical },
|
||
create: { natCode: svc.natCode, natName: svc.natName, natCategory: svc.natCategory, isMedical: svc.isMedical },
|
||
});
|
||
const existing = await prisma.svcOrg.findFirst({ where: { tenantCode: TENANT_CODE, srcId: national.id } });
|
||
let svcOrgId: string;
|
||
if (existing) {
|
||
await prisma.svcOrg.update({
|
||
where: { id: existing.id },
|
||
data: { srcType: 'national', isEnabled: true, price: svc.price, photoTier: svc.photoTier },
|
||
});
|
||
svcOrgId = existing.id;
|
||
} else {
|
||
const created = await prisma.svcOrg.create({
|
||
data: {
|
||
tenantCode: TENANT_CODE,
|
||
srcType: 'national',
|
||
srcId: national.id,
|
||
isEnabled: true,
|
||
price: svc.price,
|
||
photoTier: svc.photoTier,
|
||
},
|
||
});
|
||
svcOrgId = created.id;
|
||
}
|
||
svcs.push({
|
||
svcId: svcOrgId,
|
||
svcName: svc.natName,
|
||
svcCode: svc.natCode,
|
||
photoTier: svc.photoTier,
|
||
isMedical: svc.isMedical,
|
||
needCert: svc.isMedical,
|
||
});
|
||
}
|
||
|
||
// 5. 老人:张三(active)、李四(waiting_period,垫付演示)
|
||
const zhang = await prisma.elder.upsert({
|
||
where: { tenantCode_idCardNo: { tenantCode: TENANT_CODE, idCardNo: ID_ZHANG } },
|
||
update: {},
|
||
create: {
|
||
tenantCode: TENANT_CODE,
|
||
locCode: LOC_CODE,
|
||
name: '张三',
|
||
gender: 'M',
|
||
birthDate: dateOnly('1990-01-01'),
|
||
idCardNo: ID_ZHANG,
|
||
phone: '13900000001',
|
||
address: '昆明市五华区春城路 1 号',
|
||
guardianName: '张小明',
|
||
guardianPhone: '13900000002',
|
||
insurNo: 'YN5301000001',
|
||
insurType: 'employee',
|
||
careLevel: 'care_1',
|
||
treatStatus: 'active',
|
||
benefitStart: dateOnly(daysFromToday(-365)),
|
||
benefitEnd: dateOnly(daysFromToday(365)),
|
||
bizLine: BIZ_LINE,
|
||
},
|
||
});
|
||
|
||
await prisma.elder.upsert({
|
||
where: { tenantCode_idCardNo: { tenantCode: TENANT_CODE, idCardNo: ID_LI } },
|
||
update: {},
|
||
create: {
|
||
tenantCode: TENANT_CODE,
|
||
locCode: LOC_CODE,
|
||
name: '李四',
|
||
gender: 'F',
|
||
birthDate: dateOnly('1990-03-03'),
|
||
idCardNo: ID_LI,
|
||
phone: '13900000003',
|
||
address: '昆明市盘龙区东风路 2 号',
|
||
insurNo: 'YN5301000002',
|
||
insurType: 'resident',
|
||
careLevel: 'care_2',
|
||
treatStatus: 'waiting_period',
|
||
benefitStart: dateOnly(daysFromToday(30)),
|
||
benefitEnd: dateOnly(daysFromToday(400)),
|
||
bizLine: BIZ_LINE,
|
||
},
|
||
});
|
||
|
||
// 6. 计划(张三,active,覆盖本月)+ 计划项(勾选全部 10 项,photoTier 快照自 svc_org)
|
||
const planId = IDS.planZhang;
|
||
const planPeriodStart = `${new Date().getFullYear()}-${pad(new Date().getMonth() + 1)}-01`;
|
||
await prisma.plan.upsert({
|
||
where: { id: planId },
|
||
update: {
|
||
elderId: zhang.id,
|
||
planPeriodStart: dateOnly(planPeriodStart),
|
||
planPeriodEnd: dateOnly(lastDayOfMonth()),
|
||
careLevel: 'care_1',
|
||
careMode: 'home_individual',
|
||
itemCount: svcs.length,
|
||
planStatus: 'active',
|
||
},
|
||
create: {
|
||
id: planId,
|
||
tenantCode: TENANT_CODE,
|
||
elderId: zhang.id,
|
||
planPeriodStart: dateOnly(planPeriodStart),
|
||
planPeriodEnd: dateOnly(lastDayOfMonth()),
|
||
careLevel: 'care_1',
|
||
careMode: 'home_individual',
|
||
itemCount: svcs.length,
|
||
planStatus: 'active',
|
||
bizLine: BIZ_LINE,
|
||
},
|
||
});
|
||
await prisma.planItem.deleteMany({ where: { planId } });
|
||
for (const s of svcs) {
|
||
await prisma.planItem.create({
|
||
data: {
|
||
planId,
|
||
tenantCode: TENANT_CODE,
|
||
svcId: s.svcId,
|
||
svcNameSnapshot: s.svcName,
|
||
svcCodeSnapshot: s.svcCode,
|
||
photoTier: s.photoTier,
|
||
isMedical: s.isMedical,
|
||
needCert: s.needCert,
|
||
},
|
||
});
|
||
}
|
||
|
||
// 7. 工单(张三,今天 09:00-10:00,派给张护理,itemList 快照自 plan_item)
|
||
const orderId = IDS.orderZhang;
|
||
const itemList = svcs.map(itemSnapshot);
|
||
const needPhoto = svcs.some((s) => s.photoTier === 'must');
|
||
await prisma.order.upsert({
|
||
where: { id: orderId },
|
||
update: {
|
||
elderId: zhang.id,
|
||
planId,
|
||
serviceDate: dateOnly(todayStr()),
|
||
timeSlotStart: todayAt(9, 0),
|
||
timeSlotEnd: todayAt(10, 0),
|
||
assigneeId: zhangStaffId,
|
||
orderStatus: 'assigned',
|
||
itemList,
|
||
needPhoto,
|
||
},
|
||
create: {
|
||
id: orderId,
|
||
tenantCode: TENANT_CODE,
|
||
elderId: zhang.id,
|
||
planId,
|
||
serviceDate: dateOnly(todayStr()),
|
||
timeSlotStart: todayAt(9, 0),
|
||
timeSlotEnd: todayAt(10, 0),
|
||
assigneeId: zhangStaffId,
|
||
assignTime: new Date(),
|
||
assignMode: 'manual',
|
||
orderStatus: 'assigned',
|
||
itemList,
|
||
needPhoto,
|
||
billable: true,
|
||
bizLine: BIZ_LINE,
|
||
},
|
||
});
|
||
|
||
// 8. 留痕(张三,submitted)+ 明细 + 照片
|
||
const recordId = IDS.recordZhang;
|
||
await prisma.record.upsert({
|
||
where: { id: recordId },
|
||
update: {
|
||
orderId,
|
||
elderId: zhang.id,
|
||
assigneeId: zhangStaffId,
|
||
checkinTime: todayAt(9, 0),
|
||
checkoutTime: todayAt(10, 0),
|
||
durationMin: 60,
|
||
recordStatus: 'submitted',
|
||
},
|
||
create: {
|
||
id: recordId,
|
||
tenantCode: TENANT_CODE,
|
||
orderId,
|
||
elderId: zhang.id,
|
||
assigneeId: zhangStaffId,
|
||
checkinTime: todayAt(9, 0),
|
||
checkoutTime: todayAt(10, 0),
|
||
durationMin: 60,
|
||
recordStatus: 'submitted',
|
||
submitTime: todayAt(10, 0),
|
||
isPrepaid: false,
|
||
billable: true,
|
||
bizLine: BIZ_LINE,
|
||
},
|
||
});
|
||
await prisma.recordItem.deleteMany({ where: { recordId } });
|
||
for (const s of svcs) {
|
||
await prisma.recordItem.create({
|
||
data: {
|
||
tenantCode: TENANT_CODE,
|
||
recordId,
|
||
svcId: s.svcId,
|
||
svcNameSnapshot: s.svcName,
|
||
execResult: 'done',
|
||
photoRequired: s.photoTier === 'must',
|
||
},
|
||
});
|
||
}
|
||
await prisma.recordPhoto.deleteMany({ where: { recordId } });
|
||
const mustSvc = svcs.find((s) => s.photoTier === 'must')!;
|
||
await prisma.recordPhoto.create({
|
||
data: {
|
||
tenantCode: TENANT_CODE,
|
||
recordId,
|
||
svcId: mustSvc.svcId,
|
||
filePath: '/demo/TY20261015/record/zhang/photo1.jpg',
|
||
fileSize: 128000,
|
||
visibleToFamily: false,
|
||
},
|
||
});
|
||
|
||
// 9. 异常上报(压疮)
|
||
await prisma.recordAbnormal.upsert({
|
||
where: { id: IDS.abnormalZhang },
|
||
update: {
|
||
recordId,
|
||
elderId: zhang.id,
|
||
reporterId: zhangStaffId,
|
||
abnormalType: 'pressure_sore',
|
||
notifyStatus: 'pending',
|
||
handleStatus: 'open',
|
||
},
|
||
create: {
|
||
id: IDS.abnormalZhang,
|
||
tenantCode: TENANT_CODE,
|
||
recordId,
|
||
orderId,
|
||
elderId: zhang.id,
|
||
reporterId: zhangStaffId,
|
||
abnormalType: 'pressure_sore',
|
||
abnormalDesc: '发现背部皮肤压红,需关注',
|
||
notifyStatus: 'pending',
|
||
handleStatus: 'open',
|
||
},
|
||
});
|
||
|
||
// 10. 质控(pending)
|
||
await prisma.qc.upsert({
|
||
where: { id: IDS.qcZhang },
|
||
update: { recordId, qcType: 'normal', qcResult: 'pending' },
|
||
create: {
|
||
id: IDS.qcZhang,
|
||
tenantCode: TENANT_CODE,
|
||
recordId,
|
||
qcType: 'normal',
|
||
qcResult: 'pending',
|
||
},
|
||
});
|
||
|
||
console.log(`seed-demo 完成:机构 ${TENANT_CODE} ${TENANT_NAME}`);
|
||
}
|
||
|
||
async function verify(): Promise<void> {
|
||
const tenant = await prisma.tenant.findUnique({ where: { code: TENANT_CODE } });
|
||
const users = await prisma.user.findMany({ where: { tenantCode: TENANT_CODE }, orderBy: { username: 'asc' } });
|
||
const roles = await prisma.role.findMany({ where: { tenantCode: TENANT_CODE }, orderBy: { code: 'asc' } });
|
||
const userRoles = await prisma.userRole.findMany({ where: { tenantCode: TENANT_CODE } });
|
||
const staff = await prisma.staff.findMany({ where: { tenantCode: TENANT_CODE }, orderBy: { name: 'asc' } });
|
||
const elders = await prisma.elder.findMany({ where: { tenantCode: TENANT_CODE } });
|
||
const plans = await prisma.plan.findMany({ where: { tenantCode: TENANT_CODE } });
|
||
const planItems = await prisma.planItem.findMany({ where: { tenantCode: TENANT_CODE } });
|
||
const orders = await prisma.order.findMany({ where: { tenantCode: TENANT_CODE } });
|
||
const records = await prisma.record.findMany({ where: { tenantCode: TENANT_CODE } });
|
||
const recordItems = await prisma.recordItem.findMany({ where: { tenantCode: TENANT_CODE } });
|
||
const recordPhotos = await prisma.recordPhoto.findMany({ where: { tenantCode: TENANT_CODE } });
|
||
const abnormals = await prisma.recordAbnormal.findMany({ where: { tenantCode: TENANT_CODE } });
|
||
const qcs = await prisma.qc.findMany({ where: { tenantCode: TENANT_CODE } });
|
||
const svcOrgs = await prisma.svcOrg.findMany({ where: { tenantCode: TENANT_CODE } });
|
||
|
||
const roleByUser: Record<string, string[]> = {};
|
||
for (const ur of userRoles) {
|
||
(roleByUser[ur.userId] ??= []).push(ur.roleCode);
|
||
}
|
||
const nameByUser = new Map(users.map((u) => [u.id, u.username]));
|
||
|
||
console.log('\n===== 验证结果 =====');
|
||
console.log(`tenant: ${tenant ? '存在 ' + tenant.name : '缺失'} (code=${TENANT_CODE})`);
|
||
console.log(`账号数: ${users.length}(期望 5)`);
|
||
for (const u of users) {
|
||
console.log(` - ${u.username} → [${(roleByUser[u.id] ?? []).join(',')}]`);
|
||
}
|
||
console.log(`角色数: ${roles.length}(business roles: ${roles.map((r) => r.code).join('/')})`);
|
||
console.log(`staff: ${staff.length}(期望 4)→ ${staff.map((s) => `${s.name}(${s.jobType},${s.employStatus})`).join(' / ')}`);
|
||
console.log(`elder: ${elders.length}(期望 2)→ ${elders.map((e) => `${e.name}(${e.treatStatus},${e.careLevel})`).join(' / ')}`);
|
||
console.log(`svc_org(L4): ${svcOrgs.length}(期望 10)`);
|
||
console.log(`plan: ${plans.length}(期望 1); plan_item: ${planItems.length}(期望 10)`);
|
||
console.log(`order: ${orders.length}(期望 1); record: ${records.length}(期望 1); record_item: ${recordItems.length}(期望 10); record_photo: ${recordPhotos.length}(期望 1)`);
|
||
console.log(`record_abnormal: ${abnormals.length}(期望 1); qc: ${qcs.length}(期望 1)`);
|
||
console.log('\n===== 测试账号 =====');
|
||
const expected = ['admin01', 'director01', 'qc01', 'leader01', 'nurse01'];
|
||
for (const uname of expected) {
|
||
const u = users.find((x) => x.username === uname);
|
||
console.log(` ${uname} / ${PASSWORD} → [${(u ? roleByUser[u.id] ?? [] : []).join(',')}]`);
|
||
}
|
||
// 未绑任何角色的多余账号告警
|
||
const orphans = users.filter((u) => !expected.includes(u.username));
|
||
if (orphans.length > 0) {
|
||
console.log(`\n⚠️ 存在非预期账号: ${orphans.map((u) => `${u.username}[${nameByUser.get(u.id)}]`).join(', ')}`);
|
||
}
|
||
}
|
||
|
||
main()
|
||
.then(() => verify())
|
||
.catch((e) => {
|
||
console.error(e);
|
||
process.exitCode = 1;
|
||
})
|
||
.finally(async () => {
|
||
await prisma.$disconnect();
|
||
});
|