Parcourir la source

fix: 将所有的上传流程,改为 AI获取令牌 =>直传OSS =>AI审核

sgc il y a 3 semaines
Parent
commit
3ba8abd0e7

+ 8 - 53
src/api/modules/aiImageUpload.ts

@@ -1,6 +1,7 @@
 /**
- * Web 端:Tus 分片上传(与 Apifox / uni 版协议对齐);独立审核走上传服务 /upload/simple 等,不再调用 verify 审核域
+ * Web 端:Tus 分片上传(与 Apifox / uni 版协议对齐);简单直传走 @/api/upload.js OSS STS + finalize
  */
+import { uploadFileToOssWithDownloadMeta } from "@/api/upload.js";
 import { useUserStore } from "@/stores/modules/user";
 import { AI_UPLOAD_FILES_PUBLIC_BASE, BASE_AI_URL, BASE_DEV_UPLOAD_SIMPLE } from "@/utils/config";
 import { withSimpleUploadOverlay } from "@/utils/withSimpleUploadOverlay";
@@ -128,60 +129,14 @@ function buildDevSimpleUploadRequestUrl(): string {
 }
 
 /**
- * 官方相册等:multipart 直传(替代 Tus `POST .../upload`)
- * POST `/dev-upload-ailien/upload/simple`,表单字段 `file`
+ * 官方相册等:GET sts-token → OSS 直传 → POST finalize(替代 Tus / dev-upload simple)
  */
 export async function uploadFileViaDevSimpleEndpoint(file: File): Promise<{ fileUrl: string; coverUrl?: string }> {
-  return withSimpleUploadOverlay(async signal => {
-    const reqUrl = buildDevSimpleUploadRequestUrl();
-    const fd = new FormData();
-    fd.append("file", file, file.name);
-
-    const res = await fetch(reqUrl, {
-      method: "POST",
-      headers: {
-        Authorization: authHeader()
-      },
-      body: fd,
-      credentials: "omit",
-      signal
-    });
-
-    let body: unknown = null;
-    const ct = res.headers.get("content-type") || "";
-    if (ct.includes("application/json")) {
-      try {
-        body = await res.json();
-      } catch {
-        body = null;
-      }
-    } else {
-      const t = await res.text();
-      try {
-        body = t ? JSON.parse(t) : null;
-      } catch {
-        body = t ? { raw: t } : null;
-      }
-    }
-
-    if (res.status < 200 || res.status >= 300) {
-      const msg =
-        body && typeof body === "object" && (body as any).msg != null ? String((body as any).msg) : `上传失败(${res.status})`;
-      throw new Error(msg);
-    }
-    if (body && typeof body === "object" && (body as any).code !== undefined) {
-      const c = (body as any).code;
-      if (c !== 200 && c !== 0) {
-        throw new Error((body as any).msg || (body as any).message || "上传失败");
-      }
-    }
-
-    const rawUrl = pickFileUrlFromBody(body);
-    if (!rawUrl) {
-      throw new Error("上传完成但未返回文件地址");
-    }
-    return normalizeSimpleUploadUrls(body, rawUrl);
-  });
+  const { fileUrl } = await uploadFileToOssWithDownloadMeta(file);
+  if (!fileUrl?.trim()) {
+    throw new Error("上传完成但未返回文件地址");
+  }
+  return { fileUrl: fileUrl.trim() };
 }
 
 /** 创建上传会话 POST /upload */

+ 1 - 1
src/api/modules/newLoginApi.ts

