feat(server): 完成 P2 后端能力(支付幂等、委派回收、OCR异步、审计)

这个提交包含在:
2026-02-07 21:54:29 +08:00
父节点 7ea25e9d72
当前提交 c4df9a419a
共修改 23 个文件,包含 2102 行新增和 70 行删除
+103
查看文件
@@ -0,0 +1,103 @@
const tencentcloud = require("tencentcloud-sdk-nodejs");
function normalizeDate(value) {
if (!value || typeof value !== "string") {
return null;
}
const compact = value.trim();
if (!compact) {
return null;
}
const replaced = compact.replace(/年|\//g, "-").replace(/月/g, "-").replace(/日/g, "");
const matched = replaced.match(/^(\d{4})-(\d{1,2})-(\d{1,2})$/);
if (!matched) {
return null;
}
const [, year, month, day] = matched;
return `${year}-${month.padStart(2, "0")}-${day.padStart(2, "0")}`;
}
function toNumberString(value) {
if (value == null) {
return null;
}
if (typeof value === "number" && Number.isFinite(value)) {
return value.toFixed(2);
}
if (typeof value !== "string") {
return null;
}
const stripped = value.replace(/[,¥\s]/g, "").trim();
if (!stripped) {
return null;
}
const num = Number(stripped);
if (!Number.isFinite(num)) {
return null;
}
return num.toFixed(2);
}
function normalizeString(value) {
if (typeof value !== "string") {
return null;
}
const normalized = value.trim();
return normalized || null;
}
function createTencentOcrClient(config) {
if (!config.tencentSecretId || !config.tencentSecretKey) {
throw new Error("Tencent OCR credentials are missing");
}
const OcrClient = tencentcloud.ocr.v20181119.Client;
return new OcrClient({
credential: {
secretId: config.tencentSecretId,
secretKey: config.tencentSecretKey,
},
region: config.tencentOcrRegion,
profile: {
httpProfile: {
timeout: config.ocrTimeoutMs,
},
},
});
}
function normalizeVatInvoiceResult(result) {
return {
invoiceCode: normalizeString(result?.InvoiceCode),
invoiceNumber: normalizeString(result?.InvoiceNum),
invoiceDate: normalizeDate(result?.InvoiceDate),
amount: toNumberString(result?.AmountWithoutTax),
taxAmount: toNumberString(result?.TaxAmount),
totalAmount: toNumberString(result?.TotalAmount ?? result?.AmountInFiguers),
sellerName: normalizeString(result?.SellerName),
buyerName: normalizeString(result?.BuyerName),
raw: result ?? {},
};
}
async function runTencentVatOcr(config, imageBase64) {
const client = createTencentOcrClient(config);
const result = await client.VatInvoiceOCR({
ImageBase64: imageBase64,
});
return normalizeVatInvoiceResult(result);
}
module.exports = {
runTencentVatOcr,
};