gongxiangp2/scripts/initialize-salary-positions-local.ts

45 lines
3.5 KiB
TypeScript

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 })