H1-H10 优化OCR实时体验闭环

这个提交包含在:
2026-05-09 11:48:57 +08:00
父节点 549bd81e30
当前提交 c26cf9b954
共修改 34 个文件,包含 1217 行新增和 184 行删除
+52
查看文件
@@ -0,0 +1,52 @@
const Fastify = require("fastify");
const eventsRoutes = require("../../../src/routes/events");
const { createAppEventBus } = require("../../../src/services/events");
const { AppError } = require("../../../src/utils/AppError");
async function buildApp() {
const app = Fastify();
app.decorate("appEvents", createAppEventBus());
app.decorate("authenticate", async (request) => {
const auth = request.headers.authorization ?? "";
if (!auth.startsWith("Bearer ")) {
throw new AppError({ code: "AUTH_001", message: "未登录", statusCode: 401 });
}
request.employee = { id: "employee-1", permissionCodes: [] };
});
app.decorate("hasPermission", (employee, permissionCode) =>
(employee?.permissionCodes ?? []).includes(permissionCode),
);
app.setErrorHandler((error, request, reply) => {
if (error instanceof AppError) {
return reply.status(error.statusCode).send({
error: {
code: error.code,
message: error.message,
details: error.details ?? {},
},
});
}
return reply.status(500).send({ error: { code: "SYS_001", message: "服务异常" } });
});
await app.register(eventsRoutes, { prefix: "/api" });
return app;
}
describe("GET /api/events", () => {
it("requires bearer authentication before opening the event stream", async () => {
const app = await buildApp();
try {
const response = await app.inject({
method: "GET",
url: "/api/events",
});
expect(response.statusCode).toBe(401);
expect(response.json().error.code).toBe("AUTH_001");
} finally {
await app.close();
}
});
});
@@ -38,6 +38,8 @@ function buildMultipartRequest({ filename, contentType, content, fields = [] })
async function buildApp({
ocrProvider = "off",
jobDriver = "cron",
bossSend = null,
invoiceConflicts = [],
statusRows = [],
permissionCodes = ["invoice.upload"],
@@ -75,7 +77,11 @@ async function buildApp({
maxUploadSizeBytes: 5 * 1024 * 1024,
invoiceArchiveUnusedDays: 30,
ocrProvider,
jobDriver,
});
if (bossSend) {
app.decorate("boss", { send: bossSend });
}
app.decorate("authenticate", async (request) => {
if (!request.headers.authorization) {
throw new AppError({ code: "AUTH_001", message: "未登录", statusCode: 401 });
@@ -177,8 +183,13 @@ describe("invoicePool upload route", () => {
}
});
it("puts VAT uploads into pending_ocr when OCR provider is enabled", async () => {
const { app, prisma, tempDir } = await buildApp({ ocrProvider: "mock" });
it("puts VAT uploads into pending_ocr and enqueues a directed OCR job when OCR provider is enabled", async () => {
const bossSend = vi.fn(async () => ({ id: "ocr-job-1" }));
const { app, prisma, tempDir } = await buildApp({
ocrProvider: "mock",
jobDriver: "pgboss",
bossSend,
});
try {
const requestPayload = buildMultipartRequest({
@@ -214,6 +225,9 @@ describe("invoicePool upload route", () => {
}),
}),
);
expect(bossSend).toHaveBeenCalledWith("ocr.invoice", {
invoiceId: "11111111-1111-4111-8111-111111111111",
});
} finally {
await app.close();
await fs.rm(tempDir, { recursive: true, force: true });
@@ -133,6 +133,7 @@ async function buildApp(options = {}) {
],
travelRequests: (options.travelRequests ?? [buildTravelRequest()]).map((item) => cloneTravelRequest(item)),
auditLogs: [],
sequenceNumber: 0,
};
function buildReimbursementSelect(record, select) {
@@ -236,6 +237,28 @@ async function buildApp(options = {}) {
return found ? cloneTravelRequest(found) : null;
}),
},
documentSequence: {
findUnique: vi.fn(async () => null),
upsert: vi.fn(async ({ create, update }) => {
stateRef.sequenceNumber = (stateRef.sequenceNumber ?? 0) + 1;
return {
document_type: create.document_type,
prefix: update.prefix ?? create.prefix,
date_format: update.date_format ?? create.date_format,
separator: update.separator ?? create.separator,
number_length: update.number_length ?? create.number_length,
reset_period: update.reset_period ?? create.reset_period,
current_number: stateRef.sequenceNumber,
last_reset_date: null,
updated_at: new Date("2026-04-08T12:00:00.000Z"),
updated_by: update.updated_by ?? create.updated_by ?? null,
};
}),
},
documentCategory: {
findUnique: vi.fn(async () => null),
findFirst: vi.fn(async () => null),
},
reimbursementForm: {
create: vi.fn(async ({ data, select }) => {
const record = {
@@ -317,6 +340,7 @@ async function buildApp(options = {}) {
expenseTypes: state.expenseTypes.map((item) => ({ ...item })),
travelRequests: state.travelRequests.map((item) => cloneTravelRequest(item)),
auditLogs: state.auditLogs.map((item) => ({ ...item })),
sequenceNumber: state.sequenceNumber,
};
const tx = buildPrismaLike(txState);
const result = await callback(tx);
@@ -327,6 +351,7 @@ async function buildApp(options = {}) {
state.expenseTypes = txState.expenseTypes;
state.travelRequests = txState.travelRequests;
state.auditLogs = txState.auditLogs;
state.sequenceNumber = txState.sequenceNumber;
return result;
});
@@ -44,6 +44,7 @@ function buildReimbursementRecord(overrides = {}) {
},
},
],
attachments: [],
invoice_pool_items: [
{
id: "invoice-1",