实现腾讯通用票据OCR安全适配(07-28-travel-reimbursement-guide)
这个提交包含在:
@@ -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,
|
||||
};
|
||||
|
||||
在新工单中引用
屏蔽一个用户