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

这个提交包含在:
2026-07-28 22:02:11 +08:00
父节点 b4bd484260
当前提交 ac17092fa0
共修改 19 个文件,包含 1277 行新增和 127 行删除
@@ -43,6 +43,7 @@ async function buildApp({
invoiceConflicts = [],
statusRows = [],
permissionCodes = ["invoice.upload"],
maxUploadSizeBytes = 5 * 1024 * 1024,
} = {}) {
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "hyb-oa-r71-invoice-upload-"));
const invoicePoolCreate = vi.fn(async ({ data }) => ({
@@ -74,7 +75,7 @@ async function buildApp({
app.decorate("config", {
appTimezone: "Asia/Shanghai",
storageDir: tempDir,
maxUploadSizeBytes: 5 * 1024 * 1024,
maxUploadSizeBytes,
invoiceArchiveUnusedDays: 30,
ocrProvider,
jobDriver,
@@ -121,7 +122,7 @@ async function buildApp({
await app.register(multipart, {
attachFieldsToBody: false,
limits: { files: 1, fileSize: 5 * 1024 * 1024 },
limits: { files: 1, fileSize: maxUploadSizeBytes },
});
await app.register(invoicePoolRoutes, { prefix: "/api" });
@@ -271,6 +272,48 @@ describe("invoicePool upload route", () => {
}
});
it("rejects a PDF above the 7 MiB Tencent OCR input boundary before storage", async () => {
const { app, prisma, tempDir } = await buildApp({
maxUploadSizeBytes: 8 * 1024 * 1024,
});
try {
const requestPayload = buildMultipartRequest({
filename: "invoice.pdf",
contentType: "application/pdf",
content: Buffer.alloc(7 * 1024 * 1024 + 1, 0x20),
fields: [["ocr_type", "vat"]],
});
const response = await app.inject({
method: "POST",
url: "/api/invoice-pool/upload",
headers: {
authorization: "Bearer test",
...requestPayload.headers,
},
payload: requestPayload.payload,
});
expect(response.statusCode).toBe(400);
expect(response.json().error).toEqual(
expect.objectContaining({
code: "VALID_001",
message: "PDF 发票不能超过 7 MiB",
details: {
file: "ocr_pdf_too_large",
maxBytes: 7 * 1024 * 1024,
},
}),
);
expect(prisma.invoicePool.create).not.toHaveBeenCalled();
expect(prisma.auditLog.create).not.toHaveBeenCalled();
} finally {
await app.close();
await fs.rm(tempDir, { recursive: true, force: true });
}
});
it("reuses same-owner existing invoice instead of creating a duplicate file record", async () => {
const existingInvoiceId = "22222222-2222-4222-8222-222222222222";
const { app, prisma, tempDir } = await buildApp({