45 lines
3.5 KiB
TypeScript
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 })
|