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)
这个提交包含在:
@@ -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}`);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
在新工单中引用
屏蔽一个用户