- 新增 Prometheus 指标端点 /metrics(HTTP + DB + 作业统计) - request.log 绑定 requestId/actorId,避免日志难追踪 - cron/pg-boss 作业增加 metrics 计数(runs/processed) - 更新 env/runbook/README 与 v3 CSV 状态推进
130 行
3.2 KiB
JavaScript
130 行
3.2 KiB
JavaScript
const cron = require("node-cron");
|
|
|
|
async function claimAndArchiveUnusedInvoices(tx, batchSize, unusedDays) {
|
|
return tx.$queryRaw`
|
|
WITH picked AS (
|
|
SELECT id
|
|
FROM "invoice_pool"
|
|
WHERE linked_reimbursement_id IS NULL
|
|
AND status IN ('available', 'ocr_failed')
|
|
AND created_at < NOW() - make_interval(days => ${unusedDays})
|
|
ORDER BY created_at ASC
|
|
LIMIT ${batchSize}
|
|
FOR UPDATE SKIP LOCKED
|
|
)
|
|
UPDATE "invoice_pool" AS i
|
|
SET status = 'archived',
|
|
archived_reason = 'unused_timeout',
|
|
archived_at = NOW(),
|
|
linked_item_id = NULL,
|
|
ocr_locked_at = NULL,
|
|
ocr_lock_owner = NULL,
|
|
updated_by = NULL
|
|
FROM picked
|
|
WHERE i.id = picked.id
|
|
AND i.linked_reimbursement_id IS NULL
|
|
AND i.status IN ('available', 'ocr_failed')
|
|
RETURNING i.id, i.owner_employee_id, i.status, i.archived_at
|
|
`;
|
|
}
|
|
|
|
async function archiveUnusedInvoices(prisma, logger, options = {}) {
|
|
const batchSize = Math.max(1, Number(options.batchSize ?? 200));
|
|
const unusedDays = Math.max(1, Number(options.unusedDays ?? 90));
|
|
|
|
let total = 0;
|
|
let round = 0;
|
|
|
|
while (true) {
|
|
round += 1;
|
|
|
|
const archivedRows = await prisma.$transaction(async (tx) => {
|
|
return claimAndArchiveUnusedInvoices(tx, batchSize, unusedDays);
|
|
});
|
|
|
|
if (!archivedRows.length) {
|
|
break;
|
|
}
|
|
|
|
total += archivedRows.length;
|
|
logger.info(
|
|
{
|
|
round,
|
|
claimed: archivedRows.length,
|
|
updated: archivedRows.length,
|
|
archived: archivedRows.length,
|
|
unusedDays,
|
|
},
|
|
"Unused invoices archived batch processed",
|
|
);
|
|
}
|
|
|
|
if (total > 0) {
|
|
logger.info({ archived: total, unusedDays }, "Unused invoices auto-archived");
|
|
}
|
|
|
|
return total;
|
|
}
|
|
|
|
function startInvoiceArchiveJob(fastify) {
|
|
let running = false;
|
|
|
|
const task = cron.schedule(fastify.config.invoiceArchiveCron, async () => {
|
|
if (running) {
|
|
return;
|
|
}
|
|
|
|
running = true;
|
|
try {
|
|
const archived = await archiveUnusedInvoices(fastify.prisma, fastify.log, {
|
|
batchSize: fastify.config.invoiceArchiveBatchSize,
|
|
unusedDays: fastify.config.invoiceArchiveUnusedDays,
|
|
});
|
|
fastify.metrics?.jobRunsTotal?.inc?.({
|
|
job: "invoice.archive-unused",
|
|
driver: "cron",
|
|
outcome: "success",
|
|
});
|
|
if (archived > 0) {
|
|
fastify.metrics?.jobProcessedTotal?.inc?.(
|
|
{
|
|
job: "invoice.archive-unused",
|
|
driver: "cron",
|
|
kind: "archived",
|
|
},
|
|
archived,
|
|
);
|
|
}
|
|
} catch (error) {
|
|
fastify.log.error({ err: error }, "Invoice archive job failed");
|
|
fastify.metrics?.jobRunsTotal?.inc?.({
|
|
job: "invoice.archive-unused",
|
|
driver: "cron",
|
|
outcome: "failure",
|
|
});
|
|
} finally {
|
|
running = false;
|
|
}
|
|
});
|
|
|
|
fastify.addHook("onClose", async () => {
|
|
task.stop();
|
|
task.destroy();
|
|
});
|
|
|
|
fastify.log.info(
|
|
{
|
|
cron: fastify.config.invoiceArchiveCron,
|
|
unusedDays: fastify.config.invoiceArchiveUnusedDays,
|
|
},
|
|
"Invoice archive job scheduled",
|
|
);
|
|
|
|
return task;
|
|
}
|
|
|
|
module.exports = {
|
|
archiveUnusedInvoices,
|
|
startInvoiceArchiveJob,
|
|
};
|