From 139e37651bd1916335321d731c2490248f397508 Mon Sep 17 00:00:00 2001 From: jacy-dukang Date: Wed, 1 Jul 2026 21:46:36 +0800 Subject: [PATCH] =?UTF-8?q?web=E7=AB=AF=E6=89=93=E9=80=9Aoss=E8=B5=84?= =?UTF-8?q?=E6=BA=90=E4=B8=8A=E4=BC=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/admin-web/src/App.tsx | 2 + apps/admin-web/src/components/OssUpload.tsx | 91 ++++ apps/admin-web/src/layouts/AdminLayout.tsx | 2 + apps/admin-web/src/lib/constants.ts | 27 ++ apps/admin-web/src/lib/upload.ts | 49 ++ apps/admin-web/src/pages/ProductsPage.tsx | 9 +- apps/admin-web/src/pages/ResourcesPage.tsx | 266 +++++++++++ apps/admin-web/src/pages/StoreMediaPage.tsx | 17 +- apps/admin-web/src/pages/StoresPage.tsx | 13 +- package.json | 1 + pnpm-lock.yaml | 432 ++++++++++++++++++ scripts/configure-oss-cors.mjs | 85 ++++ server/dukang-api/.env.example | 10 +- server/dukang-api/package.json | 3 + .../src/integrations/oss/oss.aliyun.client.ts | 25 + .../integrations/oss/oss.aliyun.provider.ts | 84 +++- .../src/integrations/oss/oss.interface.ts | 17 + .../src/integrations/oss/oss.key.util.ts | 7 + .../src/integrations/oss/oss.mock.provider.ts | 24 +- .../modules/common/dto/common-mutate.dto.ts | 10 + .../src/modules/common/resource.controller.ts | 29 +- .../src/modules/common/resource.service.ts | 23 +- 22 files changed, 1186 insertions(+), 40 deletions(-) create mode 100644 apps/admin-web/src/components/OssUpload.tsx create mode 100644 apps/admin-web/src/lib/upload.ts create mode 100644 apps/admin-web/src/pages/ResourcesPage.tsx create mode 100644 scripts/configure-oss-cors.mjs create mode 100644 server/dukang-api/src/integrations/oss/oss.aliyun.client.ts create mode 100644 server/dukang-api/src/integrations/oss/oss.key.util.ts diff --git a/apps/admin-web/src/App.tsx b/apps/admin-web/src/App.tsx index 4555702..9b68121 100644 --- a/apps/admin-web/src/App.tsx +++ b/apps/admin-web/src/App.tsx @@ -17,6 +17,7 @@ import HqAccountsPage from './pages/HqAccountsPage'; import CitiesPage from './pages/CitiesPage'; import StoreMediaPage from './pages/StoreMediaPage'; import ProductsPage from './pages/ProductsPage'; +import ResourcesPage from './pages/ResourcesPage'; function RequireAuth({ children }: { children: React.ReactNode }) { if (!getToken()) return ; @@ -41,6 +42,7 @@ export default function App() { } /> } /> } /> + } /> } /> } /> } /> diff --git a/apps/admin-web/src/components/OssUpload.tsx b/apps/admin-web/src/components/OssUpload.tsx new file mode 100644 index 0000000..7196b8e --- /dev/null +++ b/apps/admin-web/src/components/OssUpload.tsx @@ -0,0 +1,91 @@ +import { useState } from 'react'; +import { Button, Image, Input, Space, Upload, message } from 'antd'; +import { UploadOutlined } from '@ant-design/icons'; +import type { UploadProps } from 'antd'; +import { uploadFileToOss, type OssMediaType, type UploadFileResult } from '../lib/upload'; + +type OssUploadProps = { + value?: string; + onChange?: (url: string) => void; + onUploaded?: (result: UploadFileResult) => void; + bizType: string; + mediaType?: OssMediaType; + accept?: string; + maxSizeMb?: number; + placeholder?: string; +}; + +const DEFAULT_MAX_MB = 10; + +export default function OssUpload({ + value, + onChange, + onUploaded, + bizType, + mediaType = 'IMAGE', + accept, + maxSizeMb = DEFAULT_MAX_MB, + placeholder = '上传后自动填入,或手动粘贴 URL', +}: OssUploadProps) { + const [uploading, setUploading] = useState(false); + + const resolvedAccept = + accept ?? (mediaType === 'VIDEO' ? 'video/*' : mediaType === 'FILE' ? undefined : 'image/*'); + + const customRequest: UploadProps['customRequest'] = async ({ file, onSuccess, onError }) => { + const raw = file as File; + if (raw.size > maxSizeMb * 1024 * 1024) { + const err = new Error(`文件不能超过 ${maxSizeMb}MB`); + message.error(err.message); + onError?.(err); + return; + } + setUploading(true); + try { + const result = await uploadFileToOss(raw, { bizType, mediaType }); + onChange?.(result.url); + onUploaded?.(result); + if (result.mock) { + message.info('当前为 Mock OSS,已使用占位 URL'); + } else { + message.success('上传成功'); + } + onSuccess?.(result); + } catch (e) { + const err = e instanceof Error ? e : new Error('上传失败'); + message.error(err.message); + onError?.(err); + } finally { + setUploading(false); + } + }; + + return ( + + {value && mediaType === 'IMAGE' && ( + + )} + {value && mediaType === 'VIDEO' && ( + + ); +} diff --git a/apps/admin-web/src/layouts/AdminLayout.tsx b/apps/admin-web/src/layouts/AdminLayout.tsx index cb88539..e9541f8 100644 --- a/apps/admin-web/src/layouts/AdminLayout.tsx +++ b/apps/admin-web/src/layouts/AdminLayout.tsx @@ -12,6 +12,7 @@ import { CarOutlined, SafetyOutlined, LogoutOutlined, + CloudUploadOutlined, } from '@ant-design/icons'; import { clearAuth, request, type HqProfile } from '../lib/api'; @@ -22,6 +23,7 @@ const MENU_ITEMS: MenuProps['items'] = [ { key: '/users', icon: , label: '用户' }, { key: '/products', icon: , label: '商品' }, { key: '/orders', icon: , label: '订单' }, + { key: '/resources', icon: , label: 'OSS 资源库' }, { key: 'stores-group', icon: , diff --git a/apps/admin-web/src/lib/constants.ts b/apps/admin-web/src/lib/constants.ts index b716ee5..53cd729 100644 --- a/apps/admin-web/src/lib/constants.ts +++ b/apps/admin-web/src/lib/constants.ts @@ -42,6 +42,33 @@ export const PARTNER_BILL_STATUS_LABELS: Record = { export const MEDIA_TYPE_LABELS: Record = { IMAGE: '图片', VIDEO: '视频', + FILE: '文件', +}; + +export const RESOURCE_OWNER_TYPE_LABELS: Record = { + PRODUCT: '商品', + STORE: '门店', + PARTNER: '合伙人', + USER: '用户', + ORDER: '订单', + HQ: '总部', +}; + +export const RESOURCE_BIZ_TYPE_LABELS: Record = { + COVER: '封面', + ENV: '环境图', + CONTRACT: '合同', + CAROUSEL: '轮播', + DETAIL: '详情', + AVATAR: '头像', + QRCODE: '二维码', + SIGN_PHOTO: '签收照', + VIDEO: '视频', +}; + +export const RESOURCE_STATUS_LABELS: Record = { + ACTIVE: '有效', + DELETED: '已删除', }; export const PRODUCT_STATUS_LABELS: Record = { diff --git a/apps/admin-web/src/lib/upload.ts b/apps/admin-web/src/lib/upload.ts new file mode 100644 index 0000000..5cf1c05 --- /dev/null +++ b/apps/admin-web/src/lib/upload.ts @@ -0,0 +1,49 @@ +import { apiBase, CLIENT_APP, getToken, clearAuth } from './api'; + +export type OssMediaType = 'IMAGE' | 'VIDEO' | 'FILE'; + +export type UploadFileResult = { + url: string; + ossKey: string; + bucket: string; + mock: boolean; +}; + +/** 经 API 服务端转存 OSS,避免浏览器直传跨域 */ +export async function uploadFileToOss( + file: File, + options: { bizType: string; mediaType?: OssMediaType }, +): Promise { + const mediaType = options.mediaType ?? (file.type.startsWith('video/') ? 'VIDEO' : 'IMAGE'); + const formData = new FormData(); + formData.append('file', file); + formData.append('bizType', options.bizType); + formData.append('mediaType', mediaType); + + const headers: Record = { + 'X-Client-App': CLIENT_APP, + }; + const token = getToken(); + if (token) headers.Authorization = `Bearer ${token}`; + + const res = await fetch(`${apiBase}/common/resources/upload`, { + method: 'POST', + headers, + body: formData, + }); + const json = await res.json(); + if (json.code === 401) { + clearAuth(); + window.location.href = '/login'; + throw new Error('未登录'); + } + if (json.code !== 0) throw new Error(json.message || '上传失败'); + + const data = json.data as UploadFileResult; + return { + url: data.url, + ossKey: data.ossKey, + bucket: data.bucket, + mock: data.mock, + }; +} diff --git a/apps/admin-web/src/pages/ProductsPage.tsx b/apps/admin-web/src/pages/ProductsPage.tsx index 6f996b1..117c7ea 100644 --- a/apps/admin-web/src/pages/ProductsPage.tsx +++ b/apps/admin-web/src/pages/ProductsPage.tsx @@ -6,6 +6,7 @@ import type { ColumnsType } from 'antd/es/table'; import { request, type Paginated } from '../lib/api'; import { AROMA_TYPE_LABELS, PRODUCT_STATUS_LABELS, fmtTime } from '../lib/constants'; import { useAdminList } from '../lib/useAdminList'; +import OssUpload from '../components/OssUpload'; type Row = { id: string; @@ -114,7 +115,9 @@ export default function ProductsPage() { + + + )} @@ -142,7 +145,9 @@ export default function ProductsPage() { + + + diff --git a/apps/admin-web/src/pages/ResourcesPage.tsx b/apps/admin-web/src/pages/ResourcesPage.tsx new file mode 100644 index 0000000..6904212 --- /dev/null +++ b/apps/admin-web/src/pages/ResourcesPage.tsx @@ -0,0 +1,266 @@ +import { useEffect, useState } from 'react'; +import { + Button, Form, Image, Input, InputNumber, Modal, Popconfirm, Select, Space, Table, Tag, Typography, message, +} from 'antd'; +import type { ColumnsType } from 'antd/es/table'; +import { request, type Paginated } from '../lib/api'; +import { + MEDIA_TYPE_LABELS, + RESOURCE_BIZ_TYPE_LABELS, + RESOURCE_OWNER_TYPE_LABELS, + RESOURCE_STATUS_LABELS, + fmtTime, +} from '../lib/constants'; +import OssUpload from '../components/OssUpload'; + +type Row = { + id: string; + ownerType: string; + ownerId: string; + bizType: string; + mediaType: string; + ossBucket: string; + ossKey: string; + url: string; + fileName?: string | null; + sortOrder: number; + status: string; + createdAt: string; +}; + +export default function ResourcesPage() { + const [form] = Form.useForm(); + const [createForm] = Form.useForm(); + const [filters, setFilters] = useState>({}); + const [data, setData] = useState | null>(null); + const [loading, setLoading] = useState(false); + const [page, setPage] = useState(1); + const [pageSize, setPageSize] = useState(20); + const [createOpen, setCreateOpen] = useState(false); + const [saving, setSaving] = useState(false); + + async function reload(p = page, ps = pageSize, f = filters) { + setLoading(true); + try { + const qs = new URLSearchParams({ page: String(p), pageSize: String(ps) }); + if (f.ownerType) qs.set('ownerType', f.ownerType); + if (f.ownerId) qs.set('ownerId', f.ownerId); + if (f.bizType) qs.set('bizType', f.bizType); + if (f.status) qs.set('status', f.status); + const res = await request>(`/common/resources?${qs}`); + setData(res); + } finally { + setLoading(false); + } + } + + useEffect(() => { + void reload(); + }, []); + + const columns: ColumnsType = [ + { title: 'ID', dataIndex: 'id', width: 80 }, + { + title: '归属', + width: 140, + render: (_, row) => ( + + {RESOURCE_OWNER_TYPE_LABELS[row.ownerType] || row.ownerType} / {row.ownerId} + + ), + }, + { title: '用途', dataIndex: 'bizType', width: 90, render: (v) => RESOURCE_BIZ_TYPE_LABELS[v] || v }, + { title: '类型', dataIndex: 'mediaType', width: 70, render: (v) => MEDIA_TYPE_LABELS[v] || v }, + { + title: '预览', + dataIndex: 'url', + width: 90, + render: (url, row) => + row.mediaType === 'IMAGE' ? ( + + ) : ( + + 查看 + + ), + }, + { title: 'URL', dataIndex: 'url', ellipsis: true }, + { title: 'OSS Key', dataIndex: 'ossKey', ellipsis: true, width: 160 }, + { title: '排序', dataIndex: 'sortOrder', width: 60 }, + { + title: '状态', + dataIndex: 'status', + width: 80, + render: (s) => {RESOURCE_STATUS_LABELS[s] || s}, + }, + { title: '创建', dataIndex: 'createdAt', width: 160, render: fmtTime }, + { + title: '操作', + width: 80, + render: (_, row) => ( + { + await request(`/common/resources/${row.id}`, { method: 'DELETE' }); + message.success('已删除'); + void reload(); + }} + > + + + ), + }, + ]; + + return ( +
+ + + OSS 资源库 + + + +
{ + setFilters(v); + setPage(1); + void reload(1, pageSize, v); + }} + > + + + + + ({ value, label }))} + /> + + + + +
+ { + setPage(p); + setPageSize(ps); + void reload(p, ps); + }, + }} + /> + setCreateOpen(false)} + onOk={async () => { + const v = await createForm.validateFields(); + setSaving(true); + try { + await request('/common/resources', { + method: 'POST', + body: JSON.stringify({ + ownerType: v.ownerType, + ownerId: v.ownerId, + bizType: v.bizType, + mediaType: v.mediaType, + ossKey: v.ossKey, + url: v.url, + fileName: v.fileName, + sortOrder: v.sortOrder ?? 0, + }), + }); + message.success('已登记'); + setCreateOpen(false); + createForm.resetFields(); + void reload(); + } finally { + setSaving(false); + } + }} + width={520} + > +
+ + + + + ({ value, label })), + { value: 'FILE', label: '文件' }, + ]} + /> + + prev.mediaType !== cur.mediaType || prev.bizType !== cur.bizType}> + {({ getFieldValue }) => { + const mediaType = getFieldValue('mediaType') || 'IMAGE'; + const bizType = getFieldValue('bizType') || 'COVER'; + return ( + + { + createForm.setFieldsValue({ ossKey: result.ossKey, url: result.url }); + }} + /> + + ); + }} + + + + + + + + + + + +
+ + ); +} diff --git a/apps/admin-web/src/pages/StoreMediaPage.tsx b/apps/admin-web/src/pages/StoreMediaPage.tsx index 32b7e73..b04805b 100644 --- a/apps/admin-web/src/pages/StoreMediaPage.tsx +++ b/apps/admin-web/src/pages/StoreMediaPage.tsx @@ -6,6 +6,7 @@ import type { ColumnsType } from 'antd/es/table'; import { request, type Paginated } from '../lib/api'; import { MEDIA_TYPE_LABELS, fmtTime } from '../lib/constants'; import { useAdminList } from '../lib/useAdminList'; +import OssUpload from '../components/OssUpload'; type Row = { id: string; mediaType: string; url: string; sortOrder: number; createdAt: string; @@ -102,7 +103,13 @@ export default function StoreMediaPage() { + prev.mediaType !== cur.mediaType}> + {({ getFieldValue }) => ( + + + + )} + @@ -116,7 +123,13 @@ export default function StoreMediaPage() { }}> + prev.mediaType !== cur.mediaType}> + {({ getFieldValue }) => ( + + + + )} + diff --git a/apps/admin-web/src/pages/StoresPage.tsx b/apps/admin-web/src/pages/StoresPage.tsx index 31e69df..4ab65d3 100644 --- a/apps/admin-web/src/pages/StoresPage.tsx +++ b/apps/admin-web/src/pages/StoresPage.tsx @@ -6,6 +6,7 @@ import type { ColumnsType } from 'antd/es/table'; import { request, type Paginated } from '../lib/api'; import { STORE_STATUS_LABELS, fmtTime } from '../lib/constants'; import { useAdminList } from '../lib/useAdminList'; +import OssUpload from '../components/OssUpload'; type StoreRow = { id: string; @@ -128,16 +129,18 @@ export default function StoresPage() { {String(detail.id)}{String(detail.province)}{String(detail.cityName)}{String(detail.district)}{String(detail.address)}{String(detail.redeemCount ?? 0)} - {detail.coverUrl && ( + {detail.coverUrl ? ( - )} + ) : null} - + + + @@ -164,7 +167,9 @@ export default function StoresPage() { - + + + diff --git a/package.json b/package.json index 23a82a8..26aed1c 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "db:seed": "pnpm --filter @dukang/api prisma:seed", "db:sync-benefit": "pnpm --filter @dukang/api prisma:sync-benefit", "db:validate": "pnpm --filter @dukang/api prisma:validate", + "oss:cors": "node scripts/configure-oss-cors.mjs", "sync:stitch": "node scripts/sync-stitch.mjs" }, "engines": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c0059b9..fc92cfd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -225,6 +225,9 @@ importers: '@prisma/client': specifier: ^5.18.0 version: 5.22.0(prisma@5.22.0) + ali-oss: + specifier: ^6.23.0 + version: 6.23.0 bullmq: specifier: ^5.12.0 version: 5.79.2 @@ -253,9 +256,15 @@ importers: '@nestjs/schematics': specifier: ^10.1.0 version: 10.2.3(chokidar@3.6.0)(typescript@5.9.3) + '@types/ali-oss': + specifier: ^6.23.3 + version: 6.23.3 '@types/express': specifier: ^4.17.21 version: 4.17.25 + '@types/multer': + specifier: ^2.1.0 + version: 2.1.0 '@types/node': specifier: ^20.14.0 version: 20.19.43 @@ -972,6 +981,9 @@ packages: '@tsconfig/node16@1.0.4': resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==} + '@types/ali-oss@6.23.3': + resolution: {integrity: sha512-huSf6njkqhSeR37OMJTGMCyUg2d9Zp2AioefhYuUMeh0vNM+WybctrBor7bO/RcCbLCQI6sHn6NtPBIWALYVJg==} + '@types/babel__core@7.20.5': resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} @@ -1017,6 +1029,9 @@ packages: '@types/mime@1.3.5': resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==} + '@types/multer@2.1.0': + resolution: {integrity: sha512-zYZb0+nJhOHtPpGDb3vqPjwpdeGlGC157VpkqNQL+UU2qwoacoQ7MpsAmUptI/0Oa127X32JzWDqQVEXp2RcIA==} + '@types/node@20.19.43': resolution: {integrity: sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==} @@ -1134,6 +1149,14 @@ packages: engines: {node: '>=0.4.0'} hasBin: true + address@1.2.2: + resolution: {integrity: sha512-4B/qKCfeE/ODUaAUpSwfzazo5x29WD4r3vXiWsB7I2mSDAihwEqKO+g8GELZUQSSAo5e1XTYh3ZVfLyxBc12nA==} + engines: {node: '>= 10.0.0'} + + agentkeepalive@3.5.3: + resolution: {integrity: sha512-yqXL+k5rr8+ZRpOAntkaaRgWgE5o8ESAj5DyRmVTCSoZxXmqemb9Dd7T4i5UzwuERdLAJUy6XzR9zFVuf0kzkw==} + engines: {node: '>= 4.0.0'} + ajv-formats@2.1.1: resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==} peerDependencies: @@ -1161,6 +1184,10 @@ packages: ajv@8.20.0: resolution: {integrity: sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==} + ali-oss@6.23.0: + resolution: {integrity: sha512-FipRmyd16Pr/tEey/YaaQ/24Pc3HEpLM9S1DRakEuXlSLXNIJnu1oJtHM53eVYpvW3dXapSjrip3xylZUTIZVQ==} + engines: {node: '>=8'} + ansi-colors@4.1.3: resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} engines: {node: '>=6'} @@ -1195,6 +1222,9 @@ packages: react: '>=16.9.0' react-dom: '>=16.9.0' + any-promise@1.3.0: + resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} + anymatch@3.1.3: resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} engines: {node: '>= 8'} @@ -1239,6 +1269,9 @@ packages: resolution: {integrity: sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==} engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + bowser@1.9.4: + resolution: {integrity: sha512-9IdMmj2KjigRq6oWhmwv1W36pDuA4STQZ8q6YO9um+x07xgYNCD3Oou+WP/3L1HNz7iqythGet3/p4wvc8AAwQ==} + brace-expansion@1.1.15: resolution: {integrity: sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==} @@ -1263,6 +1296,9 @@ packages: buffer@5.7.1: resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} + builtin-status-codes@3.0.0: + resolution: {integrity: sha512-HpGFw18DgFWlncDfjTa2rcQ4W88O1mC8e8yZ2AvQY5KDaktSTwo+KRf6nHK6FRI5FyRyb/5T6+TSxfP7QyGsmQ==} + bullmq@5.79.2: resolution: {integrity: sha512-FebD+8XCZl/hnS1R4to24L4EAN70XSndKZO0776M36vGRk5MKVhKlNFM8/34zXLXKyYB4QaeIPFhVXSYYGTHpQ==} engines: {node: '>=12.22.0'} @@ -1428,6 +1464,9 @@ packages: copy-to-clipboard@3.3.3: resolution: {integrity: sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA==} + copy-to@2.0.1: + resolution: {integrity: sha512-3DdaFaU/Zf1AnpLiFDeNCD4TOWe3Zl2RZaTzUvWiIk5ERzcCodOE20Vqq4fzCbNoHURFHT4/us/Lfq+S2zyY4w==} + core-util-is@1.0.3: resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} @@ -1458,6 +1497,9 @@ packages: csstype@3.2.3: resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + dateformat@2.2.0: + resolution: {integrity: sha512-GODcnWq3YGoTnygPfi02ygEiRxqUxpJwuRHjdhJYuxpcZmDq4rjBiXYmbCCzStxo176ixfLT6i4NPwQooRySnw==} + dayjs@1.11.21: resolution: {integrity: sha512-98IT+HOahAisibz/yjKbzuOBwYcjJ7BCLPzARyHiyEBmRz4fatF+KPJszEHXsGYjUG234aH/cOjW1wwTbKUZlA==} @@ -1486,6 +1528,10 @@ packages: resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} engines: {node: '>=0.10.0'} + default-user-agent@1.0.0: + resolution: {integrity: sha512-bDF7bg6OSNcSwFWPu4zYKpVkJZQYVrAANMYB8bc9Szem1D0yKdm4sa/rOCs2aC9+2GMqQ7KnwtZRvDhmLF0dXw==} + engines: {node: '>= 0.10.0'} + defaults@1.0.4: resolution: {integrity: sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==} @@ -1517,6 +1563,10 @@ packages: resolution: {integrity: sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==} engines: {node: '>=0.3.1'} + digest-header@1.1.0: + resolution: {integrity: sha512-glXVh42vz40yZb9Cq2oMOt70FIoWiv+vxNvdKdU8CwjLad25qHM3trLxhl9bVjdr6WaslIXhWpn0NO8T/67Qjg==} + engines: {node: '>= 8.0.0'} + dotenv-expand@10.0.0: resolution: {integrity: sha512-GopVGCpVS1UKH75VKHGuQFqS1Gusej0z4FyQkPdwjil2gNIv+LNsqBlboOzpJFZKVT95GkCyWJbBSdFEFUWI2A==} engines: {node: '>=12'} @@ -1554,6 +1604,13 @@ packages: resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} engines: {node: '>= 0.8'} + end-of-stream@1.4.5: + resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} + + end-or-error@1.0.1: + resolution: {integrity: sha512-OclLMSug+k2A0JKuf494im25ANRBVW8qsjmwbgX7lQ8P82H21PQ1PWkoYwb9y5yMBS69BPlwtzdIFClo3+7kOQ==} + engines: {node: '>= 0.11.14'} + enhanced-resolve@5.24.1: resolution: {integrity: sha512-7DdUaTjmNwMcH2gLr1qycesKII3BK4RLy/mdAb7x10Lq7bR4aNKHt1BR1ZALSv0rPM/hF5wYF0PhGop/rJm8vw==} engines: {node: '>=10.13.0'} @@ -1632,6 +1689,10 @@ packages: resolution: {integrity: sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==} engines: {node: '>= 0.10.0'} + extend-shallow@2.0.1: + resolution: {integrity: sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==} + engines: {node: '>=0.10.0'} + external-editor@3.1.0: resolution: {integrity: sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==} engines: {node: '>=4'} @@ -1678,6 +1739,9 @@ packages: typescript: '>3.6.0' webpack: ^5.11.0 + formstream@1.5.2: + resolution: {integrity: sha512-NASf0lgxC1AyKNXQIrXTEYkiX99LhCEXTkiGObXAkpBui86a4u8FjH1o2bGb3PpqI3kafC+yw4zWeK6l6VHTgg==} + forwarded@0.2.0: resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} engines: {node: '>= 0.6'} @@ -1716,6 +1780,9 @@ packages: resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} engines: {node: '>= 0.4'} + get-ready@1.0.0: + resolution: {integrity: sha512-mFXCZPJIlcYcth+N8267+mghfYN9h3EhsDa6JSnbA3Wrhh/XFpuowviFcsDeYZtKspQyWyJqfs4O6P8CHeTwzw==} + get-stream@8.0.1: resolution: {integrity: sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==} engines: {node: '>=16'} @@ -1766,10 +1833,17 @@ packages: resolution: {integrity: sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==} engines: {node: '>=16.17.0'} + humanize-ms@1.2.1: + resolution: {integrity: sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==} + iconv-lite@0.4.24: resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} engines: {node: '>=0.10.0'} + iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + ieee754@1.2.1: resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} @@ -1812,6 +1886,13 @@ packages: resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} engines: {node: '>=8'} + is-class-hotfix@0.0.6: + resolution: {integrity: sha512-0n+pzCC6ICtVr/WXnN2f03TK/3BfXY7me4cjCAqT8TYXEl0+JBRoqBo94JJHXcyDSLUeWbNX8Fvy5g5RJdAstQ==} + + is-extendable@0.1.1: + resolution: {integrity: sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==} + engines: {node: '>=0.10.0'} + is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} @@ -1836,13 +1917,22 @@ packages: resolution: {integrity: sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + is-type-of@1.4.0: + resolution: {integrity: sha512-EddYllaovi5ysMLMEN7yzHEKh8A850cZ7pykrY1aNRQGn/CDjRDE9qEWbIdt7xGEVJmjBXzU/fNnC4ABTm8tEQ==} + is-unicode-supported@0.1.0: resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} engines: {node: '>=10'} + isarray@1.0.0: + resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} + isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + isstream@0.1.2: + resolution: {integrity: sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==} + iterare@1.2.1: resolution: {integrity: sha512-RKYVTCjAnRthyJes037NX/IiqeidgN1xc3j1RjFfECFp28A1GVwK9nA+i0rJPaHqSZwygLzRnFlzUuHFoWWy+Q==} engines: {node: '>=6'} @@ -1854,6 +1944,9 @@ packages: resolution: {integrity: sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==} engines: {node: '>= 10.13.0'} + js-base64@2.6.4: + resolution: {integrity: sha512-pZe//GGmwJndub7ZghVHz7vjb2LgC1m8B07Au3eYqeqv9emhESByMXxaEgkUkEqJe87oBbSniGYoQNIBklc7IQ==} + js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -1899,6 +1992,9 @@ packages: resolution: {integrity: sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==} engines: {node: '>=12', npm: '>=6'} + jstoxml@2.2.9: + resolution: {integrity: sha512-OYWlK0j+roh+eyaMROlNbS5cd5R25Y+IUpdl7cNdB8HNrkgwQzIS7L9MegxOiWNBj9dQhA/yAxiMwCC5mwNoBw==} + jwa@1.4.2: resolution: {integrity: sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==} @@ -2018,6 +2114,11 @@ packages: engines: {node: '>=4'} hasBin: true + mime@2.6.0: + resolution: {integrity: sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==} + engines: {node: '>=4.0.0'} + hasBin: true + mimic-fn@2.1.0: resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} engines: {node: '>=6'} @@ -2071,6 +2172,9 @@ packages: resolution: {integrity: sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + mz@2.7.0: + resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} + nanoid@3.3.15: resolution: {integrity: sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} @@ -2102,6 +2206,10 @@ packages: resolution: {integrity: sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==} hasBin: true + node-hex@1.0.1: + resolution: {integrity: sha512-iwpZdvW6Umz12ICmu9IYPRxg0tOLGmU3Tq2tKetejCj3oZd7b2nUXwP3a7QA5M9glWy8wlPS1G3RwM/CdsUbdQ==} + engines: {node: '>=8.0.0'} + node-releases@2.0.50: resolution: {integrity: sha512-J6l92tKHX6w8Jy5nO1Vuc01NoIiRGi/d6qBKVxh+IQ8Cr3b6HbVNfKiF8ZpFKufTwpwxMmce2W3iQZ861ZRyTg==} engines: {node: '>=18'} @@ -2126,6 +2234,9 @@ packages: resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} engines: {node: '>= 0.8'} + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + onetime@5.1.2: resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} engines: {node: '>=6'} @@ -2138,10 +2249,20 @@ packages: resolution: {integrity: sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==} engines: {node: '>=10'} + os-name@1.0.3: + resolution: {integrity: sha512-f5estLO2KN8vgtTRaILIgEGBoBrMnZ3JQ7W9TMZCnOIGwHe8TRGSpcagnWDo+Dfhd/z08k9Xe75hvciJJ8Qaew==} + engines: {node: '>=0.10.0'} + hasBin: true + os-tmpdir@1.0.2: resolution: {integrity: sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==} engines: {node: '>=0.10.0'} + osx-release@1.1.0: + resolution: {integrity: sha512-ixCMMwnVxyHFQLQnINhmIpWqXIfS2YOXchwQrk+OFzmo6nDjQ0E4KXAyyUh0T0MZgV4bUhkRrAbVqlE4yLVq4A==} + engines: {node: '>=0.10.0'} + hasBin: true + p-limit@5.0.0: resolution: {integrity: sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ==} engines: {node: '>=18'} @@ -2192,6 +2313,9 @@ packages: pathval@1.1.1: resolution: {integrity: sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==} + pause-stream@0.0.11: + resolution: {integrity: sha512-e3FBlXLmN/D1S+zHzanP4E/4Z60oFAa3O051qt1pxa7DEJWKAyil6upYVXCWadEnuoqa4Pkc9oUx9zsxYeRv8A==} + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -2206,6 +2330,9 @@ packages: pkg-types@1.3.1: resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} + platform@1.3.6: + resolution: {integrity: sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg==} + pluralize@8.0.0: resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==} engines: {node: '>=4'} @@ -2223,10 +2350,16 @@ packages: engines: {node: '>=16.13'} hasBin: true + process-nextick-args@2.0.1: + resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} + proxy-addr@2.0.7: resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} engines: {node: '>= 0.10'} + pump@3.0.4: + resolution: {integrity: sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==} + punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} @@ -2500,6 +2633,9 @@ packages: resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} engines: {node: '>=0.10.0'} + readable-stream@2.3.8: + resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} + readable-stream@3.6.2: resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} engines: {node: '>= 6'} @@ -2557,12 +2693,19 @@ packages: rxjs@7.8.2: resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==} + safe-buffer@5.1.2: + resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} + safe-buffer@5.2.1: resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + sax@1.6.0: + resolution: {integrity: sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==} + engines: {node: '>=11.0.0'} + scheduler@0.23.2: resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==} @@ -2577,6 +2720,13 @@ packages: scroll-into-view-if-needed@3.1.0: resolution: {integrity: sha512-49oNpRjWRvnU8NyGVmUaYG4jtTkNonFZI86MmGRDqBphEK2EXT9gdEUoQPZhuBM8yWHxCWbobltqYO5M4XrUvQ==} + sdk-base@2.0.1: + resolution: {integrity: sha512-eeG26wRwhtwYuKGCDM3LixCaxY27Pa/5lK4rLKhQa7HBjJ3U3Y+f81MMZQRsDw/8SC2Dao/83yJTXJ8aULuN8Q==} + + semver@5.7.2: + resolution: {integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==} + hasBin: true + semver@6.3.1: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true @@ -2656,6 +2806,10 @@ packages: standard-as-callback@2.1.0: resolution: {integrity: sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==} + statuses@1.5.0: + resolution: {integrity: sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==} + engines: {node: '>= 0.6'} + statuses@2.0.2: resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} engines: {node: '>= 0.8'} @@ -2663,6 +2817,13 @@ packages: std-env@3.10.0: resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} + stream-http@2.8.2: + resolution: {integrity: sha512-QllfrBhqF1DPcz46WxKTs6Mz1Bpc+8Qm6vbqOpVav5odAXwbyzwnEczoWqtxrsmlO+cJqtPrp/8gWKWjaKLLlA==} + + stream-wormhole@1.1.0: + resolution: {integrity: sha512-gHFfL3px0Kctd6Po0M8TzEvt3De/xu6cnRrjlfYNhwbhLPLwigI2t1nc6jrzNuaYg5C4YF78PPFuQPzRiqn9ew==} + engines: {node: '>=4.0.0'} + streamsearch@1.1.0: resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} engines: {node: '>=10.0.0'} @@ -2678,6 +2839,9 @@ packages: resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} engines: {node: '>=12'} + string_decoder@1.1.1: + resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} + string_decoder@1.3.0: resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} @@ -2771,6 +2935,13 @@ packages: engines: {node: '>=10'} hasBin: true + thenify-all@1.6.0: + resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} + engines: {node: '>=0.8'} + + thenify@3.3.1: + resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} + throttle-debounce@5.0.2: resolution: {integrity: sha512-B71/4oyj61iNH0KeCamLuE2rmKuTO5byTOSVwECM5FA7TiAiAW+UqTKZ9ERueC4qvgSttUhdmq1mXC3kJqGX7A==} engines: {node: '>=12.22'} @@ -2793,6 +2964,9 @@ packages: resolution: {integrity: sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==} engines: {node: '>=0.6.0'} + to-arraybuffer@1.0.1: + resolution: {integrity: sha512-okFlQcoGTi4LQBG/PgSYblw9VOyptsz2KJZqc6qtgGdes8VktzUQkj4BI2blit072iS8VODNcMA+tvnS9dnuMA==} + to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} @@ -2879,6 +3053,10 @@ packages: undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + unescape@1.0.1: + resolution: {integrity: sha512-O0+af1Gs50lyH1nUu3ZyYS1cRh01Q/kUKatTOkSs7jukXE6/NebucDVxyiDsA9AQ4JC1V1jUH9EO8JX2nMDgGQ==} + engines: {node: '>=0.10.0'} + universalify@2.0.1: resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} engines: {node: '>= 10.0.0'} @@ -2896,9 +3074,22 @@ packages: uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + urllib@2.44.1: + resolution: {integrity: sha512-vreOVvFizoiIz5NK9IYMgUknkriHHBVccn2VFfJhgKz6O2qwm0SgjFk4OpXFRDXpdrTx8EzM1DB0/pejrqXwPA==} + engines: {node: '>= 0.10.0'} + peerDependencies: + proxy-agent: ^5.0.0 + peerDependenciesMeta: + proxy-agent: + optional: true + util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + utility@1.18.0: + resolution: {integrity: sha512-PYxZDA+6QtvRvm//++aGdmKG/cI07jNwbROz0Ql+VzFV1+Z0Dy55NI4zZ7RHc9KKpBePNFwoErqIuqQv/cjiTA==} + engines: {node: '>= 0.12.0'} + utils-merge@1.0.1: resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} engines: {node: '>= 0.4.0'} @@ -3016,6 +3207,10 @@ packages: engines: {node: '>=8'} hasBin: true + win-release@1.1.1: + resolution: {integrity: sha512-iCRnKVvGxOQdsKhcQId2PXV1vV3J/sDPXKA4Oe9+Eti2nb2ESEsYHRYls/UjoUW3bIc5ZDO8dTH50A/5iVN+bw==} + engines: {node: '>=0.10.0'} + wrap-ansi@6.2.0: resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} engines: {node: '>=8'} @@ -3028,6 +3223,17 @@ packages: resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} engines: {node: '>=12'} + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + xml2js@0.6.2: + resolution: {integrity: sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==} + engines: {node: '>=4.0.0'} + + xmlbuilder@11.0.1: + resolution: {integrity: sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==} + engines: {node: '>=4.0'} + xtend@4.0.2: resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} engines: {node: '>=0.4'} @@ -3725,6 +3931,8 @@ snapshots: '@tsconfig/node16@1.0.4': {} + '@types/ali-oss@6.23.3': {} + '@types/babel__core@7.20.5': dependencies: '@babel/parser': 7.29.7 @@ -3791,6 +3999,10 @@ snapshots: '@types/mime@1.3.5': {} + '@types/multer@2.1.0': + dependencies: + '@types/express': 4.17.25 + '@types/node@20.19.43': dependencies: undici-types: 6.21.0 @@ -3959,6 +4171,12 @@ snapshots: acorn@8.17.0: {} + address@1.2.2: {} + + agentkeepalive@3.5.3: + dependencies: + humanize-ms: 1.2.1 + ajv-formats@2.1.1(ajv@8.12.0): optionalDependencies: ajv: 8.12.0 @@ -3997,6 +4215,37 @@ snapshots: json-schema-traverse: 1.0.0 require-from-string: 2.0.2 + ali-oss@6.23.0: + dependencies: + address: 1.2.2 + agentkeepalive: 3.5.3 + bowser: 1.9.4 + copy-to: 2.0.1 + dateformat: 2.2.0 + debug: 4.4.3 + destroy: 1.2.0 + end-or-error: 1.0.1 + get-ready: 1.0.0 + humanize-ms: 1.2.1 + is-type-of: 1.4.0 + js-base64: 2.6.4 + jstoxml: 2.2.9 + lodash: 4.18.1 + merge-descriptors: 1.0.3 + mime: 2.6.0 + platform: 1.3.6 + pump: 3.0.4 + qs: 6.14.2 + sdk-base: 2.0.1 + stream-http: 2.8.2 + stream-wormhole: 1.1.0 + urllib: 2.44.1 + utility: 1.18.0 + xml2js: 0.6.2 + transitivePeerDependencies: + - proxy-agent + - supports-color + ansi-colors@4.1.3: {} ansi-escapes@4.3.2: @@ -4073,6 +4322,8 @@ snapshots: - luxon - moment + any-promise@1.3.0: {} + anymatch@3.1.3: dependencies: normalize-path: 3.0.0 @@ -4121,6 +4372,8 @@ snapshots: transitivePeerDependencies: - supports-color + bowser@1.9.4: {} + brace-expansion@1.1.15: dependencies: balanced-match: 1.0.2 @@ -4151,6 +4404,8 @@ snapshots: base64-js: 1.5.1 ieee754: 1.2.1 + builtin-status-codes@3.0.0: {} + bullmq@5.79.2: dependencies: cron-parser: 4.9.0 @@ -4311,6 +4566,8 @@ snapshots: dependencies: toggle-selection: 1.0.6 + copy-to@2.0.1: {} + core-util-is@1.0.3: {} cors@2.8.5: @@ -4341,6 +4598,8 @@ snapshots: csstype@3.2.3: {} + dateformat@2.2.0: {} + dayjs@1.11.21: {} debug@2.6.9: @@ -4357,6 +4616,10 @@ snapshots: deepmerge@4.3.1: {} + default-user-agent@1.0.0: + dependencies: + os-name: 1.0.3 + defaults@1.0.4: dependencies: clone: 1.0.4 @@ -4380,6 +4643,8 @@ snapshots: diff@4.0.4: {} + digest-header@1.1.0: {} + dotenv-expand@10.0.0: {} dotenv@16.4.5: {} @@ -4410,6 +4675,12 @@ snapshots: encodeurl@2.0.0: {} + end-of-stream@1.4.5: + dependencies: + once: 1.4.0 + + end-or-error@1.0.1: {} + enhanced-resolve@5.24.1: dependencies: graceful-fs: 4.2.11 @@ -4532,6 +4803,10 @@ snapshots: transitivePeerDependencies: - supports-color + extend-shallow@2.0.1: + dependencies: + is-extendable: 0.1.1 + external-editor@3.1.0: dependencies: chardet: 0.7.0 @@ -4599,6 +4874,13 @@ snapshots: typescript: 5.7.2 webpack: 5.97.1 + formstream@1.5.2: + dependencies: + destroy: 1.2.0 + mime: 2.6.0 + node-hex: 1.0.1 + pause-stream: 0.0.11 + forwarded@0.2.0: {} fresh@0.5.2: {} @@ -4638,6 +4920,8 @@ snapshots: dunder-proto: 1.0.1 es-object-atoms: 1.1.2 + get-ready@1.0.0: {} + get-stream@8.0.1: {} glob-parent@5.1.2: @@ -4683,10 +4967,18 @@ snapshots: human-signals@5.0.0: {} + humanize-ms@1.2.1: + dependencies: + ms: 2.1.3 + iconv-lite@0.4.24: dependencies: safer-buffer: 2.1.2 + iconv-lite@0.6.3: + dependencies: + safer-buffer: 2.1.2 + ieee754@1.2.1: {} import-fresh@3.3.1: @@ -4770,6 +5062,10 @@ snapshots: dependencies: binary-extensions: 2.3.0 + is-class-hotfix@0.0.6: {} + + is-extendable@0.1.1: {} + is-extglob@2.1.1: {} is-fullwidth-code-point@3.0.0: {} @@ -4784,10 +5080,20 @@ snapshots: is-stream@3.0.0: {} + is-type-of@1.4.0: + dependencies: + core-util-is: 1.0.3 + is-class-hotfix: 0.0.6 + isstream: 0.1.2 + is-unicode-supported@0.1.0: {} + isarray@1.0.0: {} + isexe@2.0.0: {} + isstream@0.1.2: {} + iterare@1.2.1: {} jackspeak@3.4.3: @@ -4802,6 +5108,8 @@ snapshots: merge-stream: 2.0.0 supports-color: 8.1.1 + js-base64@2.6.4: {} + js-tokens@4.0.0: {} js-tokens@9.0.1: {} @@ -4847,6 +5155,8 @@ snapshots: ms: 2.1.3 semver: 7.8.5 + jstoxml@2.2.9: {} + jwa@1.4.2: dependencies: buffer-equal-constant-time: 1.0.1 @@ -4944,6 +5254,8 @@ snapshots: mime@1.6.0: {} + mime@2.6.0: {} + mimic-fn@2.1.0: {} mimic-fn@4.0.0: {} @@ -5005,6 +5317,12 @@ snapshots: mute-stream@1.0.0: {} + mz@2.7.0: + dependencies: + any-promise: 1.3.0 + object-assign: 4.1.1 + thenify-all: 1.6.0 + nanoid@3.3.15: {} negotiator@0.6.3: {} @@ -5026,6 +5344,8 @@ snapshots: detect-libc: 2.1.2 optional: true + node-hex@1.0.1: {} + node-releases@2.0.50: {} normalize-path@3.0.0: {} @@ -5042,6 +5362,10 @@ snapshots: dependencies: ee-first: 1.1.1 + once@1.4.0: + dependencies: + wrappy: 1.0.2 + onetime@5.1.2: dependencies: mimic-fn: 2.1.0 @@ -5062,8 +5386,17 @@ snapshots: strip-ansi: 6.0.1 wcwidth: 1.0.1 + os-name@1.0.3: + dependencies: + osx-release: 1.1.0 + win-release: 1.1.1 + os-tmpdir@1.0.2: {} + osx-release@1.1.0: + dependencies: + minimist: 1.2.8 + p-limit@5.0.0: dependencies: yocto-queue: 1.2.2 @@ -5104,6 +5437,10 @@ snapshots: pathval@1.1.1: {} + pause-stream@0.0.11: + dependencies: + through: 2.3.8 + picocolors@1.1.1: {} picomatch@2.3.2: {} @@ -5116,6 +5453,8 @@ snapshots: mlly: 1.8.2 pathe: 2.0.3 + platform@1.3.6: {} + pluralize@8.0.0: {} postcss@8.5.15: @@ -5136,11 +5475,18 @@ snapshots: optionalDependencies: fsevents: 2.3.3 + process-nextick-args@2.0.1: {} + proxy-addr@2.0.7: dependencies: forwarded: 0.2.0 ipaddr.js: 1.9.1 + pump@3.0.4: + dependencies: + end-of-stream: 1.4.5 + once: 1.4.0 + punycode@2.3.1: {} qs@6.14.2: @@ -5502,6 +5848,16 @@ snapshots: dependencies: loose-envify: 1.4.0 + readable-stream@2.3.8: + dependencies: + core-util-is: 1.0.3 + inherits: 2.0.4 + isarray: 1.0.0 + process-nextick-args: 2.0.1 + safe-buffer: 5.1.2 + string_decoder: 1.1.1 + util-deprecate: 1.0.2 + readable-stream@3.6.2: dependencies: inherits: 2.0.4 @@ -5576,10 +5932,14 @@ snapshots: dependencies: tslib: 2.8.1 + safe-buffer@5.1.2: {} + safe-buffer@5.2.1: {} safer-buffer@2.1.2: {} + sax@1.6.0: {} + scheduler@0.23.2: dependencies: loose-envify: 1.4.0 @@ -5601,6 +5961,12 @@ snapshots: dependencies: compute-scroll-into-view: 3.1.1 + sdk-base@2.0.1: + dependencies: + get-ready: 1.0.0 + + semver@5.7.2: {} + semver@6.3.1: {} semver@7.8.5: {} @@ -5698,10 +6064,22 @@ snapshots: standard-as-callback@2.1.0: {} + statuses@1.5.0: {} + statuses@2.0.2: {} std-env@3.10.0: {} + stream-http@2.8.2: + dependencies: + builtin-status-codes: 3.0.0 + inherits: 2.0.4 + readable-stream: 2.3.8 + to-arraybuffer: 1.0.1 + xtend: 4.0.2 + + stream-wormhole@1.1.0: {} + streamsearch@1.1.0: {} string-convert@0.2.1: {} @@ -5718,6 +6096,10 @@ snapshots: emoji-regex: 9.2.2 strip-ansi: 7.2.0 + string_decoder@1.1.1: + dependencies: + safe-buffer: 5.1.2 + string_decoder@1.3.0: dependencies: safe-buffer: 5.2.1 @@ -5771,6 +6153,14 @@ snapshots: commander: 2.20.3 source-map-support: 0.5.21 + thenify-all@1.6.0: + dependencies: + thenify: 3.3.1 + + thenify@3.3.1: + dependencies: + any-promise: 1.3.0 + throttle-debounce@5.0.2: {} through@2.3.8: {} @@ -5785,6 +6175,8 @@ snapshots: dependencies: os-tmpdir: 1.0.2 + to-arraybuffer@1.0.1: {} + to-regex-range@5.0.1: dependencies: is-number: 7.0.0 @@ -5861,6 +6253,10 @@ snapshots: undici-types@6.21.0: {} + unescape@1.0.1: + dependencies: + extend-shallow: 2.0.1 + universalify@2.0.1: {} unpipe@1.0.0: {} @@ -5875,8 +6271,31 @@ snapshots: dependencies: punycode: 2.3.1 + urllib@2.44.1: + dependencies: + any-promise: 1.3.0 + content-type: 1.0.5 + default-user-agent: 1.0.0 + digest-header: 1.1.0 + ee-first: 1.1.1 + formstream: 1.5.2 + humanize-ms: 1.2.1 + iconv-lite: 0.6.3 + pump: 3.0.4 + qs: 6.14.2 + statuses: 1.5.0 + utility: 1.18.0 + util-deprecate@1.0.2: {} + utility@1.18.0: + dependencies: + copy-to: 2.0.1 + escape-html: 1.0.3 + mkdirp: 0.5.6 + mz: 2.7.0 + unescape: 1.0.1 + utils-merge@1.0.1: {} v8-compile-cache-lib@3.0.1: {} @@ -6014,6 +6433,10 @@ snapshots: siginfo: 2.0.0 stackback: 0.0.2 + win-release@1.1.1: + dependencies: + semver: 5.7.2 + wrap-ansi@6.2.0: dependencies: ansi-styles: 4.3.0 @@ -6032,6 +6455,15 @@ snapshots: string-width: 5.1.2 strip-ansi: 7.2.0 + wrappy@1.0.2: {} + + xml2js@0.6.2: + dependencies: + sax: 1.6.0 + xmlbuilder: 11.0.1 + + xmlbuilder@11.0.1: {} + xtend@4.0.2: {} yallist@3.1.1: {} diff --git a/scripts/configure-oss-cors.mjs b/scripts/configure-oss-cors.mjs new file mode 100644 index 0000000..c760789 --- /dev/null +++ b/scripts/configure-oss-cors.mjs @@ -0,0 +1,85 @@ +/** + * 为 OSS Bucket 配置浏览器直传所需的 CORS 规则。 + * 使用 server/dukang-api/.env 中的 OSS 凭证。 + * + * 用法:pnpm oss:cors + * 可选环境变量 OSS_CORS_ORIGINS(逗号分隔),默认包含本地 H5 / admin 端口。 + */ +import { readFileSync, existsSync } from 'node:fs'; +import { resolve, dirname } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { createRequire } from 'node:module'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const apiRoot = resolve(__dirname, '../server/dukang-api'); +const require = createRequire(resolve(apiRoot, 'package.json')); +const OSS = require('ali-oss'); +const envPath = resolve(apiRoot, '.env'); + +function loadEnvFile(path: string) { + if (!existsSync(path)) return; + for (const line of readFileSync(path, 'utf8').split(/\r?\n/)) { + const trimmed = line.trim(); + if (!trimmed || trimmed.startsWith('#')) continue; + const eq = trimmed.indexOf('='); + if (eq <= 0) continue; + const key = trimmed.slice(0, eq).trim(); + let value = trimmed.slice(eq + 1).trim(); + if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) { + value = value.slice(1, -1); + } + if (process.env[key] === undefined) process.env[key] = value; + } +} + +loadEnvFile(envPath); + +const { + OSS_ACCESS_KEY_ID: accessKeyId, + OSS_ACCESS_KEY_SECRET: accessKeySecret, + OSS_BUCKET: bucket, + OSS_REGION: region = 'oss-cn-hangzhou', + OSS_CORS_ORIGINS, +} = process.env; + +if (!accessKeyId || !accessKeySecret || !bucket) { + console.error('请在 server/dukang-api/.env 配置 OSS_ACCESS_KEY_ID、OSS_ACCESS_KEY_SECRET、OSS_BUCKET'); + process.exit(1); +} + +const defaultOrigins = [ + 'http://localhost:5173', + 'http://localhost:5174', + 'http://localhost:5175', + 'http://127.0.0.1:5173', + 'http://127.0.0.1:5174', + 'http://127.0.0.1:5175', +]; + +const allowedOrigin = (OSS_CORS_ORIGINS ?? defaultOrigins.join(',')) + .split(',') + .map((s) => s.trim()) + .filter(Boolean); + +const client = new OSS({ + region, + accessKeyId, + accessKeySecret, + bucket, +}); + +const rules = [ + { + allowedOrigin, + allowedMethod: ['GET', 'POST', 'PUT', 'HEAD'], + allowedHeader: ['*'], + exposeHeader: ['ETag', 'x-oss-request-id'], + maxAgeSeconds: 600, + }, +]; + +console.log(`配置 Bucket「${bucket}」CORS,允许来源:`); +for (const origin of allowedOrigin) console.log(` - ${origin}`); + +await client.putBucketCORS(bucket, rules); +console.log('CORS 规则已写入。若 H5 端仍直传 OSS,请确认来源域名已包含在列表中。'); diff --git a/server/dukang-api/.env.example b/server/dukang-api/.env.example index 041c185..d89f86c 100644 --- a/server/dukang-api/.env.example +++ b/server/dukang-api/.env.example @@ -23,7 +23,7 @@ WX_MCH_PRIVATE_KEY= WX_API_V3_KEY= WX_PAY_NOTIFY_URL=https://your-domain.com/api/v1/callbacks/wechat/pay -# 阿里云 OSS(OSS_ENABLED=true 且下方密钥齐全时生效;否则 Mock 占位 URL) +# 阿里云 OSS(ali-oss@6.x;OSS_ENABLED=true 且下方密钥齐全时生效;否则 Mock 占位 URL) # RAM 用户需具备 PutObject 权限;可在控制台 Bucket 授权策略中为该 RAM UID 授予读写 # 文档:https://help.aliyun.com/zh/oss/user-guide/use-bucket-policy-to-grant-permission-to-access-oss/ OSS_ENABLED=false @@ -31,6 +31,10 @@ OSS_ACCESS_KEY_ID= OSS_ACCESS_KEY_SECRET= OSS_BUCKET= OSS_REGION=oss-cn-hangzhou +# 可选:自定义 Endpoint(如内网 oss-cn-hangzhou-internal.aliyuncs.com) +OSS_ENDPOINT= +# 可选:开启 V4 签名(需同步调整前端 PostObject 字段,默认 false 使用 V1) +OSS_AUTHORIZATION_V4=false # 公网访问域名:自定义 CDN 或 https://{bucket}.{region}.aliyuncs.com OSS_CDN_BASE= # 直传对象前缀(默认 uploads) @@ -38,4 +42,6 @@ OSS_UPLOAD_PREFIX=uploads # 直传凭证有效期(秒,默认 900) OSS_UPLOAD_EXPIRE_SECONDS=900 # 单文件大小上限(字节,默认 10MB) -OSS_MAX_UPLOAD_BYTES=10485760 \ No newline at end of file +OSS_MAX_UPLOAD_BYTES=10485760 +# 浏览器直传 OSS 时需配置 Bucket CORS;运行 pnpm oss:cors 或控制台手动添加 +# OSS_CORS_ORIGINS=http://localhost:5173,http://localhost:5174,http://localhost:5175,https://your-domain.com \ No newline at end of file diff --git a/server/dukang-api/package.json b/server/dukang-api/package.json index fea1968..639fa59 100644 --- a/server/dukang-api/package.json +++ b/server/dukang-api/package.json @@ -24,6 +24,7 @@ "@nestjs/jwt": "^10.2.0", "@nestjs/platform-express": "^10.4.0", "@prisma/client": "^5.18.0", + "ali-oss": "^6.23.0", "bullmq": "^5.12.0", "class-transformer": "^0.5.1", "class-validator": "^0.14.1", @@ -35,7 +36,9 @@ "devDependencies": { "@nestjs/cli": "^10.4.0", "@nestjs/schematics": "^10.1.0", + "@types/ali-oss": "^6.23.3", "@types/express": "^4.17.21", + "@types/multer": "^2.1.0", "@types/node": "^20.14.0", "prisma": "^5.18.0", "ts-node": "^10.9.2", diff --git a/server/dukang-api/src/integrations/oss/oss.aliyun.client.ts b/server/dukang-api/src/integrations/oss/oss.aliyun.client.ts new file mode 100644 index 0000000..83ae77d --- /dev/null +++ b/server/dukang-api/src/integrations/oss/oss.aliyun.client.ts @@ -0,0 +1,25 @@ +import OSS = require('ali-oss'); + +export type AliyunOssClientConfig = { + accessKeyId: string; + accessKeySecret: string; + bucket: string; + region: string; + endpoint?: string; + authorizationV4?: boolean; +}; + +export function createAliyunOssClient(config: AliyunOssClientConfig): OSS { + return new OSS({ + region: config.region, + accessKeyId: config.accessKeyId, + accessKeySecret: config.accessKeySecret, + bucket: config.bucket, + ...(config.endpoint ? { endpoint: config.endpoint } : {}), + ...(config.authorizationV4 ? { authorizationV4: true } : {}), + }); +} + +export function resolveOssUploadHost(bucket: string, region: string): string { + return `https://${bucket}.${region}.aliyuncs.com`; +} diff --git a/server/dukang-api/src/integrations/oss/oss.aliyun.provider.ts b/server/dukang-api/src/integrations/oss/oss.aliyun.provider.ts index 59596f9..3557b26 100644 --- a/server/dukang-api/src/integrations/oss/oss.aliyun.provider.ts +++ b/server/dukang-api/src/integrations/oss/oss.aliyun.provider.ts @@ -1,7 +1,13 @@ -import { createHmac } from 'crypto'; import { Injectable } from '@nestjs/common'; -import { randomUUID } from 'crypto'; -import type { IOssProvider, OssUploadTokenInput, OssUploadTokenResult } from './oss.interface'; +import type { + IOssProvider, + OssPutObjectInput, + OssPutObjectResult, + OssUploadTokenInput, + OssUploadTokenResult, +} from './oss.interface'; +import { buildOssObjectKey } from './oss.key.util'; +import { createAliyunOssClient, resolveOssUploadHost } from './oss.aliyun.client'; const DEFAULT_EXPIRE_SECONDS = 15 * 60; const DEFAULT_MAX_BYTES = 10 * 1024 * 1024; @@ -13,43 +19,62 @@ export class OssAliyunProvider implements IOssProvider { private readonly bucket = process.env.OSS_BUCKET ?? ''; private readonly region = process.env.OSS_REGION ?? 'oss-cn-hangzhou'; private readonly cdnBase = process.env.OSS_CDN_BASE ?? ''; + private readonly endpoint = process.env.OSS_ENDPOINT ?? ''; private readonly uploadPrefix = (process.env.OSS_UPLOAD_PREFIX ?? 'uploads').replace(/\/$/, ''); private readonly expireSeconds = Number(process.env.OSS_UPLOAD_EXPIRE_SECONDS ?? DEFAULT_EXPIRE_SECONDS); private readonly maxUploadBytes = Number(process.env.OSS_MAX_UPLOAD_BYTES ?? DEFAULT_MAX_BYTES); + private readonly authorizationV4 = process.env.OSS_AUTHORIZATION_V4 === 'true'; + private client: ReturnType | null = null; isEnabled() { return !!(this.accessKeyId && this.accessKeySecret && this.bucket); } - buildPublicUrl(ossKey: string) { - const key = ossKey.replace(/^\//, ''); - if (this.cdnBase) { - return `${this.cdnBase.replace(/\/$/, '')}/${key}`; - } - return `https://${this.bucket}.${this.region}.aliyuncs.com/${key}`; - } - - getUploadToken(dto: OssUploadTokenInput): OssUploadTokenResult { + private getClient() { if (!this.isEnabled()) { throw new Error('OSS credentials are not configured'); } + if (!this.client) { + this.client = createAliyunOssClient({ + accessKeyId: this.accessKeyId, + accessKeySecret: this.accessKeySecret, + bucket: this.bucket, + region: this.region, + endpoint: this.endpoint || undefined, + authorizationV4: this.authorizationV4, + }); + } + return this.client; + } - const ext = dto.fileName.includes('.') ? dto.fileName.split('.').pop() : 'bin'; - const dir = `${this.uploadPrefix}/${dto.bizType.toLowerCase()}/`; - const ossKey = `${dir}${Date.now()}-${randomUUID().slice(0, 8)}.${ext}`; + buildPublicUrl(ossKey: string) { + const key = ossKey.replace(/^\//, ''); + const client = this.isEnabled() ? this.getClient() : null; + if (client) { + return client.generateObjectUrl(key, this.cdnBase || undefined); + } + if (this.cdnBase) { + return `${this.cdnBase.replace(/\/$/, '')}/${key}`; + } + return `${resolveOssUploadHost(this.bucket, this.region)}/${key}`; + } + + getUploadToken(dto: OssUploadTokenInput): OssUploadTokenResult { + const client = this.getClient(); + const ossKey = buildOssObjectKey(this.uploadPrefix, dto.bizType, dto.fileName); const expireAt = new Date(Date.now() + this.expireSeconds * 1000); - const host = `https://${this.bucket}.${this.region}.aliyuncs.com`; + const host = resolveOssUploadHost(this.bucket, this.region); const policy = { expiration: expireAt.toISOString(), conditions: [ ['content-length-range', 0, this.maxUploadBytes], ['eq', '$bucket', this.bucket], - ['starts-with', '$key', dir], + ['starts-with', '$key', `${this.uploadPrefix}/${dto.bizType.toLowerCase()}/`], ], }; - const policyBase64 = Buffer.from(JSON.stringify(policy)).toString('base64'); - const signature = createHmac('sha1', this.accessKeySecret).update(policyBase64).digest('base64'); + + const signed = client.calculatePostSignature(policy); return { bucket: this.bucket, @@ -61,9 +86,24 @@ export class OssAliyunProvider implements IOssProvider { mediaType: dto.mediaType, bizType: dto.bizType, host, - policy: policyBase64, - signature, - accessKeyId: this.accessKeyId, + policy: signed.policy, + signature: signed.Signature, + accessKeyId: signed.OSSAccessKeyId, + }; + } + + async putObject(input: OssPutObjectInput): Promise { + const client = this.getClient(); + const ossKey = buildOssObjectKey(this.uploadPrefix, input.bizType, input.fileName); + await client.put(ossKey, input.buffer, { + mime: input.mimeType || 'application/octet-stream', + }); + return { + bucket: this.bucket, + region: this.region, + ossKey, + url: this.buildPublicUrl(ossKey), + mock: false, }; } } diff --git a/server/dukang-api/src/integrations/oss/oss.interface.ts b/server/dukang-api/src/integrations/oss/oss.interface.ts index f422743..6de878c 100644 --- a/server/dukang-api/src/integrations/oss/oss.interface.ts +++ b/server/dukang-api/src/integrations/oss/oss.interface.ts @@ -23,8 +23,25 @@ export interface OssUploadTokenResult { accessKeyId?: string; } +export interface OssPutObjectInput { + bizType: string; + mediaType: string; + fileName: string; + buffer: Buffer; + mimeType?: string; +} + +export interface OssPutObjectResult { + bucket: string; + region: string; + ossKey: string; + url: string; + mock: boolean; +} + export interface IOssProvider { isEnabled(): boolean; getUploadToken(input: OssUploadTokenInput): OssUploadTokenResult; + putObject(input: OssPutObjectInput): Promise; buildPublicUrl(ossKey: string): string; } diff --git a/server/dukang-api/src/integrations/oss/oss.key.util.ts b/server/dukang-api/src/integrations/oss/oss.key.util.ts new file mode 100644 index 0000000..c098278 --- /dev/null +++ b/server/dukang-api/src/integrations/oss/oss.key.util.ts @@ -0,0 +1,7 @@ +import { randomUUID } from 'crypto'; + +export function buildOssObjectKey(uploadPrefix: string, bizType: string, fileName: string): string { + const ext = fileName.includes('.') ? fileName.split('.').pop() : 'bin'; + const dir = `${uploadPrefix.replace(/\/$/, '')}/${bizType.toLowerCase()}/`; + return `${dir}${Date.now()}-${randomUUID().slice(0, 8)}.${ext}`; +} diff --git a/server/dukang-api/src/integrations/oss/oss.mock.provider.ts b/server/dukang-api/src/integrations/oss/oss.mock.provider.ts index d858f28..c3bee2a 100644 --- a/server/dukang-api/src/integrations/oss/oss.mock.provider.ts +++ b/server/dukang-api/src/integrations/oss/oss.mock.provider.ts @@ -1,6 +1,12 @@ import { Injectable } from '@nestjs/common'; -import { randomUUID } from 'crypto'; -import type { IOssProvider, OssUploadTokenInput, OssUploadTokenResult } from './oss.interface'; +import type { + IOssProvider, + OssPutObjectInput, + OssPutObjectResult, + OssUploadTokenInput, + OssUploadTokenResult, +} from './oss.interface'; +import { buildOssObjectKey } from './oss.key.util'; @Injectable() export class OssMockProvider implements IOssProvider { @@ -17,8 +23,7 @@ export class OssMockProvider implements IOssProvider { } getUploadToken(dto: OssUploadTokenInput): OssUploadTokenResult { - const ext = dto.fileName.includes('.') ? dto.fileName.split('.').pop() : 'bin'; - const key = `uploads/${dto.bizType.toLowerCase()}/${Date.now()}-${randomUUID().slice(0, 8)}.${ext}`; + const key = buildOssObjectKey('uploads', dto.bizType, dto.fileName); return { bucket: this.bucket, region: this.region, @@ -30,4 +35,15 @@ export class OssMockProvider implements IOssProvider { bizType: dto.bizType, }; } + + async putObject(input: OssPutObjectInput): Promise { + const ossKey = buildOssObjectKey('uploads', input.bizType, input.fileName); + return { + bucket: this.bucket, + region: this.region, + ossKey, + url: this.buildPublicUrl(ossKey), + mock: true, + }; + } } diff --git a/server/dukang-api/src/modules/common/dto/common-mutate.dto.ts b/server/dukang-api/src/modules/common/dto/common-mutate.dto.ts index 45a240d..b9dd1e9 100644 --- a/server/dukang-api/src/modules/common/dto/common-mutate.dto.ts +++ b/server/dukang-api/src/modules/common/dto/common-mutate.dto.ts @@ -14,6 +14,16 @@ export class UploadTokenDto { fileName: string; } +export class UploadFileDto { + @IsString() + @IsNotEmpty() + bizType: string; + + @IsString() + @IsIn(['IMAGE', 'VIDEO', 'FILE']) + mediaType: string; +} + export class RegisterResourceDto { @IsString() @IsNotEmpty() diff --git a/server/dukang-api/src/modules/common/resource.controller.ts b/server/dukang-api/src/modules/common/resource.controller.ts index c46b062..b541f7c 100644 --- a/server/dukang-api/src/modules/common/resource.controller.ts +++ b/server/dukang-api/src/modules/common/resource.controller.ts @@ -1,8 +1,23 @@ -import { Body, Controller, Delete, Get, Param, Post, Put, Query, UseGuards } from '@nestjs/common'; +import { + Body, + Controller, + Delete, + Get, + Param, + Post, + Put, + Query, + UploadedFile, + UseGuards, + UseInterceptors, +} from '@nestjs/common'; +import { FileInterceptor } from '@nestjs/platform-express'; import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard'; import { ResourceService } from './resource.service'; import { ResourceListQueryDto } from './dto/common-query.dto'; -import { RegisterResourceDto, UpdateResourceDto, UploadTokenDto } from './dto/common-mutate.dto'; +import { RegisterResourceDto, UpdateResourceDto, UploadFileDto, UploadTokenDto } from './dto/common-mutate.dto'; + +const DEFAULT_MAX_BYTES = 10 * 1024 * 1024; @Controller('common/resources') @UseGuards(JwtAuthGuard) @@ -14,6 +29,16 @@ export class ResourceController { return this.service.getUploadToken(dto); } + @Post('upload') + @UseInterceptors( + FileInterceptor('file', { + limits: { fileSize: Number(process.env.OSS_MAX_UPLOAD_BYTES ?? DEFAULT_MAX_BYTES) }, + }), + ) + upload(@UploadedFile() file: Express.Multer.File, @Body() dto: UploadFileDto) { + return this.service.uploadFile(file, dto); + } + @Post() register(@Body() dto: RegisterResourceDto) { return this.service.register(dto); diff --git a/server/dukang-api/src/modules/common/resource.service.ts b/server/dukang-api/src/modules/common/resource.service.ts index a0476f2..8b304bc 100644 --- a/server/dukang-api/src/modules/common/resource.service.ts +++ b/server/dukang-api/src/modules/common/resource.service.ts @@ -1,4 +1,4 @@ -import { Inject, Injectable, NotFoundException } from '@nestjs/common'; +import { BadRequestException, Inject, Injectable, NotFoundException } from '@nestjs/common'; import type { ResourceBizType, ResourceMediaType, ResourceOwnerType } from '@prisma/client'; import { Prisma } from '@prisma/client'; import { PrismaService } from '../../common/prisma/prisma.module'; @@ -6,7 +6,9 @@ import { serializeBigInt } from '../../common/decorators/current-user.decorator' import { OSS_PROVIDER } from '../../integrations/integrations.constants'; import type { IOssProvider } from '../../integrations/oss/oss.interface'; import type { ResourceListQueryDto } from './dto/common-query.dto'; -import type { RegisterResourceDto, UpdateResourceDto, UploadTokenDto } from './dto/common-mutate.dto'; +import type { RegisterResourceDto, UpdateResourceDto, UploadFileDto, UploadTokenDto } from './dto/common-mutate.dto'; + +const DEFAULT_MAX_BYTES = 10 * 1024 * 1024; @Injectable() export class ResourceService { @@ -19,6 +21,23 @@ export class ResourceService { return this.oss.getUploadToken(dto); } + async uploadFile(file: Express.Multer.File | undefined, dto: UploadFileDto) { + if (!file) { + throw new BadRequestException('请选择要上传的文件'); + } + const maxUploadBytes = Number(process.env.OSS_MAX_UPLOAD_BYTES ?? DEFAULT_MAX_BYTES); + if (file.size > maxUploadBytes) { + throw new BadRequestException(`文件不能超过 ${Math.floor(maxUploadBytes / 1024 / 1024)}MB`); + } + return this.oss.putObject({ + bizType: dto.bizType, + mediaType: dto.mediaType, + fileName: file.originalname || 'upload.bin', + buffer: file.buffer, + mimeType: file.mimetype, + }); + } + async register(dto: RegisterResourceDto) { const resource = await this.prisma.commonResource.create({ data: {