借款登记支持上传原借入回单识别预填(09-17-company-loan-principal-repayment)
- 新增识别与保存接口:可读 PDF 本地解析,必要时有界调用现有回单 OCR;预填上下文加密绑定经办人、文件和 30 分钟 - 适配建行单位客户专用回单表格版式:按位置归行排序、竖排栏头、顶行无标签日期、大小写金额 - 拦截非最终、失败/处理中、多笔交易(含跨页无关联、编号冲突)、非人民币、公司支出和其他收款主体 - 金额、日期、编号整段校验;借款与原件同事务保存,失败回滚并清理对象;同一出借人原件唯一 - 保存绑定所选出借人版本;前端冻结核对时版本,迟到结果与切换账号不覆盖、不重发 - 测试:单测、隔离 HTTP/浏览器、变异;对账浏览器场景固定 UTC;五轮独立复核通过 - 无新增迁移 Co-Authored-By: Claude Opus 5.5 <[email protected]>
这个提交包含在:
@@ -79,7 +79,7 @@ module.exports = async function runBrowser({ app, finance, manager, ordinary, pa
|
||||
for (const kind of ['agreement', 'incoming', 'accounting_confirmation']) {
|
||||
const received = page.waitForResponse(r => r.url().endsWith(`/company-loans/${loan.id}/evidences/${kind}`) && r.request().method() === 'POST');
|
||||
await page.locator(`[id="${loan.id}-${kind}"]`).setInputFiles({ name: 'synthetic.pdf', mimeType: 'application/pdf',
|
||||
buffer: Buffer.from('%PDF-1.4\n1 0 obj << /Type /Catalog >> endobj\n%%EOF\n') });
|
||||
buffer: Buffer.from(`%PDF-1.4\n% synthetic-${kind}-${loan.id}\n1 0 obj << /Type /Catalog >> endobj\n%%EOF\n`) });
|
||||
assert.equal((await received).status(), 200);
|
||||
}
|
||||
await card.getByRole('button', { name: '确认期初余额' }).click();
|
||||
@@ -182,7 +182,9 @@ module.exports = async function runBrowser({ app, finance, manager, ordinary, pa
|
||||
for (const [index, batchId] of reconciliationUi.batches.entries()) {
|
||||
const mobile = index === 1;
|
||||
async function session(who) {
|
||||
const context = await browser.newContext({ viewport: mobile ? { width: 390, height: 844 } : { width: 1440, height: 1000 }, serviceWorkers: 'block' });
|
||||
// Reconciliation forms fill UTC wall-clock times; pin the browser to UTC
|
||||
// instead of inheriting the host timezone (fails 00:00–08:00 in CST).
|
||||
const context = await browser.newContext({ viewport: mobile ? { width: 390, height: 844 } : { width: 1440, height: 1000 }, serviceWorkers: 'block', timezoneId: 'UTC' });
|
||||
await context.route('**/*', route => route.request().url().startsWith(`${origin}/`) ? route.continue() : route.abort());
|
||||
const page = await context.newPage();
|
||||
page.on('pageerror', (error) => console.log(JSON.stringify({ stage: 'synthetic_browser_error', name: error.name, message: error.message.replace(/\d{8,}/g, '[redacted]').slice(0, 240) })));
|
||||
@@ -236,7 +238,9 @@ module.exports = async function runBrowser({ app, finance, manager, ordinary, pa
|
||||
for (const recovery of [false, true]) for (const [index, batch] of (recovery ? reconciliationUi.recoveryBatches : reconciliationUi.terminalBatches).entries()) {
|
||||
const mobile = index === 1;
|
||||
async function session(who) {
|
||||
const context = await browser.newContext({ viewport: mobile ? { width: 390, height: 844 } : { width: 1440, height: 1000 }, serviceWorkers: 'block' });
|
||||
// Reconciliation forms fill UTC wall-clock times; pin the browser to UTC
|
||||
// instead of inheriting the host timezone (fails 00:00–08:00 in CST).
|
||||
const context = await browser.newContext({ viewport: mobile ? { width: 390, height: 844 } : { width: 1440, height: 1000 }, serviceWorkers: 'block', timezoneId: 'UTC' });
|
||||
await context.route('**/*', route => route.request().url().startsWith(`${origin}/`) ? route.continue() : route.abort());
|
||||
const page = await context.newPage();
|
||||
page.on('pageerror', (error) => console.log(JSON.stringify({ stage: 'synthetic_browser_error', name: error.name, message: error.message.replace(/\d{8,}/g, '[redacted]').slice(0, 240) })));
|
||||
|
||||
在新工单中引用
屏蔽一个用户