实现腾讯通用票据OCR安全适配(07-28-travel-reimbursement-guide)

这个提交包含在:
2026-07-28 22:02:11 +08:00
父节点 b4bd484260
当前提交 ac17092fa0
共修改 19 个文件,包含 1277 行新增和 127 行删除
+98 -3
查看文件
@@ -1,10 +1,47 @@
const { runTencentVatOcr } = require("./tencent");
const { runTencentGeneralInvoiceOcr } = require("./tencent");
const { runMockOcr } = require("./mock");
const OCR_PDF_MAX_BYTES = 7 * 1024 * 1024;
const REQUIRED_OCR_FIELDS_BY_TYPE = {
vat: ["invoiceCode", "invoiceNumber", "invoiceDate", "totalAmount", "sellerName"],
};
const REQUIRED_OCR_FIELDS_BY_DOCUMENT_CATEGORY = {
vat_invoice: ["invoiceCode", "invoiceNumber", "invoiceDate", "totalAmount", "sellerName"],
full_vat_invoice: ["invoiceNumber", "invoiceDate", "totalAmount", "sellerName"],
electronic_train_ticket: ["invoiceNumber", "invoiceDate", "totalAmount", "buyerName"],
};
const NUMBER_ONLY_DEDUP_DOCUMENT_CATEGORIES = new Set([
"full_vat_invoice",
"electronic_train_ticket",
]);
const SAFE_DOCUMENT_CATEGORIES = new Set([
"vat_invoice",
"full_vat_invoice",
"electronic_train_ticket",
]);
const SAFE_QUALITY_ERRORS = new Set([
"invoice_not_detected",
"multiple_invoices_not_supported",
"invoice_recognition_failed",
"unsupported_invoice_subtype",
]);
const SAFE_NORMALIZED_FIELDS = [
"invoiceCode",
"invoiceNumber",
"invoiceDate",
"amount",
"taxAmount",
"totalAmount",
"sellerName",
"buyerName",
];
function isOcrEnabledForType(config, ocrType) {
if (config.ocrProvider === "mock") {
return ocrType === "vat";
@@ -14,7 +51,10 @@ function isOcrEnabledForType(config, ocrType) {
}
function getMissingRequiredOcrFields(ocrType, normalized) {
const requiredFields = REQUIRED_OCR_FIELDS_BY_TYPE[ocrType] ?? [];
const requiredFields =
REQUIRED_OCR_FIELDS_BY_DOCUMENT_CATEGORY[normalized?.documentCategory] ??
REQUIRED_OCR_FIELDS_BY_TYPE[ocrType] ??
[];
if (!requiredFields.length) {
return [];
}
@@ -26,6 +66,10 @@ function getMissingRequiredOcrFields(ocrType, normalized) {
}
function buildMissingRequiredOcrError(ocrType, normalized) {
if (typeof normalized?.qualityError === "string" && normalized.qualityError) {
return normalized.qualityError;
}
const missingFields = getMissingRequiredOcrFields(ocrType, normalized);
if (!missingFields.length) {
return null;
@@ -47,12 +91,63 @@ async function runOcrForInvoice({ config, ocrType, imageBase64, sourceMimeType }
throw new Error(`Unsupported OCR type: ${ocrType}`);
}
return runTencentVatOcr(config, { imageBase64, sourceMimeType });
return runTencentGeneralInvoiceOcr(config, { imageBase64, sourceMimeType });
}
function shouldDeduplicateByInvoiceNumberOnly(normalized) {
return Boolean(
normalized?.invoiceNumber &&
!normalized?.invoiceCode &&
NUMBER_ONLY_DEDUP_DOCUMENT_CATEGORIES.has(normalized?.documentCategory),
);
}
function normalizeSafeString(value, maxLength) {
if (typeof value !== "string") {
return null;
}
const normalized = value.trim();
return normalized ? normalized.slice(0, maxLength) : null;
}
function sanitizeOcrResultForPersistence(ocrProvider, normalized) {
if (ocrProvider !== "tencent") {
return normalized;
}
const safe = {
documentSubtype: normalizeSafeString(normalized?.documentSubtype, 64),
documentCategory: SAFE_DOCUMENT_CATEGORIES.has(normalized?.documentCategory)
? normalized.documentCategory
: null,
documentCount:
Number.isInteger(normalized?.documentCount) && normalized.documentCount >= 0
? Math.min(normalized.documentCount, 100)
: 1,
};
for (const field of SAFE_NORMALIZED_FIELDS) {
safe[field] = normalizeSafeString(normalized?.[field], field.endsWith("Name") ? 256 : 64);
}
if (SAFE_QUALITY_ERRORS.has(normalized?.qualityError)) {
safe.qualityError = normalized.qualityError;
}
if (normalized?.transportMode === "pdf_native") {
safe.transportMode = "pdf_native";
}
return safe;
}
module.exports = {
OCR_PDF_MAX_BYTES,
buildMissingRequiredOcrError,
getMissingRequiredOcrFields,
isOcrEnabledForType,
runOcrForInvoice,
sanitizeOcrResultForPersistence,
shouldDeduplicateByInvoiceNumberOnly,
};