文件
hyb-oa/server/src/jobs/invoiceArchiveJob.js
T
wangsli 8bd068f2a3 feat(server): 票据增强闭环(F9/F10/F11/F12/F13/F28)
- /invoice-pool 新增 unbind、PATCH 修正、manual archive、OCR retry、判重例外 override(含 VALID_004/006/007)

- signed-url 签发/下载写入审计日志(脱敏 extra)

- 新增未绑定票据自动归档作业 + 一次性执行脚本

- 回归脚本增强:workflow-actions/dedup/audit/ocr 覆盖上述场景;v3 CSV 状态推进至 DONE
2026-02-09 17:09:34 +08:00

111 行
2.6 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 {
await archiveUnusedInvoices(fastify.prisma, fastify.log, {
batchSize: fastify.config.invoiceArchiveBatchSize,
unusedDays: fastify.config.invoiceArchiveUnusedDays,
});
} catch (error) {
fastify.log.error({ err: error }, "Invoice archive job failed");
} 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,
};