feat(server): readiness 与作业统计可观测性(E11)

- 新增 /ready(DB 探活失败返回 503)
- OCR 作业输出 claimed/available/duplicate/failed 统计
- README 补充 /health 与 /ready 说明
这个提交包含在:
2026-02-09 00:02:49 +08:00
父节点 479565f737
当前提交 75d5b3d5de
共修改 5 个文件,包含 54 行新增和 7 行删除
+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 {