文件
hyb-oa/scripts/qa/synthetic-bank-receipt.cjs
T
wangsli 5a7a13524e 完善付款回单凭证核验与本地重检(09-17-company-loan-principal-repayment)
区分正式付款回单、非最终回执与未知凭证,在核验构建、人工证据重建和最终核销时使用同一份有效依据。非最终说明、入账方向、状态矛盾及重复交易不能经人工覆盖。

正式回单无字面成功状态时,允许财务按原件实际记账支出、日期及说明提出核验,由本批次适用的确认人确认;不补造银行成功文字。旧核验可显式本地重检,新版本与审计原子保存,不重新外发 OCR,不沿用旧人工批准或改写已核销历史。

本地验证:82 个迁移、借款 HTTP 116 组、关联电脑/手机 18 组、共享回单数据库与真实认证 38 组、共享报销页面限定重验 24 项、最新回单/报销定向 292 项通过;9 个资金守卫变异均被捕获。服务端全量 4554 项通过、1 项既有可选跳过,后续合成数据及零页详情改动由最新定向验证覆盖。

本次无新增迁移,新增本地 PDF 文本解析依赖。后续对本提交运行完整 verify-candidate,实际候选与结果以本轮回执为准。

限制:独立代码复核及真实还款验收待完成;历史材料预验不证明真实还本或银行提交对应。未推送、未部署、未访问生产或写入真实业务数据。
2026-10-09 06:18:09 +08:00

47 行
3.0 KiB
JavaScript

const { createRequire } = require('node:module');
const path = require('node:path');
const req = createRequire(path.resolve(__dirname, '../../server/package.json'));
function syntheticReceiptText({ status = 'success', final = true, direction = 'debit', amount = '10.00',
currency = '人民币', payerAccount = '000011112222', payeeAccount = '000033334444',
reference = 'SYNTHETIC-REFERENCE', bookedOn = '2026-10-01' } = {}) {
const words = { success: '交易成功', failed: '交易失败', pending: '处理中', booked: '' };
return [final ? '中国建设银行单位客户专用回单' : '中国建设银行网上银行电子回执',
'合成公司测试回单 SYNTHETIC', `记账日期 ${bookedOn}`, direction === 'credit' ? '贷' : '借',
`金额 ${amount}`, `币别 ${currency}`, `付款人账号 ${payerAccount}`, `收款人账号 ${payeeAccount}`, `凭证号码 ${reference}`,
...(words[status] ? [`单据状态 ${words[status]}`] : []),
...(!final ? ['本回执不作为收、付款方交易的最终依据,正式回单请在交易成功后另打印。'] : [])].join('\n');
}
async function syntheticBankReceiptPdf({ pages = 1, tag = 'SYNTHETIC', ...options } = {}) {
const { PDFDocument, PDFHexString, beginText, endText, setFontAndSize, moveText, showText } = req('pdf-lib');
const doc = await PDFDocument.create(); doc.setTitle(tag);
const text = syntheticReceiptText(options);
const hex = (s) => Buffer.from(s, 'utf16le').swap16().toString('hex').toUpperCase();
const codes = [...new Set([...text.replace(/\n/g, '')])].map((c) => `<${hex(c)}> <${hex(c)}>`);
const cmap = `/CIDInit /ProcSet findresource begin 12 dict begin begincmap
/CIDSystemInfo << /Registry (Adobe) /Ordering (UCS) /Supplement 0 >> def
/CMapName /Synthetic-UCS def /CMapType 2 def
1 begincodespacerange <0000> <FFFF> endcodespacerange
${codes.length} beginbfchar\n${codes.join('\n')}\nendbfchar
endcmap CMapName currentdict /CMap defineresource pop end end`;
const toUnicode = doc.context.register(doc.context.flateStream(cmap));
const cid = doc.context.register(doc.context.obj({ Type: 'Font', Subtype: 'CIDFontType0', BaseFont: 'STSong-Light',
CIDSystemInfo: { Registry: PDFHexString.fromText('Adobe'), Ordering: PDFHexString.fromText('GB1'), Supplement: 4 }, DW: 1000 }));
const font = doc.context.register(doc.context.obj({ Type: 'Font', Subtype: 'Type0', BaseFont: 'STSong-Light',
Encoding: 'Identity-H', DescendantFonts: [cid], ToUnicode: toUnicode }));
for (let n = 0; n < pages; n++) {
const page = doc.addPage([595, 842]);
const name = page.node.newFontDictionary('SYN', font);
text.split('\n').forEach((line, index) => page.pushOperators(beginText(), setFontAndSize(name, 10),
moveText(30, 790 - index * 22), showText(PDFHexString.of(hex(line))), endText()));
}
return Buffer.from(await doc.save());
}
function syntheticDocumentEvidence(options) {
return req('./src/services/paymentReceiptDocument').classifyReceiptText(syntheticReceiptText(options));
}
module.exports = { syntheticReceiptText, syntheticBankReceiptPdf, syntheticDocumentEvidence };