feat(c5): 角色权限矩阵 + 测试机构 seed-demo
- BUSINESS_ROLES 补全业务权限(director 只读/qc 主角色/leader 派单/nurse 留痕) - seed-demo.ts 初始化「投云养老服务机构」+ 5 账号 + 完整样例数据 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
9d1a3b2374
commit
f0e3deb6c8
@ -27,6 +27,7 @@
|
|||||||
"test:debug": "vitest --inspect-brk --no-file-parallelism",
|
"test:debug": "vitest --inspect-brk --no-file-parallelism",
|
||||||
"test:e2e": "vitest run --config ./vitest.config.e2e.ts",
|
"test:e2e": "vitest run --config ./vitest.config.e2e.ts",
|
||||||
"seed": "prisma db seed",
|
"seed": "prisma db seed",
|
||||||
|
"seed:demo": "tsx prisma/seed-demo.ts",
|
||||||
"sync:resources": "tsx prisma/sync-resources.ts"
|
"sync:resources": "tsx prisma/sync-resources.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
557
prisma/seed-demo.ts
Normal file
557
prisma/seed-demo.ts
Normal file
@ -0,0 +1,557 @@
|
|||||||
|
import { 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): Record<string, unknown> {
|
||||||
|
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();
|
||||||
|
});
|
||||||
@ -145,7 +145,24 @@ export const BUTTONS = [
|
|||||||
|
|
||||||
/** 业务租户预置角色(C0:nurse/leader/qc/admin/director)及初始授权。 */
|
/** 业务租户预置角色(C0:nurse/leader/qc/admin/director)及初始授权。 */
|
||||||
export const BUSINESS_ROLES: { code: string; name: string; permissions: string[] }[] = [
|
export const BUSINESS_ROLES: { code: string; name: string; permissions: string[] }[] = [
|
||||||
{ code: 'director', name: '院长', permissions: [Permissions.agency.view] },
|
{
|
||||||
|
code: 'director',
|
||||||
|
name: '院长',
|
||||||
|
permissions: [
|
||||||
|
Permissions.agency.view,
|
||||||
|
Permissions.elder.list,
|
||||||
|
Permissions.elder.detail,
|
||||||
|
Permissions.plan.detail,
|
||||||
|
Permissions.order.detail,
|
||||||
|
Permissions.order.today,
|
||||||
|
Permissions.record.list,
|
||||||
|
Permissions.record.detail,
|
||||||
|
Permissions.abnormal.list,
|
||||||
|
Permissions.qc.pendingList,
|
||||||
|
Permissions.qc.detail,
|
||||||
|
Permissions.export.export,
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
code: 'admin',
|
code: 'admin',
|
||||||
name: '机构管理员',
|
name: '机构管理员',
|
||||||
@ -169,21 +186,67 @@ export const BUSINESS_ROLES: { code: string; name: string; permissions: string[]
|
|||||||
Permissions.agency.view,
|
Permissions.agency.view,
|
||||||
Permissions.staff.list,
|
Permissions.staff.list,
|
||||||
Permissions.svcOrg.list,
|
Permissions.svcOrg.list,
|
||||||
Permissions.svcOrg.config,
|
Permissions.elder.list,
|
||||||
Permissions.svcOrg.newNational,
|
Permissions.elder.create,
|
||||||
|
Permissions.elder.detail,
|
||||||
|
Permissions.elder.statusChange,
|
||||||
|
Permissions.elder.svcSave,
|
||||||
|
Permissions.plan.create,
|
||||||
|
Permissions.plan.change,
|
||||||
|
Permissions.plan.detail,
|
||||||
|
Permissions.plan.confirm,
|
||||||
|
Permissions.order.detail,
|
||||||
|
Permissions.order.today,
|
||||||
|
Permissions.record.list,
|
||||||
|
Permissions.record.detail,
|
||||||
|
Permissions.abnormal.list,
|
||||||
|
Permissions.abnormal.report,
|
||||||
|
Permissions.abnormal.handle,
|
||||||
Permissions.qc.pendingList,
|
Permissions.qc.pendingList,
|
||||||
Permissions.qc.detail,
|
Permissions.qc.detail,
|
||||||
Permissions.qc.pass,
|
Permissions.qc.pass,
|
||||||
Permissions.qc.return,
|
Permissions.qc.return,
|
||||||
Permissions.qc.exceptionLink,
|
Permissions.qc.exceptionLink,
|
||||||
Permissions.qc.batchPass,
|
Permissions.qc.batchPass,
|
||||||
Permissions.abnormal.list,
|
|
||||||
Permissions.abnormal.handle,
|
|
||||||
Permissions.export.export,
|
Permissions.export.export,
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{ code: 'leader', name: '组长', permissions: [Permissions.staff.list, Permissions.svcOrg.list] },
|
{
|
||||||
{ code: 'nurse', name: '护理员', permissions: [] },
|
code: 'leader',
|
||||||
|
name: '组长',
|
||||||
|
permissions: [
|
||||||
|
Permissions.staff.list,
|
||||||
|
Permissions.svcOrg.list,
|
||||||
|
Permissions.elder.list,
|
||||||
|
Permissions.elder.detail,
|
||||||
|
Permissions.plan.detail,
|
||||||
|
Permissions.order.detail,
|
||||||
|
Permissions.order.today,
|
||||||
|
Permissions.order.assign,
|
||||||
|
Permissions.order.reassign,
|
||||||
|
Permissions.record.list,
|
||||||
|
Permissions.record.detail,
|
||||||
|
Permissions.abnormal.list,
|
||||||
|
Permissions.qc.pendingList,
|
||||||
|
Permissions.qc.detail,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'nurse',
|
||||||
|
name: '护理员',
|
||||||
|
permissions: [
|
||||||
|
Permissions.record.checkin,
|
||||||
|
Permissions.record.itemSave,
|
||||||
|
Permissions.record.photoUpload,
|
||||||
|
Permissions.record.checkout,
|
||||||
|
Permissions.record.offlineSync,
|
||||||
|
Permissions.record.detail,
|
||||||
|
Permissions.record.list,
|
||||||
|
Permissions.qc.correct,
|
||||||
|
Permissions.order.detail,
|
||||||
|
Permissions.order.today,
|
||||||
|
],
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
/** 内置枚举字典(值对齐 enums.ts,仅展示层 label/颜色/排序可改)。 */
|
/** 内置枚举字典(值对齐 enums.ts,仅展示层 label/颜色/排序可改)。 */
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user