feat(server): readiness 与作业统计可观测性(E11)
- 新增 /ready(DB 探活失败返回 503) - OCR 作业输出 claimed/available/duplicate/failed 统计 - README 补充 /health 与 /ready 说明
这个提交包含在:
@@ -148,6 +148,21 @@ async function buildApp() {
|
||||
return reply.send({ requestId: request.id, data: { ok: true } });
|
||||
});
|
||||
|
||||
app.get("/ready", async (request, reply) => {
|
||||
try {
|
||||
await app.prisma.$queryRaw`SELECT 1`;
|
||||
return reply.send({ requestId: request.id, data: { ok: true } });
|
||||
} catch (error) {
|
||||
app.log.error({ err: error, requestId: request.id }, "Readiness check failed");
|
||||
throw new AppError({
|
||||
code: "SYS_503",
|
||||
message: "Service Unavailable",
|
||||
statusCode: 503,
|
||||
details: { db: "unreachable" },
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
await app.register(authRoutes, { prefix: "/api/auth" });
|
||||
await app.register(meRoutes, { prefix: "/api" });
|
||||
await app.register(reimbursementRoutes, { prefix: "/api" });
|
||||
|
||||
+25
-5
@@ -249,28 +249,39 @@ async function processClaimedInvoice(prisma, config, claimedInvoice, workerId, l
|
||||
},
|
||||
"OCR invoice marked as duplicate",
|
||||
);
|
||||
return;
|
||||
return { outcome: "duplicate" };
|
||||
}
|
||||
|
||||
await markInvoiceAsAvailable(prisma, claimedInvoice.id, normalized, ocrResultId);
|
||||
return { outcome: "available" };
|
||||
} catch (error) {
|
||||
const message = trimErrorMessage(error);
|
||||
await markInvoiceAsFailed(prisma, claimedInvoice, message, config);
|
||||
logger.error({ err: error, invoiceId: claimedInvoice.id, workerId }, "OCR processing failed");
|
||||
return { outcome: "failed" };
|
||||
}
|
||||
}
|
||||
|
||||
async function runOcrOnce(prisma, config, logger, workerId = `ocr:${process.pid}`) {
|
||||
const claimed = await claimPendingInvoices(prisma, config, workerId);
|
||||
if (!claimed.length) {
|
||||
return { claimed: 0 };
|
||||
return { claimed: 0, available: 0, duplicate: 0, failed: 0 };
|
||||
}
|
||||
|
||||
const stats = { claimed: claimed.length, available: 0, duplicate: 0, failed: 0 };
|
||||
|
||||
for (const item of claimed) {
|
||||
await processClaimedInvoice(prisma, config, item, workerId, logger);
|
||||
const result = await processClaimedInvoice(prisma, config, item, workerId, logger);
|
||||
if (result?.outcome === "available") {
|
||||
stats.available += 1;
|
||||
} else if (result?.outcome === "duplicate") {
|
||||
stats.duplicate += 1;
|
||||
} else if (result?.outcome === "failed") {
|
||||
stats.failed += 1;
|
||||
}
|
||||
}
|
||||
|
||||
return { claimed: claimed.length };
|
||||
return stats;
|
||||
}
|
||||
|
||||
function startOcrJob(fastify) {
|
||||
@@ -289,7 +300,16 @@ function startOcrJob(fastify) {
|
||||
|
||||
running = true;
|
||||
try {
|
||||
await runOcrOnce(fastify.prisma, fastify.config, fastify.log, workerId);
|
||||
const result = await runOcrOnce(fastify.prisma, fastify.config, fastify.log, workerId);
|
||||
if (result.claimed > 0) {
|
||||
fastify.log.info(
|
||||
{
|
||||
workerId,
|
||||
...result,
|
||||
},
|
||||
"OCR job tick processed",
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
fastify.log.error({ err: error }, "OCR job tick failed");
|
||||
} finally {
|
||||
|
||||
@@ -15,7 +15,9 @@ const { runOcrOnce } = require("../jobs/ocrJob");
|
||||
}
|
||||
|
||||
const result = await runOcrOnce(prisma, config, console, `ocr-script:${process.pid}`);
|
||||
process.stdout.write(`claimed=${result.claimed}\n`);
|
||||
process.stdout.write(
|
||||
`claimed=${result.claimed} available=${result.available} duplicate=${result.duplicate} failed=${result.failed}\n`,
|
||||
);
|
||||
} finally {
|
||||
await prisma.$disconnect();
|
||||
await pool.end();
|
||||
|
||||
在新工单中引用
屏蔽一个用户