From f233a0d5bc440cfc4538b197a2449bd134c26feb Mon Sep 17 00:00:00 2001 From: shunli Date: Mon, 9 Feb 2026 19:35:51 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=8C=E6=88=90=20F17=20=E6=8A=A5?= =?UTF-8?q?=E8=A1=A8=E5=AF=BC=E5=87=BA=EF=BC=88CSV/XLSX=EF=BC=89\n\n-=20?= =?UTF-8?q?=E5=90=8E=E7=AB=AF=E6=96=B0=E5=A2=9E=20/api/exports/*=20?= =?UTF-8?q?=E5=AF=BC=E5=87=BA=E6=8E=A5=E5=8F=A3=EF=BC=88=E5=90=AB=E6=9D=83?= =?UTF-8?q?=E9=99=90=E4=B8=8E=E5=AE=A1=E8=AE=A1=E8=AE=B0=E5=BD=95=EF=BC=89?= =?UTF-8?q?\n-=20CSV=20=E9=87=87=E7=94=A8=E6=B5=81=E5=BC=8F=E7=94=9F?= =?UTF-8?q?=E6=88=90=EF=BC=8CXLSX=20=E4=BD=BF=E7=94=A8=20exceljs\n-=20?= =?UTF-8?q?=E5=89=8D=E7=AB=AF=E5=9C=A8=E6=8A=A5=E9=94=80/=E7=A5=A8?= =?UTF-8?q?=E6=8D=AE/=E5=AE=A1=E8=AE=A1=E9=A1=B5=E5=8A=A0=E5=85=A5?= =?UTF-8?q?=E5=AF=BC=E5=87=BA=E5=85=A5=E5=8F=A3=EF=BC=88=E4=B8=8B=E8=BD=BD?= =?UTF-8?q?=20Blob=EF=BC=89\n-=20=E6=9B=B4=E6=96=B0=20P5=20=E8=93=9D?= =?UTF-8?q?=E5=9B=BE=20v3=20=E4=B8=AD=20F17=20=E7=8A=B6=E6=80=81=E4=B8=BA?= =?UTF-8?q?=20DONE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/api/exports.ts | 45 + client/src/api/http.ts | 74 ++ client/src/lib/download.ts | 12 + client/src/pages/AuditLogsPage.tsx | 71 +- client/src/pages/InvoicePoolPage.tsx | 45 + client/src/pages/ReimbursementsPage.tsx | 52 + .../2026-02-09_13-39-02-p5-blueprint-v3.csv | 2 +- server/package-lock.json | 1023 +++++++++++++++++ server/package.json | 1 + server/src/app.js | 2 + server/src/routes/exports.js | 737 ++++++++++++ 11 files changed, 2062 insertions(+), 2 deletions(-) create mode 100644 client/src/api/exports.ts create mode 100644 client/src/lib/download.ts create mode 100644 server/src/routes/exports.js diff --git a/client/src/api/exports.ts b/client/src/api/exports.ts new file mode 100644 index 00000000..f52f42bd --- /dev/null +++ b/client/src/api/exports.ts @@ -0,0 +1,45 @@ +import { apiRequestBlob, type ApiBlobResponse } from "./http"; + +export async function exportReimbursements(params: { + format: "csv" | "xlsx"; + status?: string; + scope?: "own" | "all"; +}) { + const qs = new URLSearchParams(); + qs.set("format", params.format); + if (params.status) qs.set("status", params.status); + if (params.scope) qs.set("scope", params.scope); + + return apiRequestBlob(`/api/exports/reimbursements?${qs.toString()}`); +} + +export async function exportInvoicePool(params: { + format: "csv" | "xlsx"; + status?: string; + ownerScope?: "own" | "all"; +}) { + const qs = new URLSearchParams(); + qs.set("format", params.format); + if (params.status) qs.set("status", params.status); + if (params.ownerScope) qs.set("ownerScope", params.ownerScope); + + return apiRequestBlob(`/api/exports/invoice-pool?${qs.toString()}`); +} + +export async function exportAuditLogs(params: { + format: "csv" | "xlsx"; + action?: string; + targetType?: string; + actorId?: string; +}) { + const qs = new URLSearchParams(); + qs.set("format", params.format); + if (params.action) qs.set("action", params.action); + if (params.targetType) qs.set("targetType", params.targetType); + if (params.actorId) qs.set("actorId", params.actorId); + + return apiRequestBlob(`/api/exports/audit-logs?${qs.toString()}`); +} + +export type { ApiBlobResponse }; + diff --git a/client/src/api/http.ts b/client/src/api/http.ts index b5e22e26..df04e917 100644 --- a/client/src/api/http.ts +++ b/client/src/api/http.ts @@ -153,3 +153,77 @@ export async function apiRequestData( const response = await apiRequest(path, options); return response.data; } + +function parseFilenameFromContentDisposition(headerValue: string | null): string | null { + if (!headerValue) { + return null; + } + + const parts = headerValue.split(";").map((part) => part.trim()); + const filenamePart = parts.find((part) => part.toLowerCase().startsWith("filename=")); + if (!filenamePart) { + return null; + } + + const raw = filenamePart.slice("filename=".length).trim(); + return raw.replace(/^\"|\"$/g, "") || null; +} + +export type ApiBlobResponse = { + requestId: string; + blob: Blob; + filename: string | null; +}; + +export async function apiRequestBlob( + path: string, + options: ApiRequestOptions = {}, +): Promise { + const { + auth = true, + retryOn401 = true, + accessTokenOverride, + headers, + ...rest + } = options; + + const requestHeaders = new Headers(headers ?? {}); + const token = accessTokenOverride ?? (auth ? getAccessToken() : null); + + if (auth && token) { + requestHeaders.set("authorization", `Bearer ${token}`); + } + + const response = await fetch(path, { + ...rest, + credentials: "include", + headers: requestHeaders, + }); + + if (response.status === 401 && auth && retryOn401 && refreshAccessToken) { + const nextToken = await getRefreshedToken(); + if (nextToken) { + return apiRequestBlob(path, { + ...options, + retryOn401: false, + accessTokenOverride: nextToken, + }); + } + + if (clearSession) { + clearSession(); + } + } + + const payload = await safeParseJson(response); + if (!response.ok) { + throw buildApiError(response, payload); + } + + const blob = await response.blob(); + return { + requestId: response.headers.get("x-request-id") ?? "", + blob, + filename: parseFilenameFromContentDisposition(response.headers.get("content-disposition")), + }; +} diff --git a/client/src/lib/download.ts b/client/src/lib/download.ts new file mode 100644 index 00000000..972aef09 --- /dev/null +++ b/client/src/lib/download.ts @@ -0,0 +1,12 @@ +export function downloadBlob(blob: Blob, filename: string) { + const url = URL.createObjectURL(blob); + const link = document.createElement("a"); + link.href = url; + link.download = filename; + link.rel = "noreferrer"; + document.body.appendChild(link); + link.click(); + link.remove(); + setTimeout(() => URL.revokeObjectURL(url), 1000); +} + diff --git a/client/src/pages/AuditLogsPage.tsx b/client/src/pages/AuditLogsPage.tsx index 524dd3d4..81ad6927 100644 --- a/client/src/pages/AuditLogsPage.tsx +++ b/client/src/pages/AuditLogsPage.tsx @@ -1,7 +1,11 @@ import { useEffect, useMemo, useState } from "react"; import { isApiError } from "../api/types"; +import { exportAuditLogs } from "../api/exports"; import { fetchAuditLogs, type AuditLogListResponse } from "../api/audit"; import { PageHeader } from "../components/common/PageHeader"; +import { Button } from "../components/ui/button"; +import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "../components/ui/dropdown-menu"; +import { downloadBlob } from "../lib/download"; const DEFAULT_PAGE_SIZE = 20; @@ -10,6 +14,7 @@ export function AuditLogsPage() { const [actionFilter, setActionFilter] = useState(""); const [targetTypeFilter, setTargetTypeFilter] = useState(""); const [loading, setLoading] = useState(false); + const [exporting, setExporting] = useState(false); const [error, setError] = useState(""); const [data, setData] = useState(null); @@ -63,7 +68,71 @@ export function AuditLogsPage() { return (
- + + + + + + { + setExporting(true); + try { + const response = await exportAuditLogs({ + format: "csv", + action: actionFilter || undefined, + targetType: targetTypeFilter || undefined, + }); + downloadBlob(response.blob, response.filename ?? "audit-logs.csv"); + setError(""); + } catch (err) { + if (isApiError(err)) { + setError(`${err.code}: ${err.message}`); + } else { + setError("导出审计日志失败,请稍后重试"); + } + } finally { + setExporting(false); + } + }} + > + 导出 CSV + + { + setExporting(true); + try { + const response = await exportAuditLogs({ + format: "xlsx", + action: actionFilter || undefined, + targetType: targetTypeFilter || undefined, + }); + downloadBlob(response.blob, response.filename ?? "audit-logs.xlsx"); + setError(""); + } catch (err) { + if (isApiError(err)) { + setError(`${err.code}: ${err.message}`); + } else { + setError("导出审计日志失败,请稍后重试"); + } + } finally { + setExporting(false); + } + }} + > + 导出 Excel + + + + } + />
diff --git a/client/src/pages/InvoicePoolPage.tsx b/client/src/pages/InvoicePoolPage.tsx index 6089d193..bf9b706b 100644 --- a/client/src/pages/InvoicePoolPage.tsx +++ b/client/src/pages/InvoicePoolPage.tsx @@ -3,6 +3,7 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { isApiError } from "../api/types"; import { mapApiErrorMessage } from "../api/errorMessages"; import { fetchMyDelegations } from "../api/delegations"; +import { exportInvoicePool } from "../api/exports"; import { archiveInvoice, bindInvoice, @@ -19,9 +20,11 @@ import { import { fetchReimbursements } from "../api/reimbursements"; import { useAuth } from "../auth/AuthContext"; import { hasPermission } from "../auth/permissions"; +import { downloadBlob } from "../lib/download"; import { PageHeader } from "../components/common/PageHeader"; import { Button } from "../components/ui/button"; import { Card } from "../components/ui/card"; +import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "../components/ui/dropdown-menu"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "../components/ui/table"; const PAGE_SIZE = 20; @@ -267,6 +270,26 @@ export function InvoicePoolPage() { }, }); + const exportMutation = useMutation({ + mutationFn: (format: "csv" | "xlsx") => + exportInvoicePool({ + format, + status: status || undefined, + ownerScope, + }), + onSuccess: (response, format) => { + downloadBlob(response.blob, response.filename ?? `invoice-pool.${format}`); + setError(""); + }, + onError: (err) => { + if (isApiError(err)) { + setError(`${err.code}: ${mapApiErrorMessage(err.code, err.details, err.message)}`); + } else { + setError("导出失败,请稍后重试"); + } + }, + }); + const totalPages = Math.max(1, Math.ceil((listQuery.data?.total ?? 0) / PAGE_SIZE)); const detail = detailQuery.data; const canEditDetail = Boolean( @@ -281,6 +304,28 @@ export function InvoicePoolPage() { + + + + + exportMutation.mutate("csv")}> + 导出 CSV + + exportMutation.mutate("xlsx")}> + 导出 Excel + + + + } /> {error ? ( diff --git a/client/src/pages/ReimbursementsPage.tsx b/client/src/pages/ReimbursementsPage.tsx index c4039e25..93fa467e 100644 --- a/client/src/pages/ReimbursementsPage.tsx +++ b/client/src/pages/ReimbursementsPage.tsx @@ -4,12 +4,15 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { isApiError } from "../api/types"; import { mapApiErrorMessage } from "../api/errorMessages"; import { fetchMyDelegations } from "../api/delegations"; +import { exportReimbursements } from "../api/exports"; import { createReimbursement, fetchReimbursements, type ReimbursementStatus } from "../api/reimbursements"; import { useAuth } from "../auth/AuthContext"; import { hasPermission } from "../auth/permissions"; +import { downloadBlob } from "../lib/download"; import { PageHeader } from "../components/common/PageHeader"; import { Button } from "../components/ui/button"; import { Card } from "../components/ui/card"; +import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "../components/ui/dropdown-menu"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "../components/ui/table"; import { Tabs, TabsList, TabsTrigger } from "../components/ui/tabs"; @@ -19,6 +22,7 @@ export function ReimbursementsPage() { const [searchParams, setSearchParams] = useSearchParams(); const { permissions, me } = useAuth(); const queryClient = useQueryClient(); + const [exportError, setExportError] = useState(""); const status = (searchParams.get("status") as ReimbursementStatus | null) ?? "draft"; const page = Number(searchParams.get("page") ?? "1") || 1; @@ -51,6 +55,21 @@ export function ReimbursementsPage() { }, }); + const exportMutation = useMutation({ + mutationFn: (format: "csv" | "xlsx") => exportReimbursements({ format, status, scope }), + onSuccess: (response, format) => { + downloadBlob(response.blob, response.filename ?? `reimbursements.${format}`); + setExportError(""); + }, + onError: (err) => { + if (isApiError(err)) { + setExportError(`${err.code}: ${mapApiErrorMessage(err.code, err.details, err.message)}`); + return; + } + setExportError(err instanceof Error ? err.message : "导出失败"); + }, + }); + const totalPages = Math.max(1, Math.ceil((listQuery.data?.total ?? 0) / PAGE_SIZE)); return ( @@ -86,6 +105,33 @@ export function ReimbursementsPage() { > {createMutation.isPending ? "创建中..." : "新建报销"} + + + + + + + exportMutation.mutate("csv")} + > + 导出 CSV + + exportMutation.mutate("xlsx")} + > + 导出 Excel + + +
} /> @@ -101,6 +147,12 @@ export function ReimbursementsPage() {

) : null} + {exportError ? ( +

+ {exportError} +

+ ) : null} +
pg-boss),"将 OCR 异步处理迁移到 pg-boss:保持并发锁语义、幂等与回归脚本不变","mock/tencent 两种 provider 下 OCR 仍可跑;并发下单条仅处理一次;回归全绿","cd server && ./scripts/regression-ocr.sh && ./scripts/regression-all.sh",manual,TODO,TODO,TODO,"server/src/jobs/ocrJob.js | server/src/services/ocr/* | server/README.md","[""F19""]","迁移后移除/停用对应 node-cron tick(保留回滚路径)" diff --git a/server/package-lock.json b/server/package-lock.json index 3278a97c..165506b8 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -20,6 +20,7 @@ "@prisma/adapter-pg": "^7.3.0", "@prisma/client": "^7.3.0", "bcryptjs": "^3.0.3", + "exceljs": "^4.4.0", "fastify": "^5.7.4", "fastify-plugin": "^5.1.0", "node-cron": "^4.2.1", @@ -94,6 +95,35 @@ "@electric-sql/pglite": "0.3.15" } }, + "node_modules/@fast-csv/format": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/@fast-csv/format/-/format-4.3.5.tgz", + "integrity": "sha512-8iRn6QF3I8Ak78lNAa+Gdl5MJJBM5vRHivFtMRUWINdevNo00K7OXxS2PshawLKTejVwieIlPmK5YlLu6w4u8A==", + "license": "MIT", + "dependencies": { + "@types/node": "^14.0.1", + "lodash.escaperegexp": "^4.1.2", + "lodash.isboolean": "^3.0.3", + "lodash.isequal": "^4.5.0", + "lodash.isfunction": "^3.0.9", + "lodash.isnil": "^4.0.0" + } + }, + "node_modules/@fast-csv/parse": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/@fast-csv/parse/-/parse-4.3.6.tgz", + "integrity": "sha512-uRsLYksqpbDmWaSmzvJcuApSEe38+6NQZBUsuAyMZKqHxH0g1wcJgsKUvN3WC8tewaqFjBMMGrkHmC+T7k8LvA==", + "license": "MIT", + "dependencies": { + "@types/node": "^14.0.1", + "lodash.escaperegexp": "^4.1.2", + "lodash.groupby": "^4.6.0", + "lodash.isfunction": "^3.0.9", + "lodash.isnil": "^4.0.0", + "lodash.isundefined": "^3.0.1", + "lodash.uniq": "^4.5.0" + } + }, "node_modules/@fastify/accept-negotiator": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/@fastify/accept-negotiator/-/accept-negotiator-2.0.1.tgz", @@ -657,6 +687,12 @@ "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", "license": "MIT" }, + "node_modules/@types/node": { + "version": "14.18.63", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.63.tgz", + "integrity": "sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ==", + "license": "MIT" + }, "node_modules/@types/react": { "version": "19.2.13", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.13.tgz", @@ -718,6 +754,108 @@ } } }, + "node_modules/archiver": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/archiver/-/archiver-5.3.2.tgz", + "integrity": "sha512-+25nxyyznAXF7Nef3y0EbBeqmGZgeN/BxHX29Rs39djAfaFalmQ89SE6CWyDCHzGL0yt/ycBtNOmGTW0FyGWNw==", + "license": "MIT", + "dependencies": { + "archiver-utils": "^2.1.0", + "async": "^3.2.4", + "buffer-crc32": "^0.2.1", + "readable-stream": "^3.6.0", + "readdir-glob": "^1.1.2", + "tar-stream": "^2.2.0", + "zip-stream": "^4.1.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/archiver-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-2.1.0.tgz", + "integrity": "sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==", + "license": "MIT", + "dependencies": { + "glob": "^7.1.4", + "graceful-fs": "^4.2.0", + "lazystream": "^1.0.0", + "lodash.defaults": "^4.2.0", + "lodash.difference": "^4.5.0", + "lodash.flatten": "^4.4.0", + "lodash.isplainobject": "^4.0.6", + "lodash.union": "^4.6.0", + "normalize-path": "^3.0.0", + "readable-stream": "^2.0.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/archiver-utils/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/archiver-utils/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/archiver-utils/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/archiver-utils/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/archiver-utils/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/asn1.js": { "version": "5.4.1", "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz", @@ -730,6 +868,12 @@ "safer-buffer": "^2.1.0" } }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "license": "MIT" + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -764,6 +908,32 @@ "node": ">= 6.0.0" } }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/bcryptjs": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.3.tgz", @@ -773,6 +943,15 @@ "bcrypt": "bin/bcrypt" } }, + "node_modules/big-integer": { + "version": "1.6.52", + "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.52.tgz", + "integrity": "sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==", + "license": "Unlicense", + "engines": { + "node": ">=0.6" + } + }, "node_modules/bignumber.js": { "version": "9.3.1", "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", @@ -782,12 +961,102 @@ "node": "*" } }, + "node_modules/binary": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/binary/-/binary-0.3.0.tgz", + "integrity": "sha512-D4H1y5KYwpJgK8wk1Cue5LLPgmwHKYSChkbspQg5JtVuR5ulGckxfR62H3AE9UDkdMC8yyXlqYihuz3Aqg2XZg==", + "license": "MIT", + "dependencies": { + "buffers": "~0.1.1", + "chainsaw": "~0.1.0" + }, + "engines": { + "node": "*" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/bluebird": { + "version": "3.4.7", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.7.tgz", + "integrity": "sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA==", + "license": "MIT" + }, "node_modules/bn.js": { "version": "4.12.2", "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz", "integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==", "license": "MIT" }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/buffer-indexof-polyfill": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/buffer-indexof-polyfill/-/buffer-indexof-polyfill-1.0.2.tgz", + "integrity": "sha512-I7wzHwA3t1/lwXQh+A5PbNvJxgfo5r3xulgpYDB5zckTu/Z9oUK9biouBKQUjEqzaz3HnAT6TYoovmE+GqSf7A==", + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/buffers": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/buffers/-/buffers-0.1.1.tgz", + "integrity": "sha512-9q/rDEGSb/Qsvv2qvzIzdluL5k7AaJOTrw23z9reQthrbF7is4CtlT0DXyO1oei2DCp4uojjzQ7igaSHp1kAEQ==", + "engines": { + "node": ">=0.2.0" + } + }, "node_modules/c12": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/c12/-/c12-3.1.0.tgz", @@ -829,6 +1098,18 @@ "node": ">= 0.4" } }, + "node_modules/chainsaw": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/chainsaw/-/chainsaw-0.1.0.tgz", + "integrity": "sha512-75kWfWt6MEKNC8xYXIdRpDehRYY/tNSgwKaJq+dbbDcxORuVrrQ+SEHoWsniVn9XPYfP4gmdWIeDk/4YNp1rNQ==", + "license": "MIT/X11", + "dependencies": { + "traverse": ">=0.3.0 <0.4" + }, + "engines": { + "node": "*" + } + }, "node_modules/chevrotain": { "version": "10.5.0", "resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-10.5.0.tgz", @@ -879,6 +1160,27 @@ "node": ">= 0.8" } }, + "node_modules/compress-commons": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-4.1.2.tgz", + "integrity": "sha512-D3uMHtGc/fcO1Gt1/L7i1e33VOvD4A9hfQLP+6ewd+BvG/gQ84Yh4oftEhAdjSMgBgwGL+jsppT7JYNpo6MHHg==", + "license": "MIT", + "dependencies": { + "buffer-crc32": "^0.2.13", + "crc32-stream": "^4.0.2", + "normalize-path": "^3.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "license": "MIT" + }, "node_modules/confbox": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.2.tgz", @@ -920,6 +1222,37 @@ "url": "https://opencollective.com/express" } }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "license": "MIT" + }, + "node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "license": "Apache-2.0", + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/crc32-stream": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-4.0.3.tgz", + "integrity": "sha512-NT7w2JVU7DFroFdYkeq8cywxrgjPHWkdX1wjpRQXPX5Asews3tA+Ght6lddQO5Mkumffp3X7GEqku3epj2toIw==", + "license": "MIT", + "dependencies": { + "crc-32": "^1.2.0", + "readable-stream": "^3.4.0" + }, + "engines": { + "node": ">= 10" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -941,6 +1274,12 @@ "license": "MIT", "peer": true }, + "node_modules/dayjs": { + "version": "1.11.19", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.19.tgz", + "integrity": "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==", + "license": "MIT" + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -1041,6 +1380,45 @@ "node": ">= 0.4" } }, + "node_modules/duplexer2": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", + "integrity": "sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==", + "license": "BSD-3-Clause", + "dependencies": { + "readable-stream": "^2.0.2" + } + }, + "node_modules/duplexer2/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/duplexer2/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/duplexer2/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/ecdsa-sig-formatter": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", @@ -1069,6 +1447,15 @@ "node": ">=14" } }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, "node_modules/es-define-property": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", @@ -1120,6 +1507,35 @@ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", "license": "MIT" }, + "node_modules/exceljs": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/exceljs/-/exceljs-4.4.0.tgz", + "integrity": "sha512-XctvKaEMaj1Ii9oDOqbW/6e1gXknSY4g/aLCDicOXqBE4M0nRWkUu0PTp++UPNzoFY12BNHMfs/VadKIS6llvg==", + "license": "MIT", + "dependencies": { + "archiver": "^5.0.0", + "dayjs": "^1.8.34", + "fast-csv": "^4.3.1", + "jszip": "^3.10.1", + "readable-stream": "^3.6.0", + "saxes": "^5.0.1", + "tmp": "^0.2.0", + "unzipper": "^0.10.11", + "uuid": "^8.3.0" + }, + "engines": { + "node": ">=8.3.0" + } + }, + "node_modules/exceljs/node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/exsolve": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.8.tgz", @@ -1148,6 +1564,19 @@ "node": ">=8.0.0" } }, + "node_modules/fast-csv": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/fast-csv/-/fast-csv-4.3.6.tgz", + "integrity": "sha512-2RNSpuwwsJGP0frGsOmTb9oUF+VkFSM4SyLTDgwf2ciHWTarN0lQTC+F2f/t5J9QjW+c65VFIAAu85GsvMIusw==", + "license": "MIT", + "dependencies": { + "@fast-csv/format": "4.3.5", + "@fast-csv/parse": "4.3.6" + }, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/fast-decode-uri-component": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/fast-decode-uri-component/-/fast-decode-uri-component-1.0.1.tgz", @@ -1360,6 +1789,34 @@ "node": ">= 6" } }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "license": "MIT" + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "license": "ISC" + }, + "node_modules/fstream": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.12.tgz", + "integrity": "sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "dependencies": { + "graceful-fs": "^4.1.2", + "inherits": "~2.0.0", + "mkdirp": ">=0.5 0", + "rimraf": "2" + }, + "engines": { + "node": ">=0.6" + } + }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -1608,6 +2065,43 @@ "url": "https://opencollective.com/express" } }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", + "license": "MIT" + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", @@ -1650,6 +2144,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -1699,6 +2199,99 @@ "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", "license": "MIT" }, + "node_modules/jszip": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", + "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", + "license": "(MIT OR GPL-3.0-or-later)", + "dependencies": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" + } + }, + "node_modules/jszip/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/jszip/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/jszip/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/lazystream": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz", + "integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==", + "license": "MIT", + "dependencies": { + "readable-stream": "^2.0.5" + }, + "engines": { + "node": ">= 0.6.3" + } + }, + "node_modules/lazystream/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/lazystream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/lazystream/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "license": "MIT", + "dependencies": { + "immediate": "~3.0.5" + } + }, "node_modules/light-my-request": { "version": "6.6.0", "resolved": "https://registry.npmjs.org/light-my-request/-/light-my-request-6.6.0.tgz", @@ -1745,12 +2338,97 @@ "node": ">=10" } }, + "node_modules/listenercount": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/listenercount/-/listenercount-1.0.1.tgz", + "integrity": "sha512-3mk/Zag0+IJxeDrxSgaDPy4zZ3w05PRZeJNnlWhzFz5OkX49J4krc+A8X2d2M69vGMBEX0uyl8M+W+8gH+kBqQ==", + "license": "ISC" + }, "node_modules/lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "license": "MIT" }, + "node_modules/lodash.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==", + "license": "MIT" + }, + "node_modules/lodash.difference": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.difference/-/lodash.difference-4.5.0.tgz", + "integrity": "sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA==", + "license": "MIT" + }, + "node_modules/lodash.escaperegexp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz", + "integrity": "sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw==", + "license": "MIT" + }, + "node_modules/lodash.flatten": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", + "integrity": "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==", + "license": "MIT" + }, + "node_modules/lodash.groupby": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.groupby/-/lodash.groupby-4.6.0.tgz", + "integrity": "sha512-5dcWxm23+VAoz+awKmBaiBvzox8+RqMgFhi7UvX9DHZr2HdxHXM/Wrf8cfKpsW37RNrvtPn6hSwNqurSILbmJw==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", + "deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead.", + "license": "MIT" + }, + "node_modules/lodash.isfunction": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/lodash.isfunction/-/lodash.isfunction-3.0.9.tgz", + "integrity": "sha512-AirXNj15uRIMMPihnkInB4i3NHeb4iBtNg9WRWuK2o31S+ePwwNmDPaTL3o7dTJ+VXNZim7rFs4rxN4YU1oUJw==", + "license": "MIT" + }, + "node_modules/lodash.isnil": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/lodash.isnil/-/lodash.isnil-4.0.0.tgz", + "integrity": "sha512-up2Mzq3545mwVnMhTDMdfoG1OurpA/s5t88JmQX809eH3C8491iu2sfKhTfhQtKY78oPNhiaHJUpT/dUDAAtng==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isundefined": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/lodash.isundefined/-/lodash.isundefined-3.0.1.tgz", + "integrity": "sha512-MXB1is3s899/cD8jheYYE2V9qTHwKvt+npCwpD+1Sxm3Q3cECXCiYHjeHWXNwr6Q0SOBPrYUDxendrO6goVTEA==", + "license": "MIT" + }, + "node_modules/lodash.union": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz", + "integrity": "sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw==", + "license": "MIT" + }, + "node_modules/lodash.uniq": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", + "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==", + "license": "MIT" + }, "node_modules/long": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", @@ -1844,6 +2522,15 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/minipass": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", @@ -1853,6 +2540,18 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "license": "MIT", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, "node_modules/mnemonist": { "version": "0.40.3", "resolved": "https://registry.npmjs.org/mnemonist/-/mnemonist-0.40.3.tgz", @@ -1944,6 +2643,15 @@ "node": ">=6.0.0" } }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/nypm": { "version": "0.6.4", "resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.4.tgz", @@ -1988,6 +2696,30 @@ "node": ">=14.0.0" } }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "license": "(MIT AND Zlib)" + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -2256,6 +2988,12 @@ } } }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "license": "MIT" + }, "node_modules/process-warning": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.0.0.tgz", @@ -2344,6 +3082,50 @@ "react": "^19.2.4" } }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdir-glob": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.3.tgz", + "integrity": "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==", + "license": "Apache-2.0", + "dependencies": { + "minimatch": "^5.1.0" + } + }, + "node_modules/readdir-glob/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/readdir-glob/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/readdirp": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", @@ -2424,6 +3206,52 @@ "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", "license": "MIT" }, + "node_modules/rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/rimraf/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -2478,6 +3306,18 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "license": "MIT" }, + "node_modules/saxes": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-5.0.1.tgz", + "integrity": "sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw==", + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/scheduler": { "version": "0.27.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", @@ -2524,6 +3364,12 @@ "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", "license": "MIT" }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", + "license": "MIT" + }, "node_modules/setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", @@ -2618,6 +3464,31 @@ "reusify": "^1.0.0" } }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/tencentcloud-sdk-nodejs": { "version": "4.1.184", "resolved": "https://registry.npmjs.org/tencentcloud-sdk-nodejs/-/tencentcloud-sdk-nodejs-4.1.184.tgz", @@ -2659,6 +3530,15 @@ "node": ">=18" } }, + "node_modules/tmp": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz", + "integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==", + "license": "MIT", + "engines": { + "node": ">=14.14" + } + }, "node_modules/toad-cache": { "version": "3.7.0", "resolved": "https://registry.npmjs.org/toad-cache/-/toad-cache-3.7.0.tgz", @@ -2683,12 +3563,75 @@ "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", "license": "MIT" }, + "node_modules/traverse": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.3.9.tgz", + "integrity": "sha512-iawgk0hLP3SxGKDfnDJf8wTz4p2qImnyihM5Hh/sGvQ3K37dPi/w8sRhdNIxYA1TwFwc5mDhIJq+O0RsvXBKdQ==", + "license": "MIT/X11", + "engines": { + "node": "*" + } + }, "node_modules/tslib": { "version": "1.13.0", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.13.0.tgz", "integrity": "sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q==", "license": "0BSD" }, + "node_modules/unzipper": { + "version": "0.10.14", + "resolved": "https://registry.npmjs.org/unzipper/-/unzipper-0.10.14.tgz", + "integrity": "sha512-ti4wZj+0bQTiX2KmKWuwj7lhV+2n//uXEotUmGuQqrbVZSEGFMbI68+c6JCQ8aAmUWYvtHEz2A8K6wXvueR/6g==", + "license": "MIT", + "dependencies": { + "big-integer": "^1.6.17", + "binary": "~0.3.0", + "bluebird": "~3.4.1", + "buffer-indexof-polyfill": "~1.0.0", + "duplexer2": "~0.1.4", + "fstream": "^1.0.12", + "graceful-fs": "^4.2.2", + "listenercount": "~1.0.1", + "readable-stream": "~2.3.6", + "setimmediate": "~1.0.4" + } + }, + "node_modules/unzipper/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/unzipper/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/unzipper/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, "node_modules/uuid": { "version": "9.0.1", "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", @@ -2747,6 +3690,18 @@ "node": ">= 8" } }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "license": "MIT" + }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", @@ -2766,6 +3721,74 @@ "graphmatch": "^1.1.0" } }, + "node_modules/zip-stream": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-4.1.1.tgz", + "integrity": "sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ==", + "license": "MIT", + "dependencies": { + "archiver-utils": "^3.0.4", + "compress-commons": "^4.1.2", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/zip-stream/node_modules/archiver-utils": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-3.0.4.tgz", + "integrity": "sha512-KVgf4XQVrTjhyWmx6cte4RxonPLR9onExufI1jhvw/MQ4BB6IsZD5gT8Lq+u/+pRkWna/6JoHpiQioaqFP5Rzw==", + "license": "MIT", + "dependencies": { + "glob": "^7.2.3", + "graceful-fs": "^4.2.0", + "lazystream": "^1.0.0", + "lodash.defaults": "^4.2.0", + "lodash.difference": "^4.5.0", + "lodash.flatten": "^4.4.0", + "lodash.isplainobject": "^4.0.6", + "lodash.union": "^4.6.0", + "normalize-path": "^3.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/zip-stream/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/zip-stream/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/zod": { "version": "4.3.6", "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", diff --git a/server/package.json b/server/package.json index 7aa0dce7..caf9d870 100644 --- a/server/package.json +++ b/server/package.json @@ -35,6 +35,7 @@ "@prisma/adapter-pg": "^7.3.0", "@prisma/client": "^7.3.0", "bcryptjs": "^3.0.3", + "exceljs": "^4.4.0", "fastify": "^5.7.4", "fastify-plugin": "^5.1.0", "nodemailer": "^6.10.1", diff --git a/server/src/app.js b/server/src/app.js index 0c85b4fe..fb078849 100644 --- a/server/src/app.js +++ b/server/src/app.js @@ -28,6 +28,7 @@ const expenseTypesRoutes = require("./routes/expenseTypes"); const configReadRoutes = require("./routes/configRead"); const approvalRulesRoutes = require("./routes/approvalRules"); const notificationsRoutes = require("./routes/notifications"); +const exportsRoutes = require("./routes/exports"); const { startDelegationRevokeJob } = require("./jobs/delegationRevokeJob"); const { startOcrJob } = require("./jobs/ocrJob"); const { startInvoiceArchiveJob } = require("./jobs/invoiceArchiveJob"); @@ -204,6 +205,7 @@ async function buildApp() { await app.register(configReadRoutes, { prefix: "/api" }); await app.register(approvalRulesRoutes, { prefix: "/api" }); await app.register(notificationsRoutes, { prefix: "/api" }); + await app.register(exportsRoutes, { prefix: "/api" }); startDelegationRevokeJob(app); startOcrJob(app); diff --git a/server/src/routes/exports.js b/server/src/routes/exports.js new file mode 100644 index 00000000..3cf5bf56 --- /dev/null +++ b/server/src/routes/exports.js @@ -0,0 +1,737 @@ +const { Readable } = require("node:stream"); + +const ExcelJS = require("exceljs"); +const { z } = require("zod"); + +const { AppError } = require("../utils/AppError"); +const { buildAuditLogData } = require("../utils/audit"); + +const EXPORT_FORMAT_SCHEMA = z.enum(["csv", "xlsx"]); + +const REIMBURSEMENT_STATUS_SCHEMA = z.enum([ + "draft", + "reviewing", + "approved", + "paid", + "rejected", + "cancelled", +]); + +const REIMBURSEMENT_EXPORT_QUERY_SCHEMA = z.object({ + format: EXPORT_FORMAT_SCHEMA.optional(), + scope: z.enum(["own", "all"]).optional(), + status: REIMBURSEMENT_STATUS_SCHEMA.optional(), + limit: z.coerce.number().int().min(1).max(20000).optional(), +}); + +const INVOICE_EXPORT_QUERY_SCHEMA = z.object({ + format: EXPORT_FORMAT_SCHEMA.optional(), + ownerScope: z.enum(["own", "all"]).optional(), + status: z.enum(["pending_ocr", "ocr_failed", "available", "bound", "archived"]).optional(), + limit: z.coerce.number().int().min(1).max(20000).optional(), +}); + +const AUDIT_EXPORT_QUERY_SCHEMA = z.object({ + format: EXPORT_FORMAT_SCHEMA.optional(), + actorId: z.string().uuid().optional(), + action: z.string().min(1).max(100).optional(), + targetType: z.string().min(1).max(100).optional(), + dateFrom: z.string().datetime().optional(), + dateTo: z.string().datetime().optional(), + limit: z.coerce.number().int().min(1).max(50000).optional(), +}); + +function csvCell(value) { + if (value == null) return ""; + + const text = String(value); + if (/[",\n\r]/.test(text)) { + return `"${text.replace(/"/g, '""')}"`; + } + return text; +} + +function csvRow(values) { + return `${values.map(csvCell).join(",")}\n`; +} + +function parseQuery(schema, query) { + const parsed = schema.safeParse(query ?? {}); + if (!parsed.success) { + throw new AppError({ + code: "VALID_001", + message: "参数校验失败", + statusCode: 400, + details: parsed.error.flatten(), + }); + } + + return parsed.data; +} + +function resolveExportFormat(query) { + return query.format ?? "csv"; +} + +function ensureHasAnyPermission(fastify, request, permissionCodes) { + for (const code of permissionCodes) { + if (fastify.hasPermission(request.employee, code)) { + return; + } + } + + throw new AppError({ + code: "PERM_001", + message: "无权限", + statusCode: 403, + }); +} + +function buildOwnReimbursementScopeWhere(employeeId) { + return { + OR: [{ owner_id: employeeId }, { submitter_id: employeeId }], + }; +} + +function isGlobalViewRole(roleCodes) { + return roleCodes.some((code) => + ["admin", "ceo", "finance", "finance_mgr", "hr"].includes(code), + ); +} + +function buildViewAllScopeWhere(request) { + if (isGlobalViewRole(request.employee.roleCodes ?? [])) { + return {}; + } + + return { + OR: [ + { owner_id: request.employee.id }, + { submitter_id: request.employee.id }, + { owner: { direct_manager_id: request.employee.id } }, + ], + }; +} + +function exportFilename(prefix, format) { + const now = new Date(); + const stamp = now.toISOString().slice(0, 10); + return `${prefix}-${stamp}.${format}`; +} + +function setExportHeaders(reply, format, filename) { + if (format === "csv") { + reply.header("content-type", "text/csv; charset=utf-8"); + } else { + reply.header( + "content-type", + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + ); + } + + reply.header("content-disposition", `attachment; filename=\"${filename}\"`); +} + +async function* streamReimbursementsCsv(prisma, where, limit) { + yield "\ufeff"; + yield csvRow([ + "formNumber", + "status", + "totalAmount", + "ownerName", + "submitterName", + "reimbursementDate", + "paymentVoucherNo", + "paymentDate", + "paymentChannel", + "paidAt", + "createdAt", + "id", + ]); + + const batchSize = 500; + let remaining = limit; + let cursorCreatedAt = null; + let cursorId = null; + + while (remaining > 0) { + const take = Math.min(batchSize, remaining); + const rows = await prisma.reimbursementForm.findMany({ + where: { + ...where, + ...(cursorCreatedAt + ? { + OR: [ + { created_at: { lt: cursorCreatedAt } }, + { created_at: cursorCreatedAt, id: { lt: cursorId } }, + ], + } + : {}), + }, + orderBy: [{ created_at: "desc" }, { id: "desc" }], + take, + select: { + id: true, + form_number: true, + status: true, + total_amount: true, + reimbursement_date: true, + payment_voucher_no: true, + payment_date: true, + payment_channel: true, + paid_at: true, + created_at: true, + owner: { select: { name: true } }, + submitter: { select: { name: true } }, + }, + }); + + if (!rows.length) { + break; + } + + for (const item of rows) { + yield csvRow([ + item.form_number ?? "", + item.status, + item.total_amount?.toString?.() ?? String(item.total_amount ?? ""), + item.owner?.name ?? "", + item.submitter?.name ?? "", + item.reimbursement_date?.toISOString?.()?.slice(0, 10) ?? "", + item.payment_voucher_no ?? "", + item.payment_date?.toISOString?.()?.slice(0, 10) ?? "", + item.payment_channel ?? "", + item.paid_at?.toISOString?.() ?? "", + item.created_at?.toISOString?.() ?? "", + item.id, + ]); + remaining -= 1; + if (remaining <= 0) break; + } + + const last = rows[rows.length - 1]; + cursorCreatedAt = last.created_at; + cursorId = last.id; + } +} + +async function buildReimbursementsXlsx(prisma, where, limit) { + const workbook = new ExcelJS.Workbook(); + const sheet = workbook.addWorksheet("reimbursements"); + + sheet.columns = [ + { header: "单号", key: "formNumber", width: 18 }, + { header: "状态", key: "status", width: 12 }, + { header: "金额", key: "totalAmount", width: 12 }, + { header: "报销人", key: "ownerName", width: 14 }, + { header: "提交人", key: "submitterName", width: 14 }, + { header: "报销日期", key: "reimbursementDate", width: 14 }, + { header: "付款凭证号", key: "paymentVoucherNo", width: 18 }, + { header: "付款日期", key: "paymentDate", width: 14 }, + { header: "付款渠道", key: "paymentChannel", width: 12 }, + { header: "付款时间", key: "paidAt", width: 20 }, + { header: "创建时间", key: "createdAt", width: 20 }, + { header: "ID", key: "id", width: 36 }, + ]; + + const rows = await prisma.reimbursementForm.findMany({ + where, + orderBy: [{ created_at: "desc" }, { id: "desc" }], + take: limit, + select: { + id: true, + form_number: true, + status: true, + total_amount: true, + reimbursement_date: true, + payment_voucher_no: true, + payment_date: true, + payment_channel: true, + paid_at: true, + created_at: true, + owner: { select: { name: true } }, + submitter: { select: { name: true } }, + }, + }); + + for (const item of rows) { + sheet.addRow({ + formNumber: item.form_number ?? "", + status: item.status, + totalAmount: item.total_amount?.toString?.() ?? String(item.total_amount ?? ""), + ownerName: item.owner?.name ?? "", + submitterName: item.submitter?.name ?? "", + reimbursementDate: item.reimbursement_date?.toISOString?.()?.slice(0, 10) ?? "", + paymentVoucherNo: item.payment_voucher_no ?? "", + paymentDate: item.payment_date?.toISOString?.()?.slice(0, 10) ?? "", + paymentChannel: item.payment_channel ?? "", + paidAt: item.paid_at?.toISOString?.() ?? "", + createdAt: item.created_at?.toISOString?.() ?? "", + id: item.id, + }); + } + + return workbook.xlsx.writeBuffer(); +} + +async function* streamInvoicesCsv(prisma, where, limit) { + yield "\ufeff"; + yield csvRow([ + "status", + "ownerName", + "uploadedByName", + "invoiceCode", + "invoiceNumber", + "invoiceDate", + "totalAmount", + "archivedReason", + "linkedReimbursementId", + "ocrLastError", + "createdAt", + "id", + ]); + + const batchSize = 500; + let remaining = limit; + let cursorCreatedAt = null; + let cursorId = null; + + while (remaining > 0) { + const take = Math.min(batchSize, remaining); + const rows = await prisma.invoicePool.findMany({ + where: { + ...where, + ...(cursorCreatedAt + ? { + OR: [ + { created_at: { lt: cursorCreatedAt } }, + { created_at: cursorCreatedAt, id: { lt: cursorId } }, + ], + } + : {}), + }, + orderBy: [{ created_at: "desc" }, { id: "desc" }], + take, + select: { + id: true, + status: true, + archived_reason: true, + owner_employee: { select: { name: true } }, + uploaded_by_employee: { select: { name: true } }, + invoice_code: true, + invoice_number: true, + invoice_date: true, + total_amount: true, + linked_reimbursement_id: true, + ocr_last_error: true, + created_at: true, + }, + }); + + if (!rows.length) { + break; + } + + for (const item of rows) { + yield csvRow([ + item.status, + item.owner_employee?.name ?? "", + item.uploaded_by_employee?.name ?? "", + item.invoice_code ?? "", + item.invoice_number ?? "", + item.invoice_date?.toISOString?.()?.slice(0, 10) ?? "", + item.total_amount?.toString?.() ?? String(item.total_amount ?? ""), + item.archived_reason ?? "", + item.linked_reimbursement_id ?? "", + item.ocr_last_error ?? "", + item.created_at?.toISOString?.() ?? "", + item.id, + ]); + remaining -= 1; + if (remaining <= 0) break; + } + + const last = rows[rows.length - 1]; + cursorCreatedAt = last.created_at; + cursorId = last.id; + } +} + +async function buildInvoicesXlsx(prisma, where, limit) { + const workbook = new ExcelJS.Workbook(); + const sheet = workbook.addWorksheet("invoice_pool"); + + sheet.columns = [ + { header: "状态", key: "status", width: 12 }, + { header: "归属人", key: "ownerName", width: 14 }, + { header: "上传人", key: "uploadedByName", width: 14 }, + { header: "发票代码", key: "invoiceCode", width: 14 }, + { header: "发票号码", key: "invoiceNumber", width: 14 }, + { header: "开票日期", key: "invoiceDate", width: 14 }, + { header: "总金额", key: "totalAmount", width: 12 }, + { header: "归档原因", key: "archivedReason", width: 14 }, + { header: "关联报销单", key: "linkedReimbursementId", width: 36 }, + { header: "OCR错误", key: "ocrLastError", width: 18 }, + { header: "创建时间", key: "createdAt", width: 20 }, + { header: "ID", key: "id", width: 36 }, + ]; + + const rows = await prisma.invoicePool.findMany({ + where, + orderBy: [{ created_at: "desc" }, { id: "desc" }], + take: limit, + select: { + id: true, + status: true, + archived_reason: true, + owner_employee: { select: { name: true } }, + uploaded_by_employee: { select: { name: true } }, + invoice_code: true, + invoice_number: true, + invoice_date: true, + total_amount: true, + linked_reimbursement_id: true, + ocr_last_error: true, + created_at: true, + }, + }); + + for (const item of rows) { + sheet.addRow({ + status: item.status, + ownerName: item.owner_employee?.name ?? "", + uploadedByName: item.uploaded_by_employee?.name ?? "", + invoiceCode: item.invoice_code ?? "", + invoiceNumber: item.invoice_number ?? "", + invoiceDate: item.invoice_date?.toISOString?.()?.slice(0, 10) ?? "", + totalAmount: item.total_amount?.toString?.() ?? String(item.total_amount ?? ""), + archivedReason: item.archived_reason ?? "", + linkedReimbursementId: item.linked_reimbursement_id ?? "", + ocrLastError: item.ocr_last_error ?? "", + createdAt: item.created_at?.toISOString?.() ?? "", + id: item.id, + }); + } + + return workbook.xlsx.writeBuffer(); +} + +async function* streamAuditLogsCsv(prisma, where, limit) { + yield "\ufeff"; + yield csvRow([ + "createdAt", + "action", + "actorName", + "actorEmail", + "targetType", + "targetId", + "requestId", + "ipAddress", + ]); + + const batchSize = 1000; + let remaining = limit; + let cursorCreatedAt = null; + let cursorId = null; + + while (remaining > 0) { + const take = Math.min(batchSize, remaining); + const rows = await prisma.auditLog.findMany({ + where: { + ...where, + ...(cursorCreatedAt + ? { + OR: [ + { created_at: { lt: cursorCreatedAt } }, + { created_at: cursorCreatedAt, id: { lt: cursorId } }, + ], + } + : {}), + }, + orderBy: [{ created_at: "desc" }, { id: "desc" }], + take, + select: { + id: true, + request_id: true, + action: true, + target_type: true, + target_id: true, + ip_address: true, + created_at: true, + actor: { select: { name: true, email: true } }, + }, + }); + + if (!rows.length) { + break; + } + + for (const item of rows) { + yield csvRow([ + item.created_at?.toISOString?.() ?? "", + item.action, + item.actor?.name ?? "", + item.actor?.email ?? "", + item.target_type, + item.target_id ?? "", + item.request_id ?? "", + item.ip_address ?? "", + ]); + remaining -= 1; + if (remaining <= 0) break; + } + + const last = rows[rows.length - 1]; + cursorCreatedAt = last.created_at; + cursorId = last.id; + } +} + +async function buildAuditLogsXlsx(prisma, where, limit) { + const workbook = new ExcelJS.Workbook(); + const sheet = workbook.addWorksheet("audit_logs"); + + sheet.columns = [ + { header: "时间", key: "createdAt", width: 20 }, + { header: "Action", key: "action", width: 28 }, + { header: "Actor", key: "actorName", width: 14 }, + { header: "Actor Email", key: "actorEmail", width: 22 }, + { header: "Target Type", key: "targetType", width: 16 }, + { header: "Target ID", key: "targetId", width: 36 }, + { header: "Request ID", key: "requestId", width: 24 }, + { header: "IP", key: "ipAddress", width: 16 }, + ]; + + const rows = await prisma.auditLog.findMany({ + where, + orderBy: [{ created_at: "desc" }, { id: "desc" }], + take: limit, + select: { + id: true, + request_id: true, + action: true, + target_type: true, + target_id: true, + ip_address: true, + created_at: true, + actor: { select: { name: true, email: true } }, + }, + }); + + for (const item of rows) { + sheet.addRow({ + createdAt: item.created_at?.toISOString?.() ?? "", + action: item.action, + actorName: item.actor?.name ?? "", + actorEmail: item.actor?.email ?? "", + targetType: item.target_type, + targetId: item.target_id ?? "", + requestId: item.request_id ?? "", + ipAddress: item.ip_address ?? "", + }); + } + + return workbook.xlsx.writeBuffer(); +} + +async function exportsRoutes(fastify) { + fastify.get( + "/exports/reimbursements", + { + preHandler: fastify.authenticate, + }, + async (request, reply) => { + ensureHasAnyPermission(fastify, request, ["expense.view_own", "expense.view_all"]); + const query = parseQuery(REIMBURSEMENT_EXPORT_QUERY_SCHEMA, request.query); + + const canViewAll = fastify.hasPermission(request.employee, "expense.view_all"); + const scope = query.scope ?? (canViewAll ? "all" : "own"); + if (scope === "all" && !canViewAll) { + throw new AppError({ + code: "PERM_001", + message: "无权限", + statusCode: 403, + }); + } + + const where = { + ...(query.status ? { status: query.status } : {}), + ...(scope === "all" + ? buildViewAllScopeWhere(request) + : buildOwnReimbursementScopeWhere(request.employee.id)), + }; + + const format = resolveExportFormat(query); + const limit = query.limit ?? 5000; + const total = await fastify.prisma.reimbursementForm.count({ where }); + + await fastify.prisma.auditLog.create({ + data: buildAuditLogData(request, { + actorId: request.employee.id, + action: "EXPORT_REIMBURSEMENTS", + targetType: "export", + extra: { + format, + scope, + status: query.status ?? null, + limit, + matchedTotal: total, + }, + }), + }); + + const filename = exportFilename("reimbursements", format); + setExportHeaders(reply, format, filename); + + if (format === "xlsx") { + const buffer = await buildReimbursementsXlsx(fastify.prisma, where, limit); + return reply.send(Buffer.from(buffer)); + } + + const stream = Readable.from(streamReimbursementsCsv(fastify.prisma, where, limit)); + return reply.send(stream); + }, + ); + + fastify.get( + "/exports/invoice-pool", + { + preHandler: fastify.authenticate, + }, + async (request, reply) => { + ensureHasAnyPermission(fastify, request, ["invoice.view_own", "invoice.manage"]); + const query = parseQuery(INVOICE_EXPORT_QUERY_SCHEMA, request.query); + + const canManage = fastify.hasPermission(request.employee, "invoice.manage"); + const ownerScope = query.ownerScope ?? (canManage ? "all" : "own"); + if (ownerScope === "all" && !canManage) { + throw new AppError({ + code: "PERM_001", + message: "无权限", + statusCode: 403, + }); + } + + const where = { + ...(query.status ? { status: query.status } : {}), + ...(ownerScope === "all" + ? {} + : { + OR: [ + { owner_employee_id: request.employee.id }, + { uploaded_by_employee_id: request.employee.id }, + { + linked_reimbursement: { + OR: [ + { owner_id: request.employee.id }, + { submitter_id: request.employee.id }, + ], + }, + }, + ], + }), + }; + + const format = resolveExportFormat(query); + const limit = query.limit ?? 5000; + const total = await fastify.prisma.invoicePool.count({ where }); + + await fastify.prisma.auditLog.create({ + data: buildAuditLogData(request, { + actorId: request.employee.id, + action: "EXPORT_INVOICE_POOL", + targetType: "export", + extra: { + format, + ownerScope, + status: query.status ?? null, + limit, + matchedTotal: total, + }, + }), + }); + + const filename = exportFilename("invoice-pool", format); + setExportHeaders(reply, format, filename); + + if (format === "xlsx") { + const buffer = await buildInvoicesXlsx(fastify.prisma, where, limit); + return reply.send(Buffer.from(buffer)); + } + + const stream = Readable.from(streamInvoicesCsv(fastify.prisma, where, limit)); + return reply.send(stream); + }, + ); + + fastify.get( + "/exports/audit-logs", + { + preHandler: fastify.requirePermission("system.audit.view"), + }, + async (request, reply) => { + const query = parseQuery(AUDIT_EXPORT_QUERY_SCHEMA, request.query); + + const dateFrom = query.dateFrom ? new Date(query.dateFrom) : null; + const dateTo = query.dateTo ? new Date(query.dateTo) : null; + if (dateFrom && dateTo && dateFrom > dateTo) { + throw new AppError({ + code: "VALID_001", + message: "参数校验失败", + statusCode: 400, + details: { dateTo: "must_be_after_dateFrom" }, + }); + } + + const where = { + ...(query.actorId ? { actor_id: query.actorId } : {}), + ...(query.action ? { action: query.action } : {}), + ...(query.targetType ? { target_type: query.targetType } : {}), + ...(dateFrom || dateTo + ? { + created_at: { + ...(dateFrom ? { gte: dateFrom } : {}), + ...(dateTo ? { lte: dateTo } : {}), + }, + } + : {}), + }; + + const format = resolveExportFormat(query); + const limit = query.limit ?? 20000; + const total = await fastify.prisma.auditLog.count({ where }); + + await fastify.prisma.auditLog.create({ + data: buildAuditLogData(request, { + actorId: request.employee.id, + action: "EXPORT_AUDIT_LOGS", + targetType: "export", + extra: { + format, + actorId: query.actorId ?? null, + actionFilter: query.action ?? null, + targetType: query.targetType ?? null, + dateFrom: dateFrom?.toISOString?.() ?? null, + dateTo: dateTo?.toISOString?.() ?? null, + limit, + matchedTotal: total, + }, + }), + }); + + const filename = exportFilename("audit-logs", format); + setExportHeaders(reply, format, filename); + + if (format === "xlsx") { + const buffer = await buildAuditLogsXlsx(fastify.prisma, where, limit); + return reply.send(Buffer.from(buffer)); + } + + const stream = Readable.from(streamAuditLogsCsv(fastify.prisma, where, limit)); + return reply.send(stream); + }, + ); +} + +module.exports = exportsRoutes; +