232 lines
7.7 KiB
JavaScript
232 lines
7.7 KiB
JavaScript
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)
|
||
})
|