实现企微原生材料预览(07-27-yang-jun-reimbursement-readiness)
这个提交包含在:
@@ -105,6 +105,19 @@ function resolveInvoiceStatusReason(invoice) {
|
||||
return resolveInvoiceAvailability(invoice);
|
||||
}
|
||||
|
||||
function resolveSignedFileOrigin(config, request) {
|
||||
const configured = String(config.appOrigin ?? "")
|
||||
.split(",")
|
||||
.map((item) => item.trim())
|
||||
.find(Boolean);
|
||||
return configured || `${request.protocol}://${request.headers.host ?? "localhost:3000"}`;
|
||||
}
|
||||
|
||||
function resolvePreviewUrlTtlMs(config) {
|
||||
const configuredSeconds = Math.max(30, Number(config.signedUrlTtlSeconds ?? 120));
|
||||
return Math.min(configuredSeconds, 120) * 1000;
|
||||
}
|
||||
|
||||
function ensureInvoiceReadableByUser(request, invoice) {
|
||||
const roleCodes = request.employee.roleCodes;
|
||||
const permissionCodes = request.employee.permissionCodes ?? [];
|
||||
@@ -179,6 +192,9 @@ function registerInvoicePoolReadRoutes(fastify) {
|
||||
owner_employee_id: true,
|
||||
uploaded_by_employee_id: true,
|
||||
linked_reimbursement_id: true,
|
||||
original_name: true,
|
||||
mime_type: true,
|
||||
file_size: true,
|
||||
linked_item_id: true,
|
||||
travel_order_id: true,
|
||||
invoice_code: true,
|
||||
@@ -467,6 +483,9 @@ function registerInvoicePoolReadRoutes(fastify) {
|
||||
owner_employee_id: true,
|
||||
uploaded_by_employee_id: true,
|
||||
linked_reimbursement_id: true,
|
||||
original_name: true,
|
||||
mime_type: true,
|
||||
file_size: true,
|
||||
linked_reimbursement: {
|
||||
select: {
|
||||
owner_id: true,
|
||||
@@ -497,7 +516,7 @@ function registerInvoicePoolReadRoutes(fastify) {
|
||||
});
|
||||
}
|
||||
|
||||
const expiresAt = Date.now() + fastify.config.signedUrlTtlSeconds * 1000;
|
||||
const expiresAt = Date.now() + resolvePreviewUrlTtlMs(fastify.config);
|
||||
const token = signPayload(
|
||||
{
|
||||
invoiceId: invoice.id,
|
||||
@@ -507,9 +526,7 @@ function registerInvoicePoolReadRoutes(fastify) {
|
||||
fastify.config.jwtSecret,
|
||||
);
|
||||
|
||||
const origin =
|
||||
fastify.config.appOrigin ||
|
||||
`${request.protocol}://${request.headers.host ?? "localhost:3000"}`;
|
||||
const origin = resolveSignedFileOrigin(fastify.config, request);
|
||||
const signedUrl = `${origin}/api/invoice-pool/${invoice.id}/file?token=${encodeURIComponent(token)}`;
|
||||
|
||||
await fastify.prisma.auditLog.create({
|
||||
@@ -528,6 +545,9 @@ function registerInvoicePoolReadRoutes(fastify) {
|
||||
return sendData(reply, request, {
|
||||
signedUrl,
|
||||
expiresAt: new Date(expiresAt).toISOString(),
|
||||
originalName: invoice.original_name,
|
||||
mimeType: invoice.mime_type,
|
||||
fileSize: invoice.file_size,
|
||||
});
|
||||
},
|
||||
);
|
||||
@@ -610,24 +630,22 @@ function registerInvoicePoolReadRoutes(fastify) {
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
await fastify.prisma.auditLog.create({
|
||||
data: buildAuditLogData(request, {
|
||||
actorId,
|
||||
action: "INVOICE_FILE_DOWNLOADED",
|
||||
targetType: "invoice_pool",
|
||||
targetId: parsedInvoiceId.data,
|
||||
extra: {
|
||||
invoiceId: parsedInvoiceId.data,
|
||||
exp: payload.exp,
|
||||
},
|
||||
}),
|
||||
});
|
||||
} catch (error) {
|
||||
fastify.log.error({ err: error, invoiceId: parsedInvoiceId.data }, "Audit log write failed");
|
||||
}
|
||||
await fastify.prisma.auditLog.create({
|
||||
data: buildAuditLogData(request, {
|
||||
actorId,
|
||||
action: "INVOICE_FILE_DOWNLOADED",
|
||||
targetType: "invoice_pool",
|
||||
targetId: parsedInvoiceId.data,
|
||||
extra: {
|
||||
invoiceId: parsedInvoiceId.data,
|
||||
exp: payload.exp,
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
reply.type(invoice.mime_type || "application/octet-stream");
|
||||
reply.header("cache-control", "private, no-store");
|
||||
reply.header("referrer-policy", "no-referrer");
|
||||
reply.header(
|
||||
"content-disposition",
|
||||
buildAttachmentContentDisposition(invoice.original_name, { disposition: "inline" }),
|
||||
|
||||
在新工单中引用
屏蔽一个用户