实现业务附件受控入库 V1(05-28-business-attachment-governance-v1)
这个提交包含在:
@@ -16,6 +16,16 @@ const { flushNotificationDispatchTasks } = require("../services/notificationDisp
|
||||
const { enqueueReimbursementJournalExport } = require("../services/integrations/erpJournal");
|
||||
const { ensureRiskFlag, buildPersistedRiskFlag, toMoneyNumber, toMoneyString } = require("../services/feeControl");
|
||||
const { openReadStream, StorageNotFoundError, writeBuffer } = require("../services/storage");
|
||||
const {
|
||||
ATTACHMENT_TYPE,
|
||||
DOWNLOAD_POLICY,
|
||||
SENSITIVITY_LEVEL,
|
||||
governanceCreateData,
|
||||
mapGovernanceFields,
|
||||
parseMultipartGovernanceField,
|
||||
requiresDownloadReason,
|
||||
sanitizeDownloadReason,
|
||||
} = require("../services/businessAttachmentGovernance");
|
||||
|
||||
const UUID_SCHEMA = z.string().uuid();
|
||||
const ENTITY_TYPE_SCHEMA = z.enum(["reimbursement", "travel_order"]);
|
||||
@@ -99,6 +109,12 @@ function mapReceiptAttachment(item) {
|
||||
mimeType: item.mime_type,
|
||||
fileSize: item.file_size,
|
||||
fileSha256: item.file_sha256,
|
||||
...mapGovernanceFields(item, {
|
||||
governanceStatus: "approved",
|
||||
sensitivityLevel: SENSITIVITY_LEVEL.confidential,
|
||||
attachmentType: ATTACHMENT_TYPE.receipt,
|
||||
downloadPolicy: DOWNLOAD_POLICY.reasonRequired,
|
||||
}),
|
||||
uploadedBy: item.uploader
|
||||
? {
|
||||
id: item.uploader.id,
|
||||
@@ -681,8 +697,13 @@ function buildNotFoundError() {
|
||||
|
||||
async function readReceiptUploadFiles(fastify, request) {
|
||||
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;
|
||||
}
|
||||
@@ -725,7 +746,7 @@ async function readReceiptUploadFiles(fastify, request) {
|
||||
throw buildValidationError({ file: "missing" });
|
||||
}
|
||||
|
||||
return files;
|
||||
return files.map((file) => ({ ...file, governanceFields }));
|
||||
}
|
||||
|
||||
async function paymentBatchesRoutes(fastify) {
|
||||
@@ -841,6 +862,16 @@ async function paymentBatchesRoutes(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: {
|
||||
@@ -888,6 +919,12 @@ async function paymentBatchesRoutes(fastify) {
|
||||
file_size: file.fileSize,
|
||||
file_sha256: fileSha256,
|
||||
uploaded_by: request.employee.id,
|
||||
...governanceCreateData(file.governanceFields, {
|
||||
governanceStatus: "approved",
|
||||
sensitivityLevel: SENSITIVITY_LEVEL.confidential,
|
||||
attachmentType: ATTACHMENT_TYPE.receipt,
|
||||
downloadPolicy: DOWNLOAD_POLICY.reasonRequired,
|
||||
}),
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
@@ -895,6 +932,16 @@ async function paymentBatchesRoutes(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: {
|
||||
@@ -917,6 +964,7 @@ async function paymentBatchesRoutes(fastify) {
|
||||
extra: {
|
||||
attachmentIds: createdItems.map((item) => item.id),
|
||||
attachmentCount: createdItems.length,
|
||||
attachmentTypes: [...new Set(createdItems.map((item) => item.attachment_type))],
|
||||
},
|
||||
}),
|
||||
});
|
||||
@@ -940,11 +988,19 @@ async function paymentBatchesRoutes(fastify) {
|
||||
storage_path: true,
|
||||
original_name: true,
|
||||
mime_type: true,
|
||||
governance_status: true,
|
||||
sensitivity_level: true,
|
||||
attachment_type: true,
|
||||
download_policy: true,
|
||||
},
|
||||
});
|
||||
if (!attachment) {
|
||||
throw buildNotFoundError();
|
||||
}
|
||||
const downloadReason = sanitizeDownloadReason(request.query?.reason);
|
||||
if (requiresDownloadReason(attachment) && !downloadReason) {
|
||||
throw buildValidationError({ reason: "required" });
|
||||
}
|
||||
|
||||
let stream = null;
|
||||
try {
|
||||
@@ -966,7 +1022,13 @@ async function paymentBatchesRoutes(fastify) {
|
||||
action: "PAYMENT_BATCH_RECEIPT_DOWNLOADED",
|
||||
targetType: "payment_batch",
|
||||
targetId: batchId,
|
||||
extra: { attachmentId: attachment.id },
|
||||
extra: {
|
||||
attachmentId: attachment.id,
|
||||
attachmentType: attachment.attachment_type ?? ATTACHMENT_TYPE.receipt,
|
||||
sensitivityLevel: attachment.sensitivity_level ?? SENSITIVITY_LEVEL.confidential,
|
||||
governanceStatus: attachment.governance_status ?? "approved",
|
||||
reason: downloadReason,
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
|
||||
在新工单中引用
屏蔽一个用户