实现业务附件受控入库 V1(05-28-business-attachment-governance-v1)

这个提交包含在:
2026-05-28 19:44:15 +08:00
父节点 2c067866ef
当前提交 52e5284954
共修改 25 个文件,包含 1119 行新增和 21 行删除
+58
查看文件
@@ -31,6 +31,16 @@ const {
} = require("../services/feeControl");
const { issueDocumentNumber } = require("../services/documentSequence");
const { openReadStream, StorageNotFoundError, writeBuffer } = require("../services/storage");
const {
ATTACHMENT_TYPE,
DOWNLOAD_POLICY,
SENSITIVITY_LEVEL,
governanceCreateData,
mapGovernanceFields,
parseMultipartGovernanceField,
requiresDownloadReason,
sanitizeDownloadReason,
} = require("../services/businessAttachmentGovernance");
const {
buildApprovalWorkflowSnapshot,
getWorkflowStepsSnapshot,
@@ -289,6 +299,12 @@ function mapReimbursementAttachment(item) {
mimeType: item.mime_type,
fileSize: item.file_size,
fileSha256: item.file_sha256,
...mapGovernanceFields(item, {
governanceStatus: "approved",
sensitivityLevel: SENSITIVITY_LEVEL.internal,
attachmentType: ATTACHMENT_TYPE.supportingMaterial,
downloadPolicy: DOWNLOAD_POLICY.standard,
}),
uploadedBy: item.uploader
? {
id: item.uploader.id,
@@ -2434,8 +2450,13 @@ async function reimbursementRoutes(fastify) {
}
const files = [];
let governanceFields = {};
try {
for await (const part of request.parts()) {
if (part.type === "field") {
governanceFields = parseMultipartGovernanceField(part, governanceFields);
continue;
}
if (part.type !== "file") {
continue;
}
@@ -2505,6 +2526,12 @@ async function reimbursementRoutes(fastify) {
const datePrefix = compactDateString(fastify.config.appTimezone);
const createdItems = [];
const governanceDefaults = {
governanceStatus: "approved",
sensitivityLevel: SENSITIVITY_LEVEL.internal,
attachmentType: ATTACHMENT_TYPE.supportingMaterial,
downloadPolicy: DOWNLOAD_POLICY.standard,
};
for (const file of files) {
const fileSha256 = sha256Hex(file.buffer);
const extension = path.extname(file.originalName ?? "").toLowerCase();
@@ -2525,6 +2552,7 @@ async function reimbursementRoutes(fastify) {
file_size: file.fileSize,
file_sha256: fileSha256,
uploaded_by: request.employee.id,
...governanceCreateData(governanceFields, governanceDefaults),
},
select: {
id: true,
@@ -2532,6 +2560,16 @@ async function reimbursementRoutes(fastify) {
mime_type: true,
file_size: true,
file_sha256: true,
governance_status: true,
sensitivity_level: true,
attachment_type: true,
reviewed_by: true,
reviewed_at: true,
review_note: true,
archived_by: true,
archived_at: true,
archive_note: true,
download_policy: true,
created_at: true,
uploader: {
select: {
@@ -2545,6 +2583,7 @@ async function reimbursementRoutes(fastify) {
createdItems.push(created);
}
const uploadedGovernance = governanceCreateData(governanceFields, governanceDefaults);
await fastify.prisma.auditLog.create({
data: buildAuditLogData(request, {
actorId: request.employee.id,
@@ -2553,6 +2592,8 @@ async function reimbursementRoutes(fastify) {
targetId: reimbursementId,
extra: {
attachmentIds: createdItems.map((item) => item.id),
attachmentType: uploadedGovernance.attachment_type,
sensitivityLevel: uploadedGovernance.sensitivity_level,
},
}),
});
@@ -2657,6 +2698,10 @@ async function reimbursementRoutes(fastify) {
storage_path: true,
original_name: true,
mime_type: true,
governance_status: true,
sensitivity_level: true,
attachment_type: true,
download_policy: true,
reimbursement_form: {
select: {
owner_id: true,
@@ -2674,6 +2719,15 @@ async function reimbursementRoutes(fastify) {
}
ensureOwnOrCanViewAll(fastify, request, attachment.reimbursement_form);
const downloadReason = sanitizeDownloadReason(request.query?.reason);
if (requiresDownloadReason(attachment) && !downloadReason) {
throw new AppError({
code: "VALID_001",
message: "参数校验失败",
statusCode: 400,
details: { reason: "required" },
});
}
let stream = null;
try {
@@ -2701,6 +2755,10 @@ async function reimbursementRoutes(fastify) {
targetId: reimbursementId,
extra: {
attachmentId: attachment.id,
attachmentType: attachment.attachment_type ?? ATTACHMENT_TYPE.supportingMaterial,
sensitivityLevel: attachment.sensitivity_level ?? SENSITIVITY_LEVEL.internal,
governanceStatus: attachment.governance_status ?? "approved",
reason: downloadReason,
},
}),
});