const fs = require('node:fs'); function valueOf(source, paths) { for (const path of paths) { const value = path.split('.').reduce((acc, key) => (acc && acc[key] !== undefined ? acc[key] : undefined), source); if (value !== undefined && value !== null && String(value).trim() !== '') return value; } return ''; } function normalizeToken(value) { return String(value || '').normalize('NFKC').replace(/[^A-Za-z0-9]/g, '').toUpperCase(); } function normalizeName(value) { return String(value || '').normalize('NFKC').replace(/\s+/g, '').trim(); } function normalizePlanNo(value) { return String(value || '').trim().toLowerCase(); } function getProductType(planNo) { const code = normalizePlanNo(planNo); if (code.startsWith('cxwypro')) return 'CXWY_PRO'; if (code.startsWith('cxwy')) return 'CXWY'; if (code.startsWith('pqxf')) return 'PQXF'; if (code.startsWith('ltwy')) return 'LTWY'; if (code.startsWith('yb')) return 'YB'; return 'UNKNOWN'; } function needCommercialPolicy(productType) { return ['CXWY', 'CXWY_PRO', 'PQXF'].includes(productType); } function hasValue(value) { if (Array.isArray(value)) return value.some(hasValue); return value !== undefined && value !== null && String(value).trim() !== ''; } function normalizeDateString(value) { const text = String(value || '').normalize('NFKC').trim(); if (!text) return ''; if (/^\d{4}-\d{2}-\d{2}$/.test(text)) return text; const direct = text.match(/(\d{4})[年./\-\s](\d{1,2})[月./\-\s](\d{1,2})日?/); if (direct) { return [ String(direct[1]).padStart(4, '0'), String(direct[2]).padStart(2, '0'), String(direct[3]).padStart(2, '0'), ].join('-'); } const compact = text.match(/^(\d{4})(\d{2})(\d{2})$/); if (compact) { return `${compact[1]}-${compact[2]}-${compact[3]}`; } const timestamp = Number(text); if (Number.isFinite(timestamp) && String(Math.trunc(timestamp)).length >= 10) { const date = new Date(String(Math.trunc(timestamp)).length === 10 ? timestamp * 1000 : timestamp); if (!Number.isNaN(date.getTime())) { const year = String(date.getFullYear()).padStart(4, '0'); const month = String(date.getMonth() + 1).padStart(2, '0'); const day = String(date.getDate()).padStart(2, '0'); return `${year}-${month}-${day}`; } } return ''; } function normalizeUsageNature(value) { const text = String(value || '').normalize('NFKC').replace(/\s+/g, ''); if (!text) return ''; if (/家庭自用/.test(text)) return '家庭自用汽车'; if (/非营运/.test(text)) return '非营运'; if (/非营业/.test(text)) return '非营业'; if (/营业/.test(text)) return '营业'; if (/营运/.test(text)) return '营运'; if (/预约出租/.test(text)) return '预约出租客运'; if (/网约/.test(text)) return '网约车'; if (/出租/.test(text)) return '出租'; if (/租赁/.test(text)) return '租赁'; if (/客运/.test(text)) return '客运'; if (/货运/.test(text)) return '货运'; return text; } function isOperatingUsageNature(value) { const text = String(value || '').normalize('NFKC').replace(/\s+/g, ''); if (!text) return false; if (/家庭自用/.test(text) || /非营运/.test(text) || /非营业/.test(text)) return false; return /营运|出租|租赁|网约|预约出租|客运|货运|营业/.test(text); } function isFamilySelfUseUsageNature(value) { const text = String(value || '').normalize('NFKC').replace(/\s+/g, ''); return /家庭自用|非营运|非营业/.test(text); } function mismatchCount(a, b) { if (!a || !b || a.length !== b.length) return Number.POSITIVE_INFINITY; let count = 0; for (let i = 0; i < a.length; i += 1) { if (a[i] !== b[i]) count += 1; } return count; } function isLikelyVinOcrError(expectedVin, actualVin) { if (!expectedVin || !actualVin || expectedVin.length !== 17 || actualVin.length !== 17) { return false; } const diff = mismatchCount(expectedVin, actualVin); if (diff <= 1) return true; if (diff === 2) { const positions = []; for (let i = 0; i < expectedVin.length; i += 1) { if (expectedVin[i] !== actualVin[i]) positions.push(i); } return ( positions[1] === positions[0] + 1 && expectedVin[positions[0]] === actualVin[positions[1]] && expectedVin[positions[1]] === actualVin[positions[0]] ); } return false; } function isKnownAvatrTa60OcrSwap(expectedVin, actualVin) { if (!expectedVin || !actualVin || expectedVin.length !== 17 || actualVin.length !== 17) { return false; } if (!expectedVin.includes('TA60') || !actualVin.includes('TA06')) { return false; } return actualVin.replace('TA06', 'TA60') === expectedVin; } function canCorrectByConfusion(expected, actual, groups) { if (!expected || !actual || expected.length !== actual.length) { return false; } let hasDiff = false; for (let i = 0; i < expected.length; i += 1) { if (expected[i] === actual[i]) continue; hasDiff = true; const inSameGroup = groups.some(group => group.includes(expected[i]) && group.includes(actual[i])); if (!inSameGroup) return false; } return hasDiff; } function isPolicyNoOq0Confusion(expectedPolicyNo, actualPolicyNo) { return canCorrectByConfusion( normalizeToken(expectedPolicyNo), normalizeToken(actualPolicyNo), [['O', 'Q', '0']] ); } function isDateWithinRange(date, start, end) { if (!date || !start || !end) return false; return date >= start && date <= end; } function isRangeWithinRange(innerStart, innerEnd, outerStart, outerEnd) { if (!innerStart && !innerEnd) return false; if (!outerStart || !outerEnd) return false; if (innerStart && innerStart < outerStart) return false; if (innerEnd && innerEnd > outerEnd) return false; return true; } function collectFirstValue(source, paths) { for (const path of paths) { const raw = valueOf(source, [path]); const normalized = normalizeDateString(raw); if (normalized) return normalized; } return ''; } function getOrderPurchaseWindow(order) { const singleDate = collectFirstValue({ order }, [ 'order.purchaseTime', 'order.purchaseDate', 'order.buyTime', 'order.buyDate', 'order.commercialInsuranceDate', 'order.orderTime', 'order.createTime', ]); const purchaseStartDate = collectFirstValue({ order }, [ 'order.purchaseStartDate', 'order.purchaseStartTime', 'order.buyStartDate', 'order.buyStartTime', 'order.commercialInsuranceStartDate', 'order.commercialInsuranceStartTime', 'order.purchaseRange.startDate', 'order.purchaseRange.startTime', ]); const purchaseEndDate = collectFirstValue({ order }, [ 'order.purchaseEndDate', 'order.purchaseEndTime', 'order.buyEndDate', 'order.buyEndTime', 'order.commercialInsuranceEndDate', 'order.commercialInsuranceEndTime', 'order.purchaseRange.endDate', 'order.purchaseRange.endTime', ]); if (purchaseStartDate || purchaseEndDate) { return { purchaseDate: singleDate, purchaseStartDate: purchaseStartDate || singleDate, purchaseEndDate: purchaseEndDate || singleDate, purchaseLabel: purchaseStartDate || purchaseEndDate ? '购买时间范围' : '购买时间', }; } return { purchaseDate: singleDate, purchaseStartDate: singleDate, purchaseEndDate: singleDate, purchaseLabel: '购买时间', }; } function normalizeOcr(input) { const ocr = input.commercialPolicyOcr || {}; const nestedPolicy = ocr.policy || {}; const nestedVehicle = ocr.vehicle || {}; const nestedIdentity = ocr.identity || {}; const nameCandidates = []; if (Array.isArray(ocr.nameCandidates)) { for (const item of ocr.nameCandidates) { if (item && typeof item === 'object' && item.name) { nameCandidates.push({ label: String(item.label || '姓名候选'), name: String(item.name) }); } else if (typeof item === 'string') { nameCandidates.push({ label: '姓名候选', name: item }); } } } for (const [label, value] of [ ['被保险人', nestedIdentity.insuredName], ['车主', nestedIdentity.ownerName], ['投保人', nestedIdentity.applicantName], ['姓名', ocr.name], ]) { if (hasValue(value)) nameCandidates.push({ label, name: String(value) }); } return { policyNo: String(valueOf({ ocr, nestedPolicy }, ['ocr.policyNo', 'nestedPolicy.policyNo']) || ''), vin: String(valueOf({ ocr, nestedVehicle }, ['ocr.vin', 'nestedVehicle.vin']) || ''), nameCandidates, certificateNo: String(valueOf({ ocr, nestedIdentity }, ['ocr.certificateNo', 'nestedIdentity.certificateNo']) || ''), certificateNoMasked: Boolean(ocr.certificateNoMasked || nestedIdentity.certificateNoMasked), usageNature: normalizeUsageNature(valueOf({ ocr, nestedPolicy, nestedVehicle }, [ 'ocr.usageNature', 'ocr.useNature', 'ocr.vehicleUsageNature', 'ocr.usageNatureText', 'nestedPolicy.usageNature', 'nestedPolicy.useNature', 'nestedVehicle.usageNature', 'nestedVehicle.useNature', ])), startDate: normalizeDateString(valueOf({ ocr, nestedPolicy }, [ 'ocr.startDate', 'ocr.insuranceStartDate', 'ocr.policyStartDate', 'nestedPolicy.startDate', 'nestedPolicy.insuranceStartDate', 'nestedPolicy.policyStartDate', ])), endDate: normalizeDateString(valueOf({ ocr, nestedPolicy }, [ 'ocr.endDate', 'ocr.insuranceEndDate', 'ocr.policyEndDate', 'nestedPolicy.endDate', 'nestedPolicy.insuranceEndDate', 'nestedPolicy.policyEndDate', ])), hasVehicleDamageCoverage: valueOf({ ocr, nestedPolicy }, ['ocr.hasVehicleDamageCoverage', 'nestedPolicy.hasVehicleDamageCoverage']), confidence: typeof ocr.confidence === 'number' ? ocr.confidence : undefined, warnings: Array.isArray(ocr.warnings) ? ocr.warnings : [], }; } function isCompleteIdCard(value) { return /^\d{17}[\dX]$/i.test(String(value || '').trim()); } function maskedIdMatches(masked, actual) { const mask = String(masked || '').toUpperCase(); const id = String(actual || '').toUpperCase(); if (!mask || !id) return false; if (isCompleteIdCard(mask)) return mask === id; let idIndex = 0; for (const char of mask) { if (char === '*' || char === 'X') { idIndex += 1; continue; } while (idIndex < id.length && id[idIndex] !== char) idIndex += 1; if (idIndex >= id.length) return false; idIndex += 1; } return true; } function createAudit(input) { const order = input.order || {}; const attachmentsProvided = input.attachments && typeof input.attachments === 'object'; const attachments = input.attachments || {}; const ocrProvided = input.commercialPolicyOcr && typeof input.commercialPolicyOcr === 'object'; const ocr = normalizeOcr(input); const productType = getProductType(order.planNo); const purchaseWindow = getOrderPurchaseWindow(order); const policyStartDate = normalizeDateString(ocr.startDate); const policyEndDate = normalizeDateString(ocr.endDate); const normalizedUsageNature = normalizeUsageNature(ocr.usageNature); const fieldChecks = []; const reasons = []; const riskFlags = []; let reject = false; let manual = false; function addCheck(field, expected, actual, result, message, extra = {}) { fieldChecks.push({ field, expected: expected || '', actual: actual || '', result, message, ...extra, }); if (result === 'REJECT') reject = true; if (result === 'REVIEW') manual = true; } function addReason(result, code, message) { reasons.push({ code, message }); if (result === 'REJECT') reject = true; if (result === 'REVIEW') manual = true; } for (const field of ['businessNo', 'orderNum', 'planNo', 'vin', 'ownerName']) { if (!hasValue(order[field])) { addCheck(field, '必填', '', 'REVIEW', `订单缺少${field},需要人工确认`); } } if (hasValue(order.status) && order.status !== 'CONFIRMED') { addCheck('status', 'CONFIRMED', order.status, 'REVIEW', '阿维塔子单状态不是CONFIRMED'); } else if (hasValue(order.status)) { addCheck('status', 'CONFIRMED', order.status, 'PASS', '阿维塔子单状态正常'); } if (hasValue(order.mainStatus) && order.mainStatus !== 'WAITEXAMINE') { addCheck('mainStatus', 'WAITEXAMINE', order.mainStatus, 'REVIEW', '主订单状态不是WAITEXAMINE'); } else if (hasValue(order.mainStatus)) { addCheck('mainStatus', 'WAITEXAMINE', order.mainStatus, 'PASS', '主订单状态正常'); } if (needCommercialPolicy(productType)) { if (attachmentsProvided) { if (hasValue(attachments.commercialInsuranceUrl)) { addCheck('commercialInsuranceUrl', '必传', '已传', 'PASS', '商业险保单附件已上传'); } else { addCheck('commercialInsuranceUrl', '必传', '', 'REJECT', '产品要求商业险保单附件,但未上传'); } } else { addCheck('commercialInsuranceUrl', '必传', '', 'REVIEW', '未提供附件字段,无法确认商业险保单附件'); } if (!ocrProvided) { addReason('REVIEW', 'MISSING_POLICY_OCR', '缺少商业险/车损险保单OCR结构化结果'); } } if (productType === 'LTWY' && attachmentsProvided) { for (const field of ['leftFrontDot', 'rightFrontDot', 'leftRearDot', 'rightRearDot']) { addCheck(field, '必传', hasValue(attachments[field]) ? '已传' : '', hasValue(attachments[field]) ? 'PASS' : 'REJECT', `${field}轮胎DOT附件校验`); } } if (productType === 'YB') { addCheck('warrantyPeriod', '必填', order.warrantyPeriod || '', hasValue(order.warrantyPeriod) ? 'PASS' : 'REJECT', '整车延保产品需填写原厂整车质保期'); } if (needCommercialPolicy(productType) || ocrProvided) { if (!hasValue(ocr.policyNo)) { addCheck('policyNo', 'OCR应识别', '', 'REVIEW', '未识别到商业险保单号', { needsMultimodalReview: true, multimodalReason: 'policyNo_missing', }); } else if (hasValue(order.commercialInsuranceNo)) { const expectedPolicyNo = normalizeToken(order.commercialInsuranceNo); const actualPolicyNo = normalizeToken(ocr.policyNo); const matched = expectedPolicyNo === actualPolicyNo; if (matched) { addCheck('policyNo', order.commercialInsuranceNo, ocr.policyNo, 'PASS', '保单号一致'); } else if (isPolicyNoOq0Confusion(expectedPolicyNo, actualPolicyNo)) { addCheck('policyNo', order.commercialInsuranceNo, ocr.policyNo, 'PASS', '保单号存在O/Q/0常见OCR混淆,按订单商业险保单号校正后一致', { correctedValue: order.commercialInsuranceNo, correctionRule: 'POLICY_O_Q_0_CONFUSION', }); riskFlags.push({ level: 'LOW', code: 'POLICY_O_Q_0_OCR_CORRECTED', message: '保单号O/Q/0 OCR误识别已按订单保单号校正' }); } else { addCheck('policyNo', order.commercialInsuranceNo, ocr.policyNo, 'REVIEW', 'OCR保单号与订单商业险保单号不一致,需多模态复核保单原图', { needsMultimodalReview: true, multimodalReason: 'policyNo_mismatch', }); riskFlags.push({ level: 'MEDIUM', code: 'POLICY_NO_MISMATCH_NEEDS_MULTIMODAL', message: 'OCR保单号与订单商业险保单号不一致,需多模态复核' }); } } else { addCheck('policyNo', '订单可为空', ocr.policyNo, 'PASS', 'OCR已识别保单号,可用于回填'); } if (!hasValue(ocr.vin)) { addCheck('vin', order.vin || '订单VIN', '', 'REVIEW', '未识别到保单VIN', { needsMultimodalReview: true, multimodalReason: 'vin_missing', }); } else { const expectedVin = normalizeToken(order.vin); const actualVin = normalizeToken(ocr.vin); if (expectedVin && actualVin && expectedVin !== actualVin) { if (isKnownAvatrTa60OcrSwap(expectedVin, actualVin)) { addCheck('vin', order.vin, ocr.vin, 'PASS', '阿维塔车型VIN存在TA60被OCR识别为TA06的已知误识别,按订单VIN校正后一致', { correctedValue: order.vin, correctionRule: 'AVATR_TA06_TO_TA60', }); riskFlags.push({ level: 'LOW', code: 'VIN_TA60_TA06_OCR_CORRECTED', message: '阿维塔VIN TA60/TA06 OCR误识别已按订单VIN校正' }); } else if (isLikelyVinOcrError(expectedVin, actualVin)) { addCheck('vin', order.vin, ocr.vin, 'REVIEW', '保单VIN与订单VIN存在轻微差异,疑似OCR识别错误,需多模态复核保单原图', { needsMultimodalReview: true, multimodalReason: 'vin_suspected_ocr_error', }); riskFlags.push({ level: 'MEDIUM', code: 'VIN_OCR_SUSPECT_NEEDS_MULTIMODAL', message: '保单VIN与订单VIN存在轻微差异,需多模态复核' }); } else { addCheck('vin', order.vin, ocr.vin, 'REJECT', '保单VIN与订单VIN不一致'); riskFlags.push({ level: 'HIGH', code: 'VIN_MISMATCH', message: '保单VIN与订单VIN不一致' }); } } else { addCheck('vin', order.vin, ocr.vin, 'PASS', 'VIN一致'); } } const ownerName = normalizeName(order.ownerName); const matchedName = ocr.nameCandidates.find(item => normalizeName(item.name) === ownerName); if (!ownerName) { addCheck('ownerName', '订单车主姓名', '', 'REVIEW', '订单车主姓名缺失'); } else if (matchedName) { addCheck('ownerName', order.ownerName, `${matchedName.label}:${matchedName.name}`, 'PASS', '保单姓名候选与订单车主一致'); } else if (ocr.nameCandidates.length > 0) { addCheck('ownerName', order.ownerName, ocr.nameCandidates.map(item => `${item.label}:${item.name}`).join(', '), 'REVIEW', '保单姓名候选未匹配订单车主'); riskFlags.push({ level: 'MEDIUM', code: 'NAME_NOT_MATCHED', message: '保单姓名候选未匹配订单车主' }); } else { addCheck('ownerName', order.ownerName, '', 'REVIEW', '未识别到被保险人/车主/投保人姓名'); } if (hasValue(ocr.certificateNo) && hasValue(order.idCard)) { if (ocr.certificateNoMasked) { const matched = maskedIdMatches(ocr.certificateNo, order.idCard); addCheck('idCard', order.idCard, ocr.certificateNo, matched ? 'PASS' : 'REVIEW', matched ? '脱敏证件号可见位匹配' : '脱敏证件号无法确认匹配'); } else if (isCompleteIdCard(ocr.certificateNo)) { const matched = normalizeToken(ocr.certificateNo) === normalizeToken(order.idCard); addCheck('idCard', order.idCard, ocr.certificateNo, matched ? 'PASS' : 'REJECT', matched ? '完整证件号一致' : '完整证件号与订单身份证号不一致'); } } else { addCheck('idCard', order.idCard || '可选', ocr.certificateNo || '', 'SKIP', '保单证件号不是必填项,缺失不影响核心审核'); } if (!hasValue(ocr.usageNature)) { addCheck('usageNature', '家庭自用汽车', '', 'REVIEW', '未识别到保单使用性质,需多模态复核保单原图', { needsMultimodalReview: true, multimodalReason: 'usageNature_missing', }); } else if (isFamilySelfUseUsageNature(ocr.usageNature)) { addCheck('usageNature', '家庭自用汽车/非营运/非营业', ocr.usageNature, 'PASS', '保单使用性质为家庭自用或非营运/非营业,符合承保要求'); } else if (isOperatingUsageNature(ocr.usageNature)) { addCheck('usageNature', '家庭自用汽车', ocr.usageNature, 'REJECT', '保单使用性质为营运/营业类,不符合承保要求'); riskFlags.push({ level: 'HIGH', code: 'OPERATING_USAGE_NATURE', message: '保单使用性质不是家庭自用汽车' }); } else { addCheck('usageNature', '家庭自用汽车', ocr.usageNature, 'REJECT', '保单使用性质不是家庭自用汽车,不符合承保要求'); riskFlags.push({ level: 'HIGH', code: 'NON_FAMILY_USAGE_NATURE', message: '保单使用性质不是家庭自用汽车' }); } if (!policyStartDate || !policyEndDate) { addCheck('insurancePeriod', '应识别保险起止日期', `${ocr.startDate || ''} - ${ocr.endDate || ''}`, 'REVIEW', '未完整识别保单保险期间,需多模态复核保单原图', { needsMultimodalReview: true, multimodalReason: 'insurancePeriod_missing', }); } else if (!purchaseWindow.purchaseStartDate && !purchaseWindow.purchaseEndDate) { addCheck('insurancePeriod', '购买时间应在保险期间内', `${policyStartDate} - ${policyEndDate}`, 'REVIEW', '订单缺少购买时间,无法自动校验保险期间'); } else if (purchaseWindow.purchaseStartDate === purchaseWindow.purchaseEndDate) { const purchaseDate = purchaseWindow.purchaseStartDate; const matched = isDateWithinRange(purchaseDate, policyStartDate, policyEndDate); addCheck( 'insurancePeriod', `应覆盖购买时间 ${purchaseDate}`, `${policyStartDate} - ${policyEndDate}`, matched ? 'PASS' : 'REJECT', matched ? '购买时间在保单保险期间内' : '购买时间不在保单保险期间内' ); if (!matched) { riskFlags.push({ level: 'HIGH', code: 'PURCHASE_TIME_OUT_OF_POLICY_PERIOD', message: '购买时间不在保单保险期间内' }); } } else { const matched = isRangeWithinRange(purchaseWindow.purchaseStartDate, purchaseWindow.purchaseEndDate, policyStartDate, policyEndDate); addCheck( 'insurancePeriod', `应覆盖${purchaseWindow.purchaseLabel} ${purchaseWindow.purchaseStartDate || ''} - ${purchaseWindow.purchaseEndDate || ''}`, `${policyStartDate} - ${policyEndDate}`, matched ? 'PASS' : 'REJECT', matched ? '购买时间范围在保单保险期间内' : '购买时间范围不在保单保险期间内' ); if (!matched) { riskFlags.push({ level: 'HIGH', code: 'PURCHASE_RANGE_OUT_OF_POLICY_PERIOD', message: '购买时间范围不在保单保险期间内' }); } } if (ocr.hasVehicleDamageCoverage === true) { addCheck('vehicleDamageCoverage', '应包含车损险', '已识别', 'PASS', '识别到车损险险种证据'); } else { addCheck('vehicleDamageCoverage', '应包含车损险', '未确认', 'REVIEW', '未识别到机动车损失保险/车辆损失保险/车损险证据'); } } const decision = reject ? 'REJECT' : manual ? 'MANUAL_REVIEW' : 'PASS'; const suggestedAction = decision === 'PASS' ? '提交人保投保' : decision === 'REJECT' ? '驳回并要求重新核对或上传正确商业险/车损险保单' : '转人工复核保单原件和OCR结果'; return { success: true, decision, reasons, fieldChecks, riskFlags, suggestedAction, summary: { productType, policyNo: ocr.policyNo, vin: ocr.vin, nameCandidates: ocr.nameCandidates, usageNature: ocr.usageNature, normalizedUsageNature, policyStartDate, policyEndDate, purchaseDate: purchaseWindow.purchaseDate, purchaseStartDate: purchaseWindow.purchaseStartDate, purchaseEndDate: purchaseWindow.purchaseEndDate, ocrConfidence: ocr.confidence, ocrWarnings: ocr.warnings, }, }; } async function main() { try { const stdin = fs.readFileSync(0, 'utf8'); const input = JSON.parse(stdin || '{}'); process.stdout.write(JSON.stringify(createAudit(input))); } catch (error) { process.stdout.write(JSON.stringify({ success: false, error: error.message })); process.exitCode = 0; } } if (require.main === module) { main(); } module.exports = { createAudit, normalizeOcr, getProductType, };