@@ -62,7 +62,7 @@ export const getInputPrompt = params => {
 export const getDistrict = params => {
   return httpLogin.get(`/alienStore/gaode/getDistrict`, params);
 };
-// 文件上传(统一走 @/api/upload.js → /upload/simple
+// 文件上传(统一走 @/api/upload.js → OSS STS + finalize 审核
 export const uploadImg = (params: FormData) => uploadFormDataSimpleCompat(params);
 // 发布/更新动态(新接口)
 export const addOrUpdateDynamic = (params: {

+ 1 - 1
src/api/modules/storeDecoration.ts

@@ -63,7 +63,7 @@ export const saveOrUpdateDecoration = (params: any) => {
   return httpApi.post(`/alienStore/renovation/requirement/saveOrUpdate`, params);
 };
 
-// 上传房屋图纸(统一走 @/api/upload.js → /upload/simple
+// 上传房屋图纸(统一走 @/api/upload.js → OSS STS + finalize 审核
 export const uploadDecorationImage = (params: FormData) => uploadFormDataSimpleCompat(params);
 
 // 聊天图片/视频上传

+ 1 - 1
src/api/modules/upload.ts

@@ -1,7 +1,7 @@
 import { uploadFormDataSimpleCompat } from "@/api/upload.js";
 
 /**
- * @name 文件上传模块(图片/视频统一走 @/api/upload.js → /upload/simple
+ * @name 文件上传模块(图片/视频统一走 @/api/upload.js → OSS STS + finalize 审核
  * @param options 透传 `uploadFormDataSimpleCompat`(如 `skipSimpleUploadOverlay`),多图串行上传时可避免重复全局弹层
  */
 export const uploadImg = (params: FormData, options?: Record<string, unknown>) =>

+ 36 - 78
src/api/upload.js

@@ -521,7 +521,8 @@ async function fetchOssStsToken(fetchOptions = {}) {
 function buildOssObjectKey(prefix, file) {
   const dir = String(prefix || "uploads/").replace(/^\/+/, "");
   const normalizedDir = dir.endsWith("/") ? dir : `${dir}/`;
-  const ext = getFileExtension(file.name, "image");
+  const fileKind = String(file?.type || "").startsWith("video/") ? "video" : "image";
+  const ext = getFileExtension(file.name, fileKind);
   const safeName = (file.name || "file").replace(/[/\\?%*:|"<>]/g, "_").replace(/\s+/g, "_");
   const base = safeName.includes(".") ? safeName.slice(0, safeName.lastIndexOf(".")) : safeName;
   return `${normalizedDir}${Date.now()}_${Math.random().toString(36).slice(2, 10)}_${base || "file"}.${ext}`;
@@ -668,12 +669,25 @@ async function requestOssFinalizeAudit(file, objectKey, fetchOptions = {}) {
 }
 
 /**
- * 官方相册:先 GET /upload/oss/sts-token,再前端直传 OSS
+ * STS 直传 OSS 后 POST finalize 审核(单文件核心,无弹层)
+ * @param {File} file
+ * @param {{ signal?: AbortSignal }} [fetchOptions]
+ * @returns {Promise<{ fileUrl: string; downloadUrl: string }>}
+ */
+async function putFileToOssWithStsAndFinalize(file, fetchOptions = {}) {
+  const uploaded = await putFileToOssWithSts(file, fetchOptions);
+  const audit = await requestOssFinalizeAudit(file, uploaded.objectKey, fetchOptions);
+  const downloadUrl = audit.downloadUrl?.trim() || uploaded.downloadUrl;
+  return { fileUrl: uploaded.fileUrl, downloadUrl };
+}
+
+/**
+ * 单文件 OSS + 审核,带可选全局上传弹层
  * @param {File} file
  * @param {{ showLoading?: boolean; skipSimpleUploadOverlay?: boolean; uploadSuccessMessage?: string | null; uploadOverlayTitle?: string }} [options]
  * @returns {Promise<{ fileUrl: string; downloadUrl: string }>}
  */
-export async function uploadFileToOssStsWithDownloadMeta(file, options = {}) {
+async function uploadSingleFileWithOssOverlay(file, options = {}) {
   const { showLoading = false, skipSimpleUploadOverlay = false, uploadSuccessMessage, uploadOverlayTitle } = options;
   if (!(file instanceof File)) {
     throw new Error("请选择要上传的文件");
@@ -686,13 +700,7 @@ export async function uploadFileToOssStsWithDownloadMeta(file, options = {}) {
   }
 
   try {
-    const runUpload = async signal => {
-      const fetchOpts = signal ? { signal } : {};
-      const uploaded = await putFileToOssWithSts(file, fetchOpts);
-      const audit = await requestOssFinalizeAudit(file, uploaded.objectKey, fetchOpts);
-      const downloadUrl = audit.downloadUrl?.trim() || uploaded.downloadUrl;
-      return { fileUrl: uploaded.fileUrl, downloadUrl };
-    };
+    const runUpload = async signal => putFileToOssWithStsAndFinalize(file, signal ? { signal } : {});
 
     let result;
     if (skipSimpleUploadOverlay) {
@@ -731,6 +739,19 @@ export async function uploadFileToOssStsWithDownloadMeta(file, options = {}) {
 }
 
 /**
+ * GET sts-token → OSS 直传 → POST finalize 审核
+ * @param {File} file
+ * @param {{ showLoading?: boolean; skipSimpleUploadOverlay?: boolean; uploadSuccessMessage?: string | null; uploadOverlayTitle?: string }} [options]
+ * @returns {Promise<{ fileUrl: string; downloadUrl: string }>}
+ */
+export async function uploadFileToOssStsWithDownloadMeta(file, options = {}) {
+  return uploadSingleFileWithOssOverlay(file, options);
+}
+
+/** 与 uploadFileToOssStsWithDownloadMeta 相同 */
+export const uploadFileToOssWithDownloadMeta = uploadFileToOssStsWithDownloadMeta;
+
+/**
  * POST multipart:字段 file;解析展示用地址与业务保存用的 download_url
  * @param {File} file
  * @param {{ signal?: AbortSignal }} [fetchOptions]
@@ -852,14 +873,10 @@ async function postFileToSimpleUpload(file, fetchOptions = {}) {
 }
 
 /**
- * 上传文件:图片与视频均走同一接口 POST /upload/simple,formData 键 file。
+ * 上传文件:GET sts-token → OSS 直传 → POST finalize 审核
  * @param {File | File[] | FileList} files 浏览器文件对象;支持单个 File、数组或 FileList
  * @param {string} [_fileType] 保留参数,兼容旧调用(当前不参与分支)
  * @param {{ showLoading?: boolean; skipSimpleUploadOverlay?: boolean; uploadSuccessMessage?: string | null; uploadOverlayTitle?: string }} [options]
- *   showLoading:在未使用全局上传弹层时,用 ElMessage 提示上传中
- *   skipSimpleUploadOverlay:为 true 时不展示 PopupLoading(不弹「上传成功」)
- *   uploadSuccessMessage:传给弹层,`null` 表示上传成功不 toast(默认「上传成功」)
- *   uploadOverlayTitle:弹层标题
  * @returns {Promise<string[]>} 上传成功后的文件 URL 列表
  */
 export async function uploadFilesToOss(files, _fileType, options = {}) {
@@ -878,9 +895,10 @@ export async function uploadFilesToOss(files, _fileType, options = {}) {
   try {
     const runUpload = async signal => {
       const uploadedUrls = [];
+      const fetchOpts = signal ? { signal } : {};
       for (const file of fileArr) {
-        const url = await postFileToSimpleUpload(file, signal ? { signal } : {});
-        uploadedUrls.push(url);
+        const { fileUrl, downloadUrl } = await putFileToOssWithStsAndFinalize(file, fetchOpts);
+        uploadedUrls.push(downloadUrl || fileUrl);
       }
       return uploadedUrls;
     };
@@ -922,66 +940,6 @@ export async function uploadFilesToOss(files, _fileType, options = {}) {
 }
 
 /**
- * 单文件上传:返回列表展示用地址与业务保存用的 `download_url`(响应中无则为空字符串)
- * @param {File} file
- * @param {{ showLoading?: boolean; skipSimpleUploadOverlay?: boolean; uploadSuccessMessage?: string | null; uploadOverlayTitle?: string }} [options]
- * @returns {Promise<{ fileUrl: string; downloadUrl: string }>}
- */
-export async function uploadFileToOssWithDownloadMeta(file, options = {}) {
-  const { showLoading = false, skipSimpleUploadOverlay = false, uploadSuccessMessage, uploadOverlayTitle } = options;
-  if (!(file instanceof File)) {
-    throw new Error("请选择要上传的文件");
-  }
-
-  let closeLoading = () => {};
-  if (showLoading && skipSimpleUploadOverlay) {
-    const loading = ElMessage({ message: "上传中...", type: "info", duration: 0, showClose: false });
-    closeLoading = () => loading.close();
-  }
-
-  try {
-    const runUpload = async signal => {
-      const { url, downloadUrl } = await postFileToSimpleUploadDetailed(file, signal ? { signal } : {});
-      return { fileUrl: url, downloadUrl };
-    };
-
-    let result;
-    if (skipSimpleUploadOverlay) {
-      const overlaySignal = useSimpleUploadOverlayStore().getActiveAbortSignal?.();
-      result = await runUpload(overlaySignal ?? null);
-    } else {
-      const overlayOpts =
-        uploadSuccessMessage !== undefined || uploadOverlayTitle
-          ? {
-              title: uploadOverlayTitle,
-              successMessage: uploadSuccessMessage
-            }
-          : undefined;
-      result = await withSimpleUploadOverlay(signal => runUpload(signal), overlayOpts);
-    }
-
-    closeLoading();
-    return result;
-  } catch (e) {
-    closeLoading();
-    console.error("上传失败", e);
-    if (isUploadUserCancelledError(e)) {
-      throw e;
-    }
-    const msg = e?.message || "上传失败";
-    ElMessage.error(msg);
-    try {
-      if (e && typeof e === "object") {
-        Object.defineProperty(e, "__uploadMessageShown", { value: true, enumerable: false, configurable: true });
-      }
-    } catch (_) {
-      /* ignore */
-    }
-    throw e;
-  }
-}
-
-/**
  * 上传单个文件,返回 URL 字符串
  * @param {File} file
  * @param {string} [fileType]
@@ -1016,7 +974,7 @@ export async function uploadFormDataToOss(formData, fileType, options = {}) {
 }
 
 /**
- * FormData 走 `/upload/simple` 后,包装成旧组件习惯的 `{ code, msg, data: { fileUrl }, fileUrl }`(并非请求 `/file/uploadMore`)
+ * FormData OSS 上传后,包装成旧组件习惯的 `{ code, msg, data: { fileUrl }, fileUrl }`
  * @param {FormData} formData
  * @param {{ showLoading?: boolean }} [options]
  * @returns {Promise<{ code: number; msg: string; data: { fileUrl: string }; fileUrl: string }>}

+ 1 - 1
src/components/Upload/Imgs.vue

@@ -158,7 +158,7 @@ const uploadingFiles = new Set<string | number>();
 let hasShownSuccessNotification = false;
 
 /**
- * 多选时 el-upload 会对每个文件并发触发 http-request,易导致 /upload/simple 并发报错
+ * 多选时 el-upload 会对每个文件并发触发 http-request,通过 Promise 链串行化 OSS 上传
  * 通过 Promise 链串行化,同一时间只发起一次上传请求。
  */
 let sequentialUploadTail = Promise.resolve();

+ 1 - 1
src/utils/config.ts

@@ -13,7 +13,7 @@ export const AI_UPLOAD_FILES_PUBLIC_BASE = trimSlash(
 );
 
 /**
- * 官方相册视频等:POST multipart 至 `/dev-upload-ailien/upload/simple`(非 Tus)
+ * 上传:GET /upload/oss/sts-token → OSS 直传 → POST /upload/oss/finalize 审核
  * 不配时:开发环境请求同源相对路径(需在 VITE_PROXY 中把 `/dev-upload-ailien` 指到上传服务);生产默认 upload.ailien.shop:8443
  */
 export const BASE_DEV_UPLOAD_SIMPLE = trimSlash(

+ 1 - 1
src/utils/withSimpleUploadOverlay.ts

@@ -6,7 +6,7 @@ function sleep(ms: number) {
 }
 
 /**
- * 使用全局 PopupLoading 包裹「/upload/simple」类上传;成功后提示「上传成功」。
+ * 使用全局 PopupLoading 包裹 OSS 上传;成功后提示「上传成功」。
  * 取消(AbortError)不弹成功提示;失败时关闭弹层,由调用方决定是否 ElMessage.error。
  * `successMessage === null` 时不弹成功提示(用于上传后还要继续审核等场景)。
  */

+ 2 - 2
src/views/storeDecoration/officialPhotoAlbum/index.vue

@@ -119,7 +119,7 @@ import {
   getOfficialImgList,
   deleteOfficialImg
 } from "@/api/modules/storeDecoration";
-import { uploadFileToOssStsWithDownloadMeta } from "@/api/upload.js";
+import { uploadFileToOssWithDownloadMeta } from "@/api/upload.js";
 import PcImagePreviewViewer from "@/components/pcMediaPreview/PcImagePreviewViewer.vue";
 
 /** 普通官方相册图 */
@@ -276,7 +276,7 @@ const customImageUploadApi = async (formData: FormData, options?: Record<string,
     throw error;
   }
 
-  const { fileUrl, downloadUrl } = await uploadFileToOssStsWithDownloadMeta(file, options ?? {});
+  const { fileUrl, downloadUrl } = await uploadFileToOssWithDownloadMeta(file, options ?? {});
   if (!downloadUrl?.trim()) {
     throw new Error("上传成功但未返回 download_url,无法保存相册");
   }