feat(server): 完成 P2 后端能力(支付幂等、委派回收、OCR异步、审计)
这个提交包含在:
+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,
|
||||
),
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
在新工单中引用
屏蔽一个用户