V1-V6 收口移动视口与 PDF 发票上传

这个提交包含在:
2026-05-10 23:30:13 +08:00
父节点 3c9cd3aa41
当前提交 491b42a232
共修改 47 个文件,包含 387 行新增和 244 行删除
+8 -9
查看文件
@@ -23,12 +23,7 @@ const {
resolveUploadDedupDecision,
} = require("../services/invoiceGovernance");
const MIME_ALLOWLIST = new Set([
"application/pdf",
"image/jpeg",
"image/png",
"image/webp",
]);
const INVOICE_UPLOAD_MIME_TYPE = "application/pdf";
const OCR_TYPE_ALLOWLIST = new Set(["vat", "train", "flight", "taxi", "general"]);
const bindBodySchema = z.union([
@@ -200,6 +195,10 @@ function parseDateOnlyString(value) {
return new Date(`${trimmed}T00:00:00.000Z`);
}
function isPdfInvoiceUpload(fileName, mimeType) {
return mimeType === INVOICE_UPLOAD_MIME_TYPE && path.extname(fileName ?? "").toLowerCase() === ".pdf";
}
async function resolveDefaultExpenseType(tx) {
const preferred = await tx.expenseType.findFirst({
where: {
@@ -369,12 +368,12 @@ async function invoicePoolRoutes(fastify) {
fileName = part.filename ?? "upload";
fileMimeType = part.mimetype;
if (!MIME_ALLOWLIST.has(fileMimeType)) {
if (!isPdfInvoiceUpload(fileName, fileMimeType)) {
throw new AppError({
code: "VALID_001",
message: "参数校验失败",
message: "请上传 PDF 发票文件",
statusCode: 400,
details: { file: "unsupported_mime_type" },
details: { file: "pdf_invoice_required" },
});
}
@@ -193,9 +193,9 @@ describe("invoicePool upload route", () => {
try {
const requestPayload = buildMultipartRequest({
filename: "invoice.png",
contentType: "image/png",
content: "mock-image",
filename: "invoice.pdf",
contentType: "application/pdf",
content: "%PDF-1.4 mock invoice",
fields: [["ocr_type", "vat"]],
});
@@ -234,6 +234,43 @@ describe("invoicePool upload route", () => {
}
});
it("rejects image invoice uploads because formal invoice OCR only accepts PDF", async () => {
const { app, prisma, tempDir } = await buildApp();
try {
const requestPayload = buildMultipartRequest({
filename: "invoice.png",
contentType: "image/png",
content: "mock-image",
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 发票文件",
details: { file: "pdf_invoice_required" },
}),
);
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({