'use strict'; /** * 身份证号判据(导入通道的单一判据源) * * ⚠️ 同源约束:本模块的判定口径必须与下列两处保持一致,任一处变更须同步其余两处: * 1. `scripts/preflight_import_check.py`(导入前离线校验器,CLI) * 2. `backend/app/model/contestant.js`(id_number: STRING(18) + validate.len [18,18]) * * 口径来源(2026-10-12 依据生产事故校准): * · 报名表「已审核(含增补)」导入时,第 188 / 1012 / 1421 行因身份证号被写成 * 19 / 17 / 20 位,被 Sequelize 的 len 校验拦下,但错误信息只有 * `Validation len on id_number failed` —— 管理员在页面上完全看不到是哪几行、是谁。 * · 故把「长度必须 18 位」前置为**可读错误**(与原行为等价:原本也进不了库,只是报错难懂)。 * * 分级原则(避免误伤真实数据): * · 长度 ≠ 18 → error:绝不会写入成功(模型层硬约束),前置拦截并给出人话原因。 * · 18 位但含非法字符 → warn :模型只查长度、不查字符集,原值可入库 ⇒ 只提示不阻断。 * · 18 位但校验位不符 → warn :生产库现存 753 条中即有 4 条校验位不符(真实在册考生), * 若硬拦截会造成合法考生漏导 ⇒ 只提示不阻断。 */ const ID_LEN = 18; const ID_CHARSET_RE = /^\d{17}[\dX]$/; // GB 11643-1999 校验位加权因子与余数映射 const WEIGHTS = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2]; const CHECK_CODES = '10X98765432'; /** * 身份证号归一化:去首尾空白 + 末位 x 统一为大写 X。 * @param {*} raw - 原始值(可能为 undefined / number / string) * @returns {string} 归一化后的字符串(空值返回 '') */ function normalizeIdNumber(raw) { if (raw === undefined || raw === null) return ''; return String(raw).trim().toUpperCase(); } /** * 计算 18 位身份证号第 18 位(校验位)。 * @param {string} s - 至少 17 位的数字串(已归一化) * @returns {string} 期望的校验位('0'-'9' / 'X');入参不足或含非数字时返回 '' */ function expectedCheckCode(s) { if (typeof s !== 'string' || s.length < 17) return ''; const body = s.slice(0, 17); if (!/^\d{17}$/.test(body)) return ''; let sum = 0; for (let i = 0; i < 17; i++) sum += Number(body[i]) * WEIGHTS[i]; return CHECK_CODES[sum % 11]; } /** * 检验一个身份证号是否可用于导入,并给出人话级原因。 * * @param {*} raw - 原始单元格值 * @returns {{ importable: boolean, level: 'error'|'warn'|null, message: string, value: string }} * · importable=false ⇒ 该行**不可能写入成功**,调用方应跳过(level 恒为 'error') * · importable=true 且 level='warn' ⇒ 可导入,但需人工复核(message 说明原因) * · importable=true 且 level=null ⇒ 无异常 */ function inspectIdNumber(raw) { const value = normalizeIdNumber(raw); if (!value) { return { importable: false, level: 'error', message: '身份证号为空', value }; } if (value.length !== ID_LEN) { return { importable: false, level: 'error', message: `身份证号应为 ${ID_LEN} 位,实际 ${value.length} 位(${value})`, value, }; } if (!ID_CHARSET_RE.test(value)) { return { importable: true, level: 'warn', message: `身份证号含非法字符(${value}),已按原值导入,请人工核对`, value, }; } const expect = expectedCheckCode(value); if (expect && expect !== value[ID_LEN - 1]) { return { importable: true, level: 'warn', message: `身份证号校验位不符(末位应为 ${expect},实际为 ${value[ID_LEN - 1]}),已按原值导入,请人工核对`, value, }; } return { importable: true, level: null, message: '', value }; } module.exports = { ID_LEN, ID_CHARSET_RE, normalizeIdNumber, expectedCheckCode, inspectIdNumber, };