import { PrismaClient } from '@prisma/client' import { INITIAL_SALARY_POSITIONS } from '../lib/salary-position-seeds' import { positionFieldsSchema } from '../lib/salary-position-contract' import { randomUUID } from 'node:crypto' async function main() { const url = new URL(process.env.DATABASE_URL || '') if (!['localhost', '127.0.0.1'].includes(url.hostname) || !url.pathname.startsWith('/salary_isolated_')) throw new Error('仅允许隔离本地薪资数据库') const db = new PrismaClient({ log: [] }) const actor = await db.user.findFirst({ where: { isSuperAdmin: true, isActive: true, status: 'active', archivedAt: null }, select: { id: true } }) if (!actor) throw new Error('缺少本地超管') try { const result = await db.$transaction(async tx => { await tx.$executeRaw`SELECT pg_advisory_xact_lock(8261028)` let created = 0, assigned = 0, unresolved = 0 const existing = await tx.salaryPositionConfig.findMany({ where: { initializationKey: { in: INITIAL_SALARY_POSITIONS.map(seed => seed.key) } } }) // Includes tombstones: once initialized, never overwrite or recreate a deleted job. if (existing.length) return { initialized: false, created, assigned, unresolved } for (const seed of INITIAL_SALARY_POSITIONS) { const value = positionFieldsSchema.parse(seed.value) await tx.salaryPositionConfig.create({ data: { ...value, id: seed.id, initializationKey: seed.key, createdByUserId: actor.id, updatedByUserId: actor.id } }) await tx.salaryPositionConfigAudit.create({ data: { positionId: seed.id, actorUserId: actor.id, action: 'INITIALIZE', after: value } }) created++ } const month = '2026-10' const bases = await tx.personnelSalaryBase.findMany({ where: { deletedAt: null, positionId: null } }) for (const base of bases) { const assignments = await tx.performanceSurveyEmployeePositionAssignment.findMany({ where: { employeeProfileId: base.employeeProfileId, status: 'active', startMonth: { lte: month }, OR: [{ endMonth: null }, { endMonth: { gte: month } }] }, include: { positionGroup: true } }) const assignment = assignments.length === 1 && assignments[0].assignmentType === 'position' ? assignments[0].positionGroup : null const supervisorCodes = ['position_433e97c440384e5ea9dfb0e5438fd480', 'position_2ef247ce4ca441e09ae2e5951fd6e1d6', 'position_0cb2b7bb46654c6caa1ad2f97df39b6c'] const seed = assignment?.code === 'ai_planning_creative' || assignment?.code === 'director' ? INITIAL_SALARY_POSITIONS[1] : assignment?.code === 'ai_technical_designer' ? INITIAL_SALARY_POSITIONS[2] : assignment && supervisorCodes.includes(assignment.code) ? INITIAL_SALARY_POSITIONS[0] : null if (!seed) { unresolved++; continue } await tx.personnelSalaryBase.update({ where: { id: base.id }, data: { positionId: seed.id, revision: { increment: 1 }, updatedByUserId: actor.id } }) await tx.personnelSalaryBaseAudit.create({ data: { id: randomUUID(), recordId: base.id, actorUserId: actor.id, action: 'ASSIGN_POSITION_INITIAL', before: { positionId: null }, after: { positionId: seed.id } } }) assigned++ } return { initialized: true, created, assigned, unresolved } }, { isolationLevel: 'Serializable', timeout: 20000 }) console.log(JSON.stringify(result)) } finally { await db.$disconnect() } } main().catch(error => { console.error(error instanceof Error ? error.message : '初始化失败'); process.exitCode = 1 })