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

这个提交包含在:
2026-02-07 21:54:29 +08:00
父节点 7ea25e9d72
当前提交 c4df9a419a
共修改 23 个文件,包含 2102 行新增和 70 行删除
+98 -32
查看文件
@@ -3,6 +3,10 @@ const { AppError } = require("../utils/AppError");
const { sendData } = require("../utils/reply");
const { resolvePermissionsForRoles, hasPermission } = require("../constants/permissions");
const { getWorkflowStepsSnapshot, resolveActionableStep } = require("../utils/workflow");
const {
findActiveDelegationForDelegate,
listActiveDelegationPrincipals,
} = require("../utils/delegation");
const TASK_ID_SCHEMA = z.string().uuid();
const REQUIRED_COMMENT_SCHEMA = z.object({
@@ -98,20 +102,54 @@ function ensurePendingTask(task) {
}
}
function ensureTaskAssignableToCurrentEmployee(task, request) {
async function ensureTaskAssignableToCurrentEmployee(tx, task, request) {
const matchesAssigneeId = Boolean(task.assignee_id && task.assignee_id === request.employee.id);
const matchesAssigneeRole = Boolean(
task.assignee_role && request.employee.roleCodes.includes(task.assignee_role),
);
const isUnassigned = !task.assignee_id && !task.assignee_role;
if (!(matchesAssigneeId || matchesAssigneeRole || isUnassigned)) {
throw new AppError({
code: "PERM_002",
message: "不在审批范围/非当前处理人",
statusCode: 403,
});
if (matchesAssigneeId || matchesAssigneeRole || isUnassigned) {
return {
delegated: false,
delegationId: null,
principalId: null,
};
}
if (task.assignee_id) {
const delegation = await findActiveDelegationForDelegate(tx, {
principalEmployeeId: task.assignee_id,
delegateEmployeeId: request.employee.id,
allowedScopes: ["all"],
});
if (delegation) {
return {
delegated: true,
delegationId: delegation.id,
principalId: delegation.principal_employee_id,
};
}
}
throw new AppError({
code: "PERM_002",
message: "不在审批范围/非当前处理人",
statusCode: 403,
});
}
function withDelegationPayload(payload, assignment, delegateId) {
const nextPayload = payload ? { ...payload } : {};
if (assignment?.delegated) {
nextPayload.delegation_id = assignment.delegationId;
nextPayload.principal_id = assignment.principalId;
nextPayload.delegate_id = delegateId;
}
return nextPayload;
}
async function loadWorkflowInstanceForTask(tx, instanceId) {
@@ -253,11 +291,19 @@ async function workflowRoutes(fastify) {
const roleFilters = request.employee.roleCodes.length
? [{ assignee_role: { in: request.employee.roleCodes } }]
: [];
const delegatedPrincipalIds = await listActiveDelegationPrincipals(fastify.prisma, {
delegateEmployeeId: request.employee.id,
allowedScopes: ["all"],
});
const assigneeFilters = [{ assignee_id: request.employee.id }, ...roleFilters];
if (delegatedPrincipalIds.length) {
assigneeFilters.push({ assignee_id: { in: delegatedPrincipalIds } });
}
const tasks = await fastify.prisma.workflowTask.findMany({
where: {
status,
OR: [{ assignee_id: request.employee.id }, ...roleFilters],
OR: assigneeFilters,
},
orderBy: { created_at: "desc" },
select: {
@@ -299,7 +345,7 @@ async function workflowRoutes(fastify) {
await fastify.prisma.$transaction(async (tx) => {
const task = await lockWorkflowTask(tx, taskId);
ensurePendingTask(task);
ensureTaskAssignableToCurrentEmployee(task, request);
const assignment = await ensureTaskAssignableToCurrentEmployee(tx, task, request);
const workflowInstance = await loadWorkflowInstanceForTask(tx, task.instance_id);
const steps = getWorkflowStepsSnapshot(workflowInstance);
@@ -367,10 +413,14 @@ async function workflowRoutes(fastify) {
task_id: taskId,
actor_id: request.employee.id,
action: "approve",
payload: {
from_step_id: task.step_id,
to_step_id: actionable.step.id,
},
payload: withDelegationPayload(
{
from_step_id: task.step_id,
to_step_id: actionable.step.id,
},
assignment,
request.employee.id,
),
},
});
@@ -407,10 +457,14 @@ async function workflowRoutes(fastify) {
task_id: taskId,
actor_id: request.employee.id,
action: "approve",
payload: {
from_step_id: task.step_id,
terminal: true,
},
payload: withDelegationPayload(
{
from_step_id: task.step_id,
terminal: true,
},
assignment,
request.employee.id,
),
},
});
@@ -440,7 +494,7 @@ async function workflowRoutes(fastify) {
await fastify.prisma.$transaction(async (tx) => {
const task = await lockWorkflowTask(tx, taskId);
ensurePendingTask(task);
ensureTaskAssignableToCurrentEmployee(task, request);
const assignment = await ensureTaskAssignableToCurrentEmployee(tx, task, request);
const workflowInstance = await loadWorkflowInstanceForTask(tx, task.instance_id);
const now = new Date();
@@ -482,9 +536,13 @@ async function workflowRoutes(fastify) {
actor_id: request.employee.id,
action: "reject",
comment,
payload: {
from_step_id: task.step_id,
},
payload: withDelegationPayload(
{
from_step_id: task.step_id,
},
assignment,
request.employee.id,
),
},
});
@@ -513,7 +571,7 @@ async function workflowRoutes(fastify) {
await fastify.prisma.$transaction(async (tx) => {
const task = await lockWorkflowTask(tx, taskId);
ensurePendingTask(task);
ensureTaskAssignableToCurrentEmployee(task, request);
const assignment = await ensureTaskAssignableToCurrentEmployee(tx, task, request);
const workflowInstance = await loadWorkflowInstanceForTask(tx, task.instance_id);
const now = new Date();
@@ -555,9 +613,13 @@ async function workflowRoutes(fastify) {
actor_id: request.employee.id,
action: "return",
comment,
payload: {
from_step_id: task.step_id,
},
payload: withDelegationPayload(
{
from_step_id: task.step_id,
},
assignment,
request.employee.id,
),
},
});
@@ -586,7 +648,7 @@ async function workflowRoutes(fastify) {
await fastify.prisma.$transaction(async (tx) => {
const task = await lockWorkflowTask(tx, taskId);
ensurePendingTask(task);
ensureTaskAssignableToCurrentEmployee(task, request);
const assignment = await ensureTaskAssignableToCurrentEmployee(tx, task, request);
const workflowInstance = await loadWorkflowInstanceForTask(tx, task.instance_id);
const transferCount = await ensureTransferChainNotExceeded(tx, taskId);
@@ -630,12 +692,16 @@ async function workflowRoutes(fastify) {
actor_id: request.employee.id,
action: "transfer",
comment: reason,
payload: {
from_approver_id: request.employee.id,
to_approver_id: toApproverId,
reason,
transfer_count: transferCount + 1,
},
payload: withDelegationPayload(
{
from_approver_id: request.employee.id,
to_approver_id: toApproverId,
reason,
transfer_count: transferCount + 1,
},
assignment,
request.employee.id,
),
},
});