// @ts-nocheck import { PrismaClient } from '@prisma/client'; import * as argon2 from 'argon2'; import * as fs from 'fs'; import * as path from 'path'; const prisma = new PrismaClient(); const DUMP = path.resolve('/opt/import/prod/oa_hr.json'); const DEFAULT_PASSWORD = '123456'; const DEMO_USERS = [ 'owner', 'bizdir', 'bizstaff', 'dirsoft', 'dir3d', 'dirfilm', 'dirmaterial', 'finance', 'hr', 'employee', 'softpm', 'batchdemo1', ]; const DEPT_META: Record = { 1: { deptType: 'ADMIN' }, 2: { deptType: 'ADMIN' }, 4: { deptType: 'BUSINESS' }, 7: { deptType: 'ADMIN' }, 8: { deptType: 'ADMIN' }, 9: { deptType: 'TECH', categoryCode: 'CAT_SOFTWARE' }, 10: { deptType: 'TECH', categoryCode: 'CAT_SOFTWARE' }, 19: { deptType: 'TECH', categoryCode: 'CAT_FILM' }, 23: { deptType: 'TECH', categoryCode: 'CAT_3D' }, 210: { deptType: 'TECH', categoryCode: 'CAT_SOFTWARE' }, 211: { deptType: 'TECH', categoryCode: 'CAT_3D' }, 212: { deptType: 'TECH', categoryCode: 'CAT_FILM' }, 213: { deptType: 'TECH', categoryCode: 'CAT_MATERIAL' }, 214: { deptType: 'TECH', categoryCode: 'CAT_SOFTWARE' }, }; const OLD_DEPT_TO_OA: Record = { ROOT: 'OA_1', BIZ: 'OA_4', TECH_SOFTWARE: 'OA_9', TECH_3D: 'OA_211', TECH_FILM: 'OA_212', TECH_MATERIAL: 'OA_213', FIN: 'OA_2', HR: 'OA_2', }; const DEPT_MANAGER_OA: Record = { 1: 101, 2: 101, 4: 107, 7: 101, 8: 101, 9: 131, 10: 131, 19: 102, 23: 105, 210: 131, 211: 105, 212: 102, 213: 104, 214: 131, }; const POST_ROLE: Record = { sysadmin: 'admin', boss: 'owner', business_director: 'biz_director', business_staff: 'biz_staff', rd_director: 'rd_director', '3d_director': '3d_director', video_director: 'video_director', material_director: 'material_director', pm: 'pm', tech: 'tech', rd_staff: 'rd_staff', '3d_staff': '3d_staff', video_staff: 'video_staff', material_staff: 'material_staff', }; const ROLE_RANK: Record = { employee: 0, tech: 1, rd_staff: 1, '3d_staff': 1, video_staff: 1, material_staff: 1, pm: 2, biz_staff: 3, tech_director: 4, rd_director: 4, '3d_director': 4, video_director: 4, material_director: 4, biz_director: 5, owner: 6, admin: 7, }; type Dump = { depts: { id: number; name: string; parentId: number; orderNum: number; delFlag: string }[]; posts: { id: number; code: string; name: string }[]; users: { id: number; sex: string; email: string; phone: string; deptId: number; remark: string | null; status: string; nickName: string; userName: string; entryDate: string | null; leaveDate: string | null; employmentStatus: string; }[]; userPosts: { postId: number; userId: number }[]; }; function empNo(oaId: number) { return `E${String(oaId).padStart(4, '0')}`; } function gender(sex: string) { if (sex === '0') return 'MALE'; if (sex === '1') return 'FEMALE'; return null; } function empStatus(code: string) { if (code === '1') return 'INTERN'; if (code === '2') return 'PROBATION'; if (code === '4') return 'LEFT'; return 'REGULAR'; } function when(v: string | null) { if (!v || v.startsWith('0000')) return null; const d = new Date(v.includes('T') ? v : `${v}T00:00:00`); return Number.isNaN(d.getTime()) ? null : d; } function addMonths(d: Date, n: number) { const x = new Date(d); x.setMonth(x.getMonth() + n); return x; } function pickRole(userName: string, postCodes: string[]) { if (userName === 'xibei') return 'employee'; let best: string | null = null; let bestRank = -1; for (const c of postCodes) { const role = POST_ROLE[c]; if (!role) continue; const r = ROLE_RANK[role] ?? 1; if (r > bestRank) { best = role; bestRank = r; } } return best || 'employee'; } async function remumber( rows: T[], prefix: string, update: (id: string, no: string) => Promise, ) { for (const r of rows) await update(r.id, `TMP-${r.id.replace(/-/g, '').slice(0, 20)}`); const groups = new Map(); const sorted = [...rows].sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime() || a.id.localeCompare(b.id)); for (const r of sorted) { const y = r.createdAt.getFullYear(); const list = groups.get(y) || []; list.push(r); groups.set(y, list); } for (const [year, list] of [...groups.entries()].sort((a, b) => a[0] - b[0])) { let i = 1; for (const r of list) { await update(r.id, `${prefix}-${year}-${String(i).padStart(5, '0')}`); i += 1; } } } async function remapDeptFks(codeToId: Map) { const old = await prisma.department.findMany({ where: { tenantId: '1' } }); const oldByCode = new Map(old.map((d) => [d.code, d.id])); const mapping = new Map(); for (const d of old) { const targetCode = OLD_DEPT_TO_OA[d.code] || (d.code.startsWith('OA_') ? d.code : 'OA_2'); const nid = codeToId.get(targetCode); if (nid && nid !== d.id) mapping.set(d.id, nid); } for (const [from, to] of mapping) { await prisma.projectTask.updateMany({ where: { departmentId: from }, data: { departmentId: to } }); await prisma.employmentEvent.updateMany({ where: { fromDeptId: from }, data: { fromDeptId: to } }); await prisma.employmentEvent.updateMany({ where: { toDeptId: from }, data: { toDeptId: to } }); } return { oldByCode, mapping }; } async function reassignDemoUsers(demoIds: string[], adminId: string) { if (!demoIds.length) return; await prisma.bidReviewer.deleteMany({ where: { userId: { in: demoIds } } }); await prisma.bidAssignee.deleteMany({ where: { userId: { in: demoIds } } }); await prisma.contractReviewer.deleteMany({ where: { userId: { in: demoIds } } }); await prisma.todoTask.deleteMany({ where: { assigneeId: { in: demoIds } } }); await prisma.approvalTask.deleteMany({ where: { assigneeId: { in: demoIds } } }); await prisma.workReport.deleteMany({ where: { OR: [{ authorId: { in: demoIds } }, { toUserId: { in: demoIds } }, { ccUserId: { in: demoIds } }] } }); await prisma.refreshToken.deleteMany({ where: { userId: { in: demoIds } } }); await prisma.userRole.deleteMany({ where: { userId: { in: demoIds } } }); const patch = async (model: string, field: string) => { const delegate = (prisma as unknown as Record Promise }>)[model]; if (!delegate?.updateMany) return; try { await delegate.updateMany({ where: { [field]: { in: demoIds } }, data: { [field]: adminId }, }); } catch (e) { console.warn(`skip ${model}.${field}`, (e as Error).message); } }; await patch('bidCase', 'createdById'); await patch('bidActionLog', 'actorId'); await patch('bidDocVersion', 'submittedById'); await patch('contract', 'createdById'); await patch('contractActionLog', 'actorId'); await patch('expenseClaim', 'applicantId'); await patch('expenseClaim', 'decidedById'); await patch('loanRecord', 'applicantId'); await patch('loanRecord', 'decidedById'); await patch('sealRequest', 'applicantId'); await patch('sealRequest', 'decidedById'); await patch('timesheet', 'userId'); await patch('projectTask', 'assigneeId'); await patch('projectAcceptance', 'actorId'); await patch('employmentEvent', 'actorId'); await patch('attendanceMonth', 'importedById'); await patch('payrollRun', 'createdById'); await patch('purchaseRequest', 'applicantId'); await patch('purchaseRequest', 'decidedById'); await patch('paymentRecord', 'createdById'); await patch('projectChange', 'actorId'); await patch('assetOccupancy', 'holderId'); await patch('invoiceRecord', 'createdById'); await patch('operationLog', 'userId'); await patch('credentialBorrow', 'borrowerId'); await prisma.user.deleteMany({ where: { id: { in: demoIds } } }); } async function main() { const dump = JSON.parse(fs.readFileSync(DUMP, 'utf8')) as Dump; const hash = await argon2.hash(DEFAULT_PASSWORD); const posts = new Map(dump.posts.map((p) => [p.id, p])); const postsByUser = new Map(); const postNamesByUser = new Map(); for (const up of dump.userPosts) { const p = posts.get(up.postId); if (!p) continue; postsByUser.set(up.userId, [...(postsByUser.get(up.userId) || []), p.code]); postNamesByUser.set(up.userId, [...(postNamesByUser.get(up.userId) || []), p.name]); } await prisma.role.updateMany({ where: { code: { in: ['admin', 'owner', 'finance', 'hr'] } }, data: { dataScope: 'ALL' } }); await prisma.role.updateMany({ where: { code: { in: ['biz_director', 'tech_director'] } }, data: { dataScope: 'DEPT' } }); await prisma.role.updateMany({ where: { code: { in: ['biz_staff', 'employee'] } }, data: { dataScope: 'SELF' } }); const numberRules = [ { code: 'BID', name: '投标编号', prefix: 'BID', padLength: 5 }, { code: 'HT', name: '合同编号', prefix: 'HT', padLength: 5 }, { code: 'PRJ', name: '项目编号', prefix: 'PRJ', padLength: 5 }, { code: 'BX', name: '报销编号', prefix: 'BX', padLength: 5 }, { code: 'JK', name: '借款编号', prefix: 'JK', padLength: 5 }, { code: 'BZ', name: '保证金编号', prefix: 'BZ', padLength: 5 }, ]; for (const r of numberRules) { await prisma.numberRule.upsert({ where: { tenantId_code: { tenantId: '1', code: r.code } }, update: { prefix: r.prefix, padLength: r.padLength, name: r.name }, create: { ...r, tenantId: '1' }, }); } const roles = await prisma.role.findMany(); const roleByCode = new Map(roles.map((r) => [r.code, r.id])); const codeToId = new Map(); const sortedDepts = [...dump.depts].sort((a, b) => a.parentId - b.parentId || a.orderNum - b.orderNum); for (const d of sortedDepts) { const code = `OA_${d.id}`; const meta = DEPT_META[d.id] || { deptType: 'ADMIN' }; const parentCode = d.parentId ? `OA_${d.parentId}` : null; const parentId = parentCode ? codeToId.get(parentCode) || null : null; const existing = await prisma.department.findFirst({ where: { tenantId: '1', code } }); const row = existing ? await prisma.department.update({ where: { id: existing.id }, data: { name: d.name, deptType: meta.deptType, categoryCode: meta.categoryCode || null, sortNo: d.orderNum, parentId, }, }) : await prisma.department.create({ data: { name: d.name, code, deptType: meta.deptType, categoryCode: meta.categoryCode || null, sortNo: d.orderNum, parentId, }, }); codeToId.set(code, row.id); } await remapDeptFks(codeToId); const officeId = codeToId.get('OA_2')!; const empByOa = new Map(); const empByName = new Map(); const adminUser = await prisma.user.findFirst({ where: { username: 'admin' } }); if (!adminUser) throw new Error('本地必须先有 admin 账号'); const adminEmpId = adminUser.employeeId || ( await prisma.employee.create({ data: { name: '系统管理员', employeeNo: 'E0000', departmentId: officeId, title: '系统管理员', employmentStatus: 'REGULAR', }, }) ).id; const adminOa = dump.users.find((u) => u.userName === 'administrator'); await prisma.employee.update({ where: { id: adminEmpId }, data: { name: adminOa?.nickName || '系统管理员', employeeNo: 'E0000', departmentId: officeId, title: '系统管理员', employmentStatus: 'REGULAR', hiredAt: when(adminOa?.entryDate || '2026-06-04'), gender: gender(adminOa?.sex || '2'), email: adminOa?.email || null, mobile: adminOa?.phone || null, remark: adminOa?.remark || '超级管理员', managerId: null, }, }); empByOa.set(1, adminEmpId); empByName.set('系统管理员', adminEmpId); for (const u of dump.users) { if (u.userName === 'administrator') continue; const deptId = codeToId.get(`OA_${u.deptId}`) || officeId; const status = empStatus(u.employmentStatus); const hiredAt = when(u.entryDate); const titles = postNamesByUser.get(u.id) || []; const title = titles[0] || u.remark || '员工'; const no = empNo(u.id); const data = { name: u.nickName, departmentId: deptId, title, restSchedule: DEPT_META[u.deptId]?.categoryCode === 'CAT_SOFTWARE' ? 'DOUBLE' : 'SINGLE', employmentStatus: status, hiredAt, internStartAt: status === 'INTERN' ? hiredAt : null, probationEndAt: status === 'PROBATION' && hiredAt ? addMonths(hiredAt, 3) : null, leftAt: when(u.leaveDate), gender: gender(u.sex), email: u.email || null, mobile: u.phone || null, remark: u.remark, }; const existing = await prisma.employee.findFirst({ where: { tenantId: '1', employeeNo: no } }); const row = existing ? await prisma.employee.update({ where: { id: existing.id }, data }) : await prisma.employee.create({ data: { ...data, employeeNo: no } }); empByOa.set(u.id, row.id); empByName.set(u.nickName, row.id); } for (const u of dump.users) { if (u.userName === 'administrator') continue; const mgrOa = DEPT_MANAGER_OA[u.deptId]; if (!mgrOa || mgrOa === u.id) continue; const managerId = empByOa.get(mgrOa); const id = empByOa.get(u.id); if (managerId && id) await prisma.employee.update({ where: { id }, data: { managerId } }); } await prisma.user.update({ where: { id: adminUser.id }, data: { passwordHash: hash, displayName: '系统管理员', employeeId: adminEmpId, status: 'ACTIVE', }, }); for (const u of dump.users) { if (u.userName === 'administrator') continue; const employeeId = empByOa.get(u.id)!; const roleCode = pickRole(u.userName, postsByUser.get(u.id) || []); const roleId = roleByCode.get(roleCode); if (!roleId) throw new Error(`缺少角色 ${roleCode}`); const disabled = u.status === '1' || u.employmentStatus === '4'; const existing = await prisma.user.findFirst({ where: { tenantId: '1', username: u.userName } }); const row = existing ? await prisma.user.update({ where: { id: existing.id }, data: { passwordHash: hash, displayName: u.nickName, employeeId, status: disabled ? 'DISABLED' : 'ACTIVE', }, }) : await prisma.user.create({ data: { username: u.userName, passwordHash: hash, displayName: u.nickName, employeeId, status: disabled ? 'DISABLED' : 'ACTIVE', }, }); await prisma.userRole.deleteMany({ where: { userId: row.id } }); await prisma.userRole.create({ data: { userId: row.id, roleId } }); } const users = await prisma.user.findMany({ include: { employee: true } }); const userByName = new Map(users.map((u) => [u.displayName, u.id])); userByName.set('系统管理员', adminUser.id); const rematchByText = async ( model: 'expenseClaim' | 'loanRecord' | 'sealRequest', field: 'remark' | 'reason', ) => { const rows = await prisma[model].findMany({ select: { id: true, [field]: true } as never }); let n = 0; for (const r of rows as { id: string; remark?: string; reason?: string }[]) { const name = ((r.remark || r.reason || '') as string).split(' / ')[0]?.trim(); if (!name) continue; const uid = userByName.get(name); if (!uid) continue; await prisma[model].update({ where: { id: r.id }, data: { applicantId: uid } }); n += 1; } console.log(`matched ${model}`, n); }; await rematchByText('expenseClaim', 'remark'); await rematchByText('loanRecord', 'remark'); await rematchByText('sealRequest', 'reason'); const keepUsernames = new Set(['admin', ...dump.users.filter((u) => u.userName !== 'administrator').map((u) => u.userName)]); const demo = await prisma.user.findMany({ where: { OR: [{ username: { in: DEMO_USERS } }, { username: { notIn: [...keepUsernames] } }] }, }); const demoIds = demo.filter((u) => u.username !== 'admin').map((u) => u.id); console.log( 'delete demo users', demo.filter((u) => u.username !== 'admin').map((u) => u.username), ); await reassignDemoUsers(demoIds, adminUser.id); const keepEmp = new Set([adminEmpId, ...empByOa.values()]); const leftoverEmp = await prisma.employee.findMany({ where: { id: { notIn: [...keepEmp] } } }); const leftoverIds = leftoverEmp.map((e) => e.id); if (leftoverIds.length) { await prisma.payrollItem.deleteMany({ where: { employeeId: { in: leftoverIds } } }); await prisma.attendanceMonth.deleteMany({ where: { employeeId: { in: leftoverIds } } }); await prisma.employmentEvent.deleteMany({ where: { employeeId: { in: leftoverIds } } }); await prisma.user.updateMany({ where: { employeeId: { in: leftoverIds } }, data: { employeeId: null } }); await prisma.employee.updateMany({ where: { managerId: { in: leftoverIds } }, data: { managerId: null } }); await prisma.employee.deleteMany({ where: { id: { in: leftoverIds } } }); } console.log( 'deleted demo employees', leftoverEmp.map((e) => e.name), ); const keepDept = new Set(codeToId.values()); const leftoverDept = await prisma.department.findMany({ where: { id: { notIn: [...keepDept] } } }); if (leftoverDept.length) { const leftoverDeptIds = leftoverDept.map((d) => d.id); await prisma.employee.updateMany({ where: { departmentId: { in: leftoverDeptIds } }, data: { departmentId: officeId }, }); await prisma.projectTask.updateMany({ where: { departmentId: { in: leftoverDeptIds } }, data: { departmentId: officeId }, }); await prisma.department.updateMany({ where: { id: { in: leftoverDeptIds } }, data: { parentId: null } }); await prisma.department.deleteMany({ where: { id: { in: leftoverDeptIds } } }); } console.log( 'deleted demo depts', leftoverDept.map((d) => `${d.code}:${d.name}`), ); const demoBid = await prisma.bidCase.findMany({ where: { name: '某市展厅数字化升级' } }); for (const b of demoBid) { const used = (await prisma.expenseClaim.count({ where: { bidCaseId: b.id } })) + (await prisma.contract.count({ where: { bidCaseId: b.id } })); if (used) continue; await prisma.bidReviewer.deleteMany({ where: { bidCaseId: b.id } }); await prisma.bidAssignee.deleteMany({ where: { bidCaseId: b.id } }); await prisma.bidActionLog.deleteMany({ where: { bidCaseId: b.id } }); await prisma.bidDocVersion.deleteMany({ where: { bidCaseId: b.id } }); await prisma.bondRecord.deleteMany({ where: { bidCaseId: b.id } }); await prisma.sealRequest.deleteMany({ where: { bidCaseId: b.id } }); await prisma.bidCase.delete({ where: { id: b.id } }); console.log('deleted demo bid', b.bidNo); } await remumber( await prisma.bidCase.findMany({ select: { id: true, createdAt: true }, orderBy: { createdAt: 'asc' } }), 'BID', (id, bidNo) => prisma.bidCase.update({ where: { id }, data: { bidNo } }), ); await remumber( await prisma.contract.findMany({ select: { id: true, createdAt: true }, orderBy: { createdAt: 'asc' } }), 'HT', (id, contractNo) => prisma.contract.update({ where: { id }, data: { contractNo } }), ); await remumber( await prisma.project.findMany({ select: { id: true, createdAt: true }, orderBy: { createdAt: 'asc' } }), 'PRJ', (id, projectNo) => prisma.project.update({ where: { id }, data: { projectNo } }), ); await remumber( await prisma.expenseClaim.findMany({ select: { id: true, createdAt: true }, orderBy: { createdAt: 'asc' } }), 'BX', (id, claimNo) => prisma.expenseClaim.update({ where: { id }, data: { claimNo } }), ); await remumber( await prisma.loanRecord.findMany({ select: { id: true, createdAt: true }, orderBy: { createdAt: 'asc' } }), 'JK', (id, loanNo) => prisma.loanRecord.update({ where: { id }, data: { loanNo } }), ); await remumber( await prisma.bondRecord.findMany({ select: { id: true, createdAt: true }, orderBy: { createdAt: 'asc' } }), 'BZ', (id, bondNo) => prisma.bondRecord.update({ where: { id }, data: { bondNo } }), ); const empCount = await prisma.employee.count(); const userCount = await prisma.user.count(); const deptCount = await prisma.department.count(); const sample = await prisma.expenseClaim.findMany({ take: 3, include: { applicant: true }, orderBy: { createdAt: 'desc' } }); console.log({ empCount, userCount, deptCount, sampleNos: sample.map((s) => `${s.claimNo}:${s.applicant.displayName}`) }); } main() .catch((e) => { console.error(e); process.exit(1); }) .finally(() => prisma.$disconnect());