GuDingZiChan/backend/scripts/exportManualReviewCandidates.js
2026-03-24 14:21:09 +08:00

232 lines
7.7 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

const fs = require('fs')
const path = require('path')
const ExcelJS = require('exceljs')
const SOURCE_PATH = path.resolve(
__dirname,
'../../temp_migration_output/multi_unit_child_repair_2026-03-24_01-16-47/manual_review_candidates.csv'
)
const OUTPUT_PATH = path.resolve(
__dirname,
'../../行政手动处理清单-最终仅剩8条-2026-03-24.xlsx'
)
const OBSOLETE_FILES = [
path.resolve(__dirname, '../../父子资产补建-行政手动处理清单-2026-03-23.xlsx'),
path.resolve(__dirname, '../../行政手动补录清单-2026-03-23_当前待人工处理.xlsx'),
path.resolve(__dirname, '../../子资产多件拆分-行政手动处理清单-2026-03-23.xlsx')
]
const REASON_MAP = {
outer_capacity_not_equal_to_inner_times_quantity:
'表格总容量与单条容量乘数量对不上,系统不能在不猜测的前提下自动拆分。',
child_type_and_model_are_inconsistent: '子资产类型与型号内容不一致,系统不能自动处理。',
pattern_detected_but_no_safe_split_rule: '识别到多件格式,但现有信息不足以安全拆分。',
storage_plus_pattern_is_not_single_capacity_pair:
'硬盘型号中混有容量和具体型号,不能只按容量安全拆分。'
}
const SPECIAL_REASON_OVERRIDES = {
'JS-PC-2127':
'原表错行:该行硬件字段整体错位,不能按当前行内容自动落库,需行政按原始资料人工核对后录入。'
}
const normalize = (value) => String(value == null ? '' : value)
const extractMeta = (description, key) => {
const text = normalize(description)
const matched = text.match(new RegExp(`(?:^|;\\s*)${key}=([^;]*)`))
return matched ? matched[1].trim() : ''
}
async function readCsvRows(csvPath) {
const workbook = new ExcelJS.Workbook()
const sheet = await workbook.csv.readFile(csvPath)
const headers = sheet.getRow(1).values.slice(1).map((item) => normalize(item))
const rows = []
for (let rowNumber = 2; rowNumber <= sheet.rowCount; rowNumber += 1) {
const row = sheet.getRow(rowNumber)
if (row.cellCount === 0) {
continue
}
const record = {}
headers.forEach((header, index) => {
record[header] = normalize(row.getCell(index + 1).text)
})
if (Object.values(record).some((value) => value !== '')) {
rows.push(record)
}
}
return { headers, rows }
}
function styleHeaderRow(row) {
row.font = { bold: true }
row.alignment = { vertical: 'middle', horizontal: 'center' }
}
function resolveReasonDescription(row) {
return SPECIAL_REASON_OVERRIDES[row.parent_code] || REASON_MAP[row.reason] || row.reason
}
function buildTimestampedOutputPath() {
const now = new Date()
const pad = (value) => String(value).padStart(2, '0')
const stamp = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}_${pad(now.getHours())}-${pad(now.getMinutes())}-${pad(now.getSeconds())}`
return path.resolve(__dirname, `../../行政手动处理清单-最终仅剩8条-${stamp}.xlsx`)
}
async function writeWorkbookWithFallback(workbook) {
try {
await workbook.xlsx.writeFile(OUTPUT_PATH)
return OUTPUT_PATH
} catch (error) {
if (error && error.code === 'EBUSY') {
const fallbackPath = buildTimestampedOutputPath()
await workbook.xlsx.writeFile(fallbackPath)
return fallbackPath
}
throw error
}
}
async function exportWorkbook() {
const { headers, rows } = await readCsvRows(SOURCE_PATH)
const workbook = new ExcelJS.Workbook()
workbook.creator = 'Codex'
workbook.created = new Date()
const taskSheet = workbook.addWorksheet('人工处理清单')
taskSheet.columns = [
{ header: '源表行号', key: 'source_row', width: 10 },
{ header: '父资产编码', key: 'parent_code', width: 16 },
{ header: '父资产名称', key: 'parent_name', width: 12 },
{ header: '配件字段', key: 'field_key', width: 12 },
{ header: '子资产类型', key: 'child_type', width: 12 },
{ header: '系统当前子资产编码', key: 'current_child_code', width: 18 },
{ header: '表格原始型号', key: 'source_child_model', width: 42 },
{ header: '表格部门', key: 'workbook_department', width: 18 },
{ header: '表格使用人', key: 'workbook_owner', width: 12 },
{ header: '表格位置', key: 'workbook_location', width: 18 },
{ header: '原因代码', key: 'reason', width: 44 },
{ header: '人工处理原因', key: 'reason_desc', width: 50 }
]
taskSheet.views = [{ state: 'frozen', ySplit: 1 }]
taskSheet.autoFilter = 'A1:L1'
styleHeaderRow(taskSheet.getRow(1))
rows.forEach((row) => {
taskSheet.addRow({
source_row: row.source_row,
parent_code: row.parent_code,
parent_name: row.parent_name,
field_key: row.field_key,
child_type: row.child_type,
current_child_code: row.current_child_code,
source_child_model: row.source_child_model,
workbook_department: extractMeta(row.planned_description, 'workbook_department'),
workbook_owner: extractMeta(row.planned_description, 'workbook_owner'),
workbook_location: extractMeta(row.planned_description, 'workbook_location'),
reason: row.reason,
reason_desc: resolveReasonDescription(row)
})
})
taskSheet.eachRow((row, rowNumber) => {
row.alignment = { vertical: 'top', wrapText: true }
if (rowNumber > 1) {
row.height = 22
}
})
const rawSheet = workbook.addWorksheet('原始数据')
rawSheet.columns = headers.map((header) => ({
header,
key: header,
width: Math.max(14, header.length + 2)
}))
rawSheet.views = [{ state: 'frozen', ySplit: 1 }]
styleHeaderRow(rawSheet.getRow(1))
rows.forEach((row) => rawSheet.addRow(row))
rawSheet.eachRow((row) => {
row.alignment = { vertical: 'top', wrapText: true }
})
const actualOutputPath = await writeWorkbookWithFallback(workbook)
const verifyBook = new ExcelJS.Workbook()
await verifyBook.xlsx.readFile(actualOutputPath)
const verifyRawSheet = verifyBook.getWorksheet('原始数据')
const verifyTaskSheet = verifyBook.getWorksheet('人工处理清单')
const verifiedRows = []
for (let rowNumber = 2; rowNumber <= verifyRawSheet.rowCount; rowNumber += 1) {
const row = verifyRawSheet.getRow(rowNumber)
if (row.cellCount === 0) {
continue
}
const record = {}
headers.forEach((header, index) => {
record[header] = normalize(row.getCell(index + 1).text)
})
if (Object.values(record).some((value) => value !== '')) {
verifiedRows.push(record)
}
}
if (verifiedRows.length !== rows.length) {
throw new Error(`校验失败:原始数据行数不一致,source=${rows.length}, xlsx=${verifiedRows.length}`)
}
rows.forEach((row, index) => {
headers.forEach((header) => {
if (normalize(verifiedRows[index][header]) !== normalize(row[header])) {
throw new Error(`校验失败:第 ${index + 1} 行字段 ${header} 不一致`)
}
})
})
const taskRowCount = Math.max(verifyTaskSheet.rowCount - 1, 0)
if (taskRowCount !== rows.length) {
throw new Error(`校验失败:人工处理清单行数不一致,source=${rows.length}, xlsx=${taskRowCount}`)
}
const removedFiles = []
const skippedFiles = []
for (const filePath of OBSOLETE_FILES) {
if (fs.existsSync(filePath)) {
try {
fs.unlinkSync(filePath)
removedFiles.push(filePath)
} catch (error) {
skippedFiles.push({
filePath,
code: error && error.code ? error.code : 'UNKNOWN'
})
}
}
}
return {
outputPath: actualOutputPath,
rowCount: rows.length,
removedFiles,
skippedFiles
}
}
exportWorkbook()
.then((result) => {
console.log(JSON.stringify({ verified: true, ...result }, null, 2))
})
.catch((error) => {
console.error(error)
process.exit(1)
})