实现企微原生材料预览(07-27-yang-jun-reimbursement-readiness)
ci-e2e / e2e (push) Canceled after 0s
ci-minimal / check-minimal (push) Canceled after 0s

这个提交包含在:
2026-07-31 18:57:34 +08:00
父节点 e123d50f86
当前提交 410d8cd71b
共修改 23 个文件,包含 1352 行新增和 69 行删除
+38 -20
查看文件
@@ -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" }),