feat(server): 完成 P3 后端API与回归(D2~D9)

- workflow: 任务详情/转审候选/列表分页字段(D5)
- reimbursements: 列表API + pay凭证字段/幂等增强(D7)
- invoice-pool: 列表/详情API + 上传pending_ocr(D9)
- ocr/audit: mock provider + 回归脚本 + regression-all开关(D2/D3)
- CI: 新增 ci-minimal / ci-regression 工作流(D4)
这个提交包含在:
2026-02-08 23:08:18 +08:00
父节点 17a6ff4ee0
当前提交 65e7b37ebd
共修改 14 个文件,包含 1681 行新增和 41 行删除
+9
查看文件
@@ -1,10 +1,19 @@
const { runTencentVatOcr } = require("./tencent");
const { runMockOcr } = require("./mock");
function isOcrEnabledForType(config, ocrType) {
if (config.ocrProvider === "mock") {
return true;
}
return config.ocrProvider === "tencent" && ocrType === "vat";
}
async function runOcrForInvoice({ config, ocrType, imageBase64 }) {
if (config.ocrProvider === "mock") {
return runMockOcr({ ocrType, imageBase64 });
}
if (config.ocrProvider !== "tencent") {
throw new Error(`Unsupported OCR provider: ${config.ocrProvider}`);
}
+60
查看文件
@@ -0,0 +1,60 @@
const crypto = require("node:crypto");
function decodeMarker(imageBase64) {
try {
return Buffer.from(imageBase64, "base64").toString("utf8");
} catch {
return "";
}
}
function buildInvoiceKey(markerText) {
const duplicateMatch = markerText.match(/MOCK_DUP:([A-Za-z0-9_-]+)/);
if (duplicateMatch?.[1]) {
return `dup:${duplicateMatch[1]}`;
}
const hash = crypto.createHash("sha256").update(markerText || "mock-empty").digest("hex");
return `hash:${hash.slice(0, 20)}`;
}
function toAmount(seedHex, fallback) {
const value = Number.parseInt(seedHex.slice(0, 8), 16);
if (!Number.isFinite(value)) {
return fallback;
}
return ((value % 50000) / 100 + fallback).toFixed(2);
}
async function runMockOcr({ ocrType, imageBase64 }) {
const markerText = decodeMarker(imageBase64);
if (markerText.includes("MOCK_OCR_FAIL")) {
throw new Error("mock_ocr_failed");
}
const invoiceKey = buildInvoiceKey(markerText);
const hash = crypto.createHash("sha256").update(invoiceKey).digest("hex");
const invoiceCode = `MOCK${hash.slice(0, 8).toUpperCase()}`;
const invoiceNumber = hash.slice(8, 16).toUpperCase();
return {
invoiceCode,
invoiceNumber,
invoiceDate: "2026-02-08",
amount: toAmount(hash.slice(16, 24), 100),
taxAmount: toAmount(hash.slice(24, 32), 10),
totalAmount: toAmount(hash.slice(32, 40), 110),
sellerName: `Mock Seller (${ocrType})`,
buyerName: "Mock Buyer",
raw: {
provider: "mock",
invoiceKey,
marker: markerText.slice(0, 128),
},
};
}
module.exports = {
runMockOcr,
};