feat: 完成错误原因合同与付款就绪提示(09-05-error-explainability-payment-readiness)
ci-e2e / e2e (push) Canceled after 0s
ci-minimal / check-minimal (push) Canceled after 0s

这个提交包含在:
2026-09-05 19:00:35 +08:00
父节点 ab82fe30c0
当前提交 a837912638
共修改 42 个文件,包含 2867 行新增和 876 行删除
+9 -18
查看文件
@@ -1,3 +1,4 @@
const { buildBusinessValidationError } = require("../utils/businessValidationError");
const { z } = require("zod");
const { hasPermission } = require("../constants/permissions");
@@ -60,19 +61,12 @@ const LIST_QUERY_SCHEMA = z.object({
pageSize: z.coerce.number().int().min(1).max(100).optional(),
});
function buildValidationError(details, statusCode = 400) {
return new AppError({
code: "VALID_001",
message: "参数校验失败",
statusCode,
details,
});
}
function parseInput(schema, value) {
const parsed = schema.safeParse(value ?? {});
if (!parsed.success) {
throw buildValidationError(parsed.error.flatten());
throw buildBusinessValidationError("attendance_correction.invalid_input", parsed.error.flatten(), 400);
}
return parsed.data;
}
@@ -80,7 +74,7 @@ function parseInput(schema, value) {
function parseUuid(value, key = "id") {
const parsed = UUID_SCHEMA.safeParse(value);
if (!parsed.success) {
throw buildValidationError({ [key]: "invalid_uuid" });
throw buildBusinessValidationError("attendance_correction.invalid_uuid", { [key]: "invalid_uuid" }, 400);
}
return parsed.data;
}
@@ -93,7 +87,7 @@ function parseOptionalDateTime(value) {
if (!value) return null;
const date = new Date(value);
if (Number.isNaN(date.getTime())) {
throw buildValidationError({ datetime: "invalid" });
throw buildBusinessValidationError("attendance_correction.datetime_invalid", { datetime: "invalid" }, 400);
}
return date;
}
@@ -119,10 +113,7 @@ async function ensureUnifiedFeatureEnabled(fastify, request) {
strictDepartment: false,
});
if (!gate.enabled) {
throw buildValidationError(
{ featureFlag: "attendance_unified_v1_disabled", reason: gate.reason },
409,
);
throw buildBusinessValidationError("attendance_correction.feature_flag_attendance_unified_v1_disabled", { featureFlag: "attendance_unified_v1_disabled", reason: gate.reason }, 409);
}
}
@@ -289,7 +280,7 @@ async function attendanceCorrectionsRoutes(fastify) {
select: { id: true, definition_json: true },
});
if (!workflowDefinition) {
throw buildValidationError({ workflow: "missing_definition" }, 409);
throw buildBusinessValidationError("attendance_correction.workflow_missing_definition", { workflow: "missing_definition" }, 409);
}
const workflowSteps = parseWorkflowSteps(workflowDefinition.definition_json);
const { actionable, skipped } = await resolveActionableStep({
@@ -301,7 +292,7 @@ async function attendanceCorrectionsRoutes(fastify) {
blockOnUnresolvedAssignee: true,
});
if (!actionable) {
throw buildValidationError({ workflow: "no_actionable_approver" }, 409);
throw buildBusinessValidationError("attendance_correction.workflow_no_actionable_approver", { workflow: "no_actionable_approver" }, 409);
}
const workflowInstance = await tx.workflowInstance.create({
@@ -512,7 +503,7 @@ async function attendanceCorrectionsRoutes(fastify) {
throw new AppError({ code: "PERM_001", message: "无权限", statusCode: 403 });
}
if (correction.status !== "reviewing") {
throw buildValidationError({ status: "invalid_cancel_status" }, 409);
throw buildBusinessValidationError("attendance_correction.status_invalid_cancel_status", { status: "invalid_cancel_status" }, 409);
}
const workflowInstance = await tx.workflowInstance.findFirst({