feat(server): 完成 P2 后端能力(支付幂等、委派回收、OCR异步、审计)

这个提交包含在:
2026-02-07 21:54:29 +08:00
父节点 7ea25e9d72
当前提交 c4df9a419a
共修改 23 个文件,包含 2102 行新增和 70 行删除
+103
查看文件
@@ -0,0 +1,103 @@
const cron = require("node-cron");
async function revokeExpiredDelegations(prisma, logger, options = {}) {
const batchSize = options.batchSize ?? 200;
let total = 0;
while (true) {
const now = new Date();
const expired = await prisma.delegationGrant.findMany({
where: {
revoked_at: null,
valid_to: {
not: null,
lte: now,
},
},
orderBy: { valid_to: "asc" },
take: batchSize,
select: {
id: true,
principal_employee_id: true,
delegate_employee_id: true,
},
});
if (!expired.length) {
break;
}
await prisma.$transaction(async (tx) => {
const ids = expired.map((item) => item.id);
await tx.delegationGrant.updateMany({
where: {
id: { in: ids },
revoked_at: null,
},
data: {
revoked_at: now,
},
});
await tx.auditLog.createMany({
data: expired.map((item) => ({
request_id: `job:delegation-revoke:${now.getTime()}:${item.id}`,
actor_id: item.principal_employee_id,
action: "DELEGATION_AUTO_REVOKED",
target_type: "delegation_grant",
target_id: item.id,
ip_address: null,
user_agent: "system/node-cron",
extra: {
principalEmployeeId: item.principal_employee_id,
delegateEmployeeId: item.delegate_employee_id,
},
})),
});
});
total += expired.length;
}
if (total > 0) {
logger.info({ revoked: total }, "Expired delegations auto-revoked");
}
return total;
}
function startDelegationRevokeJob(fastify) {
let running = false;
const task = cron.schedule(fastify.config.delegationRevokeCron, async () => {
if (running) {
return;
}
running = true;
try {
await revokeExpiredDelegations(fastify.prisma, fastify.log);
} catch (error) {
fastify.log.error({ err: error }, "Delegation revoke job failed");
} finally {
running = false;
}
});
fastify.addHook("onClose", async () => {
task.stop();
task.destroy();
});
fastify.log.info(
{ cron: fastify.config.delegationRevokeCron },
"Delegation revoke job scheduled",
);
return task;
}
module.exports = {
revokeExpiredDelegations,
startDelegationRevokeJob,
};