Просмотр исходного кода

Merge remote-tracking branch 'origin/development' into uat

LuTong 2 дней назад
Родитель
Сommit
5ba9e7bb0b

+ 2 - 3
.env.development

@@ -24,9 +24,8 @@ VITE_API_URL_PLATFORM = /api/alienStorePlatform
 
 # 开发环境跨域代理,支持配置多个
 # VITE_PROXY = [["/api","https://api.ailien.shop"]] #生产环境
-# /ai-upload、/ai-moderate 供 Tus 上传与 AI 审核(见 src/utils/config.ts、src/api/modules/aiImageUpload.ts)
-# 图片上传默认走 AI 审核;若需恢复仅 OSS/uploadMore,设置 VITE_UPLOAD_IMAGE_AI_MODERATE=false
-VITE_PROXY = [["/api","http://120.26.186.130:8000"],["/ai-upload","https://upload.ailien.shop:8443"],["/ai-moderate","https://verify.ailien.shop:8444"]] # 邹建宇
+# /ai-upload 供 Tus 上传(见 src/utils/config.ts、src/api/modules/aiImageUpload.ts)
+VITE_PROXY = [["/api","http://120.26.186.130:8000"],["/ai-upload","https://upload.ailien.shop:8443"]] # 邹建宇
 
 
 # WebSocket 基础地址(分享等能力,与商家端一致)

+ 7 - 2
.env.production

@@ -37,9 +37,14 @@ VITE_PROXY = [["/alienStore","http://120.26.186.130:8000/alienStore"]]
 # AI接口
 VITE_PROXY_AI = [["/ai-api","http://124.93.18.180:9000"]]
 
-# Tus 上传与内容审核(生产构建默认见 src/utils/config.ts;同源反代时可覆盖)
+# 上传请求:不配则走同源 /ai-upload(Nginx 反代示例)
+# location /ai-upload/ {
+#   proxy_pass https://upload.ailien.shop:8443/;
+#   proxy_set_header Host upload.ailien.shop;
+#   proxy_ssl_server_name on;
+# }
+# 仅在上传服务已配置完整 CORS 时才直连:
 # VITE_AI_UPLOAD_BASE = https://upload.ailien.shop:8443
-# VITE_AI_MODERATE_BASE = https://verify.ailien.shop:8444
 # VITE_AI_FILES_PUBLIC_BASE = https://upload.ailien.shop:8443/files
 
 # 接口加密配置

+ 124 - 370
src/api/modules/aiImageUpload.ts

@@ -1,15 +1,12 @@
 /**
- * Web 端:Tus 分片上传 + AI 图片审核(与 Apifox / uni 版协议对齐)
+ * Web 端:Tus 分片上传(与 Apifox / uni 版协议对齐);独立审核走上传服务 /upload/simple 等,不再调用 verify 审核域
  */
 import { useUserStore } from "@/stores/modules/user";
-import { ResultEnum } from "@/enums/httpEnum";
-import { AI_UPLOAD_FILES_PUBLIC_BASE, BASE_AI_MODERATE_URL, BASE_AI_URL } from "@/utils/config";
+import { AI_UPLOAD_FILES_PUBLIC_BASE, BASE_AI_URL, BASE_DEV_UPLOAD_SIMPLE } from "@/utils/config";
 
 const TUS_VERSION = "1.0.0";
 const TUS_CHUNK_SIZE = 1024 * 1024;
 
-const DEFAULT_MODERATE_USER_TIP = "图片未通过内容审核,请更换后重试";
-
 function authHeader(): string {
   try {
     return useUserStore().token || "";
@@ -18,23 +15,6 @@ function authHeader(): string {
   }
 }
 
-function mapModerateReasonToUserMessage(raw?: string | null): string {
-  const s = raw == null ? "" : String(raw).trim();
-  if (!s) return DEFAULT_MODERATE_USER_TIP;
-
-  if (/赌|赌博|casino|gambl/i.test(s)) return "图片涉及赌博等违规内容,请更换后重试";
-  if (/色情|淫秽|porn|sex/i.test(s)) return "图片涉及色情等违规内容,请更换后重试";
-  if (/暴力|血腥|violence|gore/i.test(s)) return "图片涉及暴力等违规内容,请更换后重试";
-  if (/辱骂|谩骂|人身攻击|abuse|insult/i.test(s)) return "图片涉及不文明内容,请更换后重试";
-  if (/政治|谣言/i.test(s)) return "图片涉及违规信息,请更换后重试";
-  if (/广告|spam|营销/i.test(s)) return "图片涉及违规推广内容,请更换后重试";
-  if (/违法|违禁|illegal/i.test(s)) return "图片含违法违规内容,请更换后重试";
-  if (/黄赌毒/i.test(s)) return "请勿上传涉黄、涉赌、涉毒等违规内容";
-  if (/审核|moderat|vlm|violation|flagged/i.test(s)) return DEFAULT_MODERATE_USER_TIP;
-
-  return DEFAULT_MODERATE_USER_TIP;
-}
-
 function pickPayload(res: unknown): unknown {
   if (res == null) return null;
   if (typeof res === "object" && !Array.isArray(res) && (res as any).data !== undefined) {
@@ -87,6 +67,119 @@ function pickFileUrlFromBody(body: unknown): string {
   return typeof d === "string" && d.startsWith("http") ? d : "";
 }
 
+/** 从 simple 上传或列表接口的 data 中取封面地址 */
+function pickCoverUrlFromBody(body: unknown): string {
+  const d = pickPayload(body);
+  let cur: any = d;
+  if (cur && typeof cur === "object" && cur.data !== undefined && !Array.isArray(cur.data)) {
+    cur = cur.data;
+  }
+  if (!cur || typeof cur !== "object") return "";
+  let cover = String(cur.cover || cur.coverUrl || cur.poster || cur.thumbnailUrl || cur.thumbnail || "").trim();
+  const imgUrlRaw = cur.imgUrl;
+  if (!cover && imgUrlRaw != null) {
+    if (typeof imgUrlRaw === "object" && imgUrlRaw !== null) {
+      const o = imgUrlRaw as Record<string, string>;
+      cover = String(o.cover || o.coverUrl || o.poster || "").trim();
+    } else if (typeof imgUrlRaw === "string") {
+      const s = imgUrlRaw.trim();
+      if (s.startsWith("{")) {
+        try {
+          const o = JSON.parse(s) as Record<string, string>;
+          cover = String(o.cover || o.coverUrl || o.poster || "").trim();
+        } catch {
+          /* ignore */
+        }
+      }
+    }
+  }
+  return cover;
+}
+
+/** simple 上传返回的 fileUrl 可能为 JSON 字符串 {"cover","video"} */
+function normalizeSimpleUploadUrls(body: unknown, fileUrl: string): { fileUrl: string; coverUrl?: string } {
+  let url = (fileUrl || "").trim();
+  let coverUrl = pickCoverUrlFromBody(body);
+  if (url.startsWith("{")) {
+    try {
+      const o = JSON.parse(url) as Record<string, string>;
+      if (o && typeof o === "object") {
+        const v = String(o.video || o.videoUrl || o.url || "").trim();
+        const c = String(o.cover || o.coverUrl || o.poster || "").trim();
+        if (v) url = v;
+        if (c && !coverUrl) coverUrl = c;
+      }
+    } catch {
+      /* keep url as-is */
+    }
+  }
+  return { fileUrl: url, coverUrl: coverUrl || undefined };
+}
+
+const DEV_SIMPLE_UPLOAD_PATH = "/dev-upload-ailien/upload/simple";
+
+function buildDevSimpleUploadRequestUrl(): string {
+  const base = String(BASE_DEV_UPLOAD_SIMPLE || "").replace(/\/$/, "");
+  if (base) {
+    return `${base}${DEV_SIMPLE_UPLOAD_PATH}`;
+  }
+  return DEV_SIMPLE_UPLOAD_PATH;
+}
+
+/**
+ * 官方相册等:multipart 直传(替代 Tus `POST .../upload`)
+ * POST `/dev-upload-ailien/upload/simple`,表单字段 `file`
+ */
+export async function uploadFileViaDevSimpleEndpoint(file: File): Promise<{ fileUrl: string; coverUrl?: string }> {
+  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"
+  });
+
+  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);
+}
+
 /** 创建上传会话 POST /upload */
 export async function createUploadSession(data: {
   filename: string;
@@ -227,232 +320,8 @@ export async function finalizeUploadSession(uploadId: string, data: Record<strin
   return body;
 }
 
-export interface ModerateImageParams {
-  text?: string;
-  image_urls?: string | string[];
-  file?: File | null;
-}
-
-/** POST multipart /api/v1/moderate,文件字段名 files */
-export async function moderateImage(params: ModerateImageParams = {}): Promise<any> {
-  const urlsStr = Array.isArray(params.image_urls)
-    ? params.image_urls.filter(Boolean).join(",")
-    : String(params.image_urls ?? "");
-
-  const fd = new FormData();
-  fd.append("text", String(params.text ?? ""));
-  fd.append("image_urls", urlsStr);
-  if (params.file) {
-    fd.append("files", params.file, params.file.name);
-  }
-
-  const res = await fetch(`${BASE_AI_MODERATE_URL}/api/v1/moderate`, {
-    method: "POST",
-    headers: {
-      Authorization: authHeader()
-    },
-    body: fd
-  });
-
-  let body: any = 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 = { raw: t };
-    }
-  }
-
-  if (res.status < 200 || res.status >= 300) {
-    throw new Error(`审核请求失败(${res.status})`);
-  }
-  if (body && typeof body === "object" && body.code !== undefined && body.code !== 200 && body.code !== 0) {
-    throw new Error(body.msg || body.message || "审核请求失败");
-  }
-  return body;
-}
-
-export interface ModerateVideoParams {
-  /** 可选;与图片同一套 Tus 上传后应只传 `video_url`,不在此再传文件 */
-  file?: File | null;
-  filePath?: string;
-  path?: string;
-  /** 服务端视频地址,无 Tus 时可传空字符串 */
-  video_path?: string;
-  /** 标题/简介等 */
-  text?: string;
-  /** 远程 http(s) 视频地址 */
-  video_url?: string;
-}
-
 /**
- * 仅提交「已上传视频」的审核任务:POST `/api/v1/video/submit`
- * 与图片共用 Tus 上传拿到地址后,传 `video_url`;`getVideoModerateResult` 查询审核进度/结果。
- */
-export async function moderateVideo(params: ModerateVideoParams = {}): Promise<any> {
-  const fd = new FormData();
-  fd.append("video_path", String(params.video_path ?? ""));
-  fd.append("text", String(params.text ?? ""));
-  fd.append("video_url", String(params.video_url ?? ""));
-
-  const file = params.file ?? null;
-  if (file) {
-    fd.append("file", file, file.name);
-  }
-
-  const res = await fetch(`${BASE_AI_MODERATE_URL}/api/v1/video/submit`, {
-    method: "POST",
-    headers: {
-      Authorization: authHeader()
-    },
-    body: fd
-  });
-
-  let body: any = 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 = { raw: t };
-    }
-  }
-
-  if (res.status < 200 || res.status >= 300) {
-    throw new Error(`视频审核提交失败(${res.status})`);
-  }
-  if (body && typeof body === "object" && body.code !== undefined && body.code !== 200 && body.code !== 0) {
-    throw new Error(body.msg || body.message || "视频审核提交失败");
-  }
-  return body;
-}
-
-/**
- * 查询视频审核进度与结果:GET `/api/v1/video/status/{task_id}`
- * @param data.task_id 或传入字符串即 task_id
- */
-export async function getVideoModerateResult(data: { task_id?: string } | string): Promise<any> {
-  const taskId = typeof data === "string" ? data : String(data?.task_id ?? "").trim();
-  if (!taskId) {
-    throw new Error("缺少 task_id");
-  }
-
-  const res = await fetch(`${BASE_AI_MODERATE_URL}/api/v1/video/status/${encodeURIComponent(taskId)}`, {
-    method: "GET",
-    headers: {
-      Authorization: authHeader()
-    }
-  });
-
-  let body: any = 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 = { raw: t };
-    }
-  }
-
-  if (res.status < 200 || res.status >= 300) {
-    throw new Error(`查询视频审核状态失败(${res.status})`);
-  }
-  if (body && typeof body === "object" && body.code !== undefined && body.code !== 200 && body.code !== 0) {
-    throw new Error(body.msg || body.message || "查询视频审核状态失败");
-  }
-  return body;
-}
-
-function mapVideoModerateReasonToUserMessage(raw?: string | null): string {
-  return mapModerateReasonToUserMessage(raw).replace(/图片/g, "视频");
-}
-
-function pickVideoTaskIdFromSubmit(res: unknown): string {
-  const r = res as Record<string, any> | null | undefined;
-  if (!r || typeof r !== "object") return "";
-  let cur: any = r.data !== undefined ? r.data : r;
-  if (cur && typeof cur === "object" && cur.data !== undefined) cur = cur.data;
-  if (cur && typeof cur === "object") {
-    return String(cur.task_id ?? cur.taskId ?? cur.taskID ?? "").trim();
-  }
-  return "";
-}
-
-function flattenVideoStatusPayload(body: Record<string, any>): Record<string, any> {
-  const d = body?.data;
-  if (d && typeof d === "object" && !Array.isArray(d)) {
-    return { ...body, ...d };
-  }
-  return body;
-}
-
-function isVideoModerationStatusTerminal(payload: Record<string, any>): boolean {
-  const p = flattenVideoStatusPayload(payload);
-  const s = String(p?.status ?? p?.state ?? "").toLowerCase();
-  const terminal = [
-    "completed",
-    "complete",
-    "success",
-    "succeeded",
-    "done",
-    "finished",
-    "failed",
-    "fail",
-    "rejected",
-    "error",
-    "cancelled",
-    "canceled"
-  ];
-  if (terminal.includes(s)) return true;
-  const r = p?.result;
-  if (r && typeof r === "object" && (r.flagged !== undefined || r.passed !== undefined || r.approved !== undefined)) {
-    return true;
-  }
-  return false;
-}
-
-function isVideoModerationRejectedPayload(payload: Record<string, any>): boolean {
-  const p = flattenVideoStatusPayload(payload);
-  if (p?.flagged === true) return true;
-  const r = p?.result;
-  if (r && typeof r === "object" && r.flagged === true) return true;
-  const s = String(p?.status ?? "").toLowerCase();
-  if (["failed", "fail", "rejected", "error"].includes(s)) return true;
-  return false;
-}
-
-function pickVideoRejectReason(payload: Record<string, any>): string | null {
-  const p = flattenVideoStatusPayload(payload);
-  const r = p?.result;
-  const fromResult = r && typeof r === "object" ? (r.reason ?? r.message) : null;
-  const raw = fromResult ?? p?.reason ?? p?.message ?? p?.msg;
-  return raw != null && typeof raw === "string" ? raw : null;
-}
-
-/**
- * 是否为视频(MIME 或后缀)。上传与图片同走 Tus;通过后以 `moderateVideo({ video_url })` + 轮询区分审核链路。
+ * 是否为视频(MIME 或后缀),用于 Tus 上传命名等
  */
 export function isLikelyVideoFileForAiUpload(file: File): boolean {
   if (!(file instanceof File)) return false;
@@ -500,139 +369,26 @@ async function uploadViaTusToPublicUrl(file: File, onProgress?: (p: number) => v
   return fileUrl;
 }
 
-/**
- * Tus 已得到视频 URL:`moderateVideo` 仅提交审核(video_url),`getVideoModerateResult` 轮询至结束。
- */
-async function submitAndAwaitVideoModeration(fileUrl: string, onProgress?: (p: number) => void): Promise<void> {
-  const report = (n: number) => onProgress?.(Math.min(100, Math.max(0, Math.round(n))));
-  const submitRes: any = await moderateVideo({ video_url: fileUrl, text: "", video_path: "" });
-  report(92);
-  const taskId = pickVideoTaskIdFromSubmit(submitRes);
-  const submitFlat =
-    submitRes && typeof submitRes === "object" ? flattenVideoStatusPayload(submitRes as Record<string, any>) : {};
-
-  if (!taskId) {
-    if (isVideoModerationStatusTerminal(submitFlat)) {
-      if (isVideoModerationRejectedPayload(submitFlat)) {
-        throw new Error(mapVideoModerateReasonToUserMessage(pickVideoRejectReason(submitFlat)));
-      }
-      return;
-    }
-    const c = submitRes?.code;
-    if (c === 200 || c === 0) return;
-    throw new Error("视频审核提交未返回任务信息,请稍后重试");
-  }
-
-  const maxMs = 5 * 60 * 1000;
-  const step = 2000;
-  const t0 = Date.now();
-  let pollCount = 0;
-  while (Date.now() - t0 < maxMs) {
-    const raw: any = await getVideoModerateResult(taskId);
-    const flat = raw && typeof raw === "object" ? flattenVideoStatusPayload(raw as Record<string, any>) : {};
-    pollCount += 1;
-    report(Math.min(98, 92 + Math.min(6, pollCount)));
-
-    if (isVideoModerationStatusTerminal(flat)) {
-      if (isVideoModerationRejectedPayload(flat)) {
-        throw new Error(mapVideoModerateReasonToUserMessage(pickVideoRejectReason(flat)));
-      }
-      return;
-    }
-    await new Promise<void>(resolve => setTimeout(resolve, step));
-  }
-  throw new Error("视频审核等待超时,请稍后重试");
-}
-
 export interface UploadFileViaAiOptions {
   /** 0~100 */
   onProgress?: (progress: number) => void;
-  /** 为 true 时跳过审核(仅上传) */
+  /** 已废弃;保留仅为兼容旧调用,不再触发独立审核服务 */
   skipModerate?: boolean;
 }
 
 /**
- * 图片与视频均:Tus 上传 → finalize 得同一套访问 URL。
- * 图片再 `moderateImage`;视频再 `moderateVideo({ video_url })` + `getVideoModerateResult` 轮询。`skipModerate` 则只上传不审。
+ * 图片与视频:Tus 上传 → finalize 得访问 URL(不再调用 verify / ai-moderate)
  */
 export async function uploadFileViaAi(file: File, options: UploadFileViaAiOptions = {}): Promise<string> {
-  const { onProgress, skipModerate } = options;
+  const { onProgress } = options;
 
   const report = (p: number) => {
     onProgress?.(Math.min(100, Math.max(0, Math.round(p))));
   };
 
-  try {
-    const fileUrl = await uploadViaTusToPublicUrl(file, onProgress);
-
-    if (!skipModerate) {
-      if (isLikelyVideoFileForAiUpload(file)) {
-        await submitAndAwaitVideoModeration(fileUrl, onProgress);
-      } else {
-        const moderateRes = await moderateImage({
-          text: "",
-          image_urls: fileUrl,
-          file
-        });
-        const first = moderateRes?.results?.[0];
-        if (first?.flagged) {
-          if (first.reason) console.warn("[AI审核]", first.reason);
-          throw new Error(mapModerateReasonToUserMessage(first.reason));
-        }
-      }
-    }
-
-    report(100);
-    return fileUrl;
-  } catch (e) {
-    throw e;
-  }
-}
-
-/** 与 /file/uploadMore 成功态兼容,供 Upload 组件、http 封装统一消费 */
-export type UploadMoreLikeResponse = {
-  code: number;
-  msg: string;
-  data: { fileUrl: string };
-  /** 部分组件直接读顶层 fileUrl */
-  fileUrl: string;
-};
-
-export function isAiImageModerationEnabled(): boolean {
-  return import.meta.env.VITE_UPLOAD_IMAGE_AI_MODERATE !== "false";
-}
-
-/** 图/视频均 Tus 上传;图 moderateImage、视频 moderateVideo(url)+轮询;返回与 uploadMore 相近结构 */
-export async function uploadImageAsUploadMoreResponse(file: File): Promise<UploadMoreLikeResponse> {
-  const fileUrl = await uploadFileViaAi(file);
-  return {
-    code: ResultEnum.SUCCESS,
-    msg: "success",
-    data: { fileUrl },
-    fileUrl
-  };
-}
-
-/**
- * FormData 内含字段 `file` 且为图片或视频时走 Tus + 对应审核(图 moderateImage,视频 moderateVideo 的 video_url + 轮询),否则走原接口
- */
-export function runUploadMoreWithOptionalAi<T = UploadMoreLikeResponse>(
-  params: FormData,
-  fallback: () => Promise<T>
-): Promise<T> {
-  if (!isAiImageModerationEnabled()) {
-    return fallback();
-  }
-  const file = params.get("file");
-  if (!(file instanceof File)) {
-    return fallback();
-  }
-  const isImage = typeof file.type === "string" && file.type.startsWith("image/");
-  const isVideo = isLikelyVideoFileForAiUpload(file);
-  if (isImage || isVideo) {
-    return uploadImageAsUploadMoreResponse(file) as Promise<T>;
-  }
-  return fallback();
+  const fileUrl = await uploadViaTusToPublicUrl(file, onProgress);
+  report(100);
+  return fileUrl;
 }
 
 /** 多文件顺序上传,整体进度 0~100 */
@@ -662,11 +418,9 @@ export const aiImageApi = {
   getUploadProgress,
   deleteUploadSession,
   finalizeUploadSession,
-  moderateImage,
-  moderateVideo,
-  getVideoModerateResult,
   uploadFileViaAi,
-  uploadFilesViaAi
+  uploadFilesViaAi,
+  uploadFileViaDevSimpleEndpoint
 };
 
 export default aiImageApi;

+ 11 - 3
src/api/modules/licenseManagement.ts

@@ -3,6 +3,7 @@ import { PORT_NONE } from "@/api/config/servicePort";
 import http from "@/api";
 import http_store from "@/api/indexStore";
 import httpApi from "@/api/indexApi";
+import { uploadFormDataToOss } from "@/api/upload.js";
 
 // 获取营业执照
 export const getBusinessLicense = params => {
@@ -83,9 +84,16 @@ export const submitContractReview = params => {
   return http.post(PORT_NONE + `/license/uploadRenewalContract`, params);
 };
 
-// 上传合同图片
-export const uploadContractImage = (formData: FormData, onProgress?: (progress: number) => void) => {
-  return http.upload("/file/uploadMore", formData, onProgress, import.meta.env.VITE_API_URL as string);
+// 上传合同图片(统一走 @/api/upload.js;兼容旧逻辑 result.data[0] 为 URL)
+export const uploadContractImage = async (formData: FormData, onProgress?: (progress: number) => void) => {
+  void onProgress;
+  const { data } = await uploadFormDataToOss(formData, "image");
+  const url = data.fileUrl;
+  return {
+    code: 200,
+    msg: "success",
+    data: [url] as unknown as string[]
+  };
 };
 
 // OCR 二次校验接口

+ 3 - 7
src/api/modules/newLoginApi.ts

@@ -1,7 +1,7 @@
 import type { Login } from "@/api/interface";
 import httpLogin from "@/api/indexApi";
 import { Upload } from "@/api/interface/index";
-import { runUploadMoreWithOptionalAi } from "@/api/modules/aiImageUpload";
+import { uploadFormDataSimpleCompat } from "@/api/upload.js";
 // 获取图片验证码
 export const getImgCode = () => {
   return httpLogin.get(
@@ -62,12 +62,8 @@ export const getInputPrompt = params => {
 export const getDistrict = params => {
   return httpLogin.get(`/alienStore/gaode/getDistrict`, params);
 };
-//文件上传(图片默认走 Tus + AI 审核,可设 VITE_UPLOAD_IMAGE_AI_MODERATE=false 关闭)
-export const uploadImg = (params: FormData) => {
-  return runUploadMoreWithOptionalAi(params, () =>
-    httpLogin.post<Upload.ResFileUrl>(`/alienStore/file/uploadMore`, params, { cancel: false })
-  );
-};
+// 文件上传(统一走 @/api/upload.js → /upload/simple)
+export const uploadImg = (params: FormData) => uploadFormDataSimpleCompat(params);
 // 发布/更新动态(新接口)
 export const addOrUpdateDynamic = (params: {
   address?: string; // 经纬度

+ 20 - 14
src/api/modules/storeDecoration.ts

@@ -2,8 +2,7 @@ import { ResPage, StoreUser } from "@/api/interface/index";
 import { PORT_NONE } from "@/api/config/servicePort";
 import http from "@/api";
 import httpApi from "@/api/indexApi";
-import { Upload } from "@/api/interface/index";
-import { runUploadMoreWithOptionalAi } from "@/api/modules/aiImageUpload";
+import { uploadFormDataSimpleCompat } from "@/api/upload.js";
 
 /**
  * @name 商铺用户模块
@@ -63,19 +62,11 @@ export const saveOrUpdateDecoration = (params: any) => {
   return httpApi.post(`/alienStore/renovation/requirement/saveOrUpdate`, params);
 };
 
-// 上传房屋图纸 - 使用 /alienStore/file/uploadMore 接口
-export const uploadDecorationImage = (params: FormData) => {
-  return runUploadMoreWithOptionalAi(params, () =>
-    httpApi.post<Upload.ResFileUrl>(`/alienStore/file/uploadMore`, params, { cancel: false })
-  );
-};
+// 上传房屋图纸(统一走 @/api/upload.js → /upload/simple)
+export const uploadDecorationImage = (params: FormData) => uploadFormDataSimpleCompat(params);
 
-// 聊天图片/视频上传(与商家端一致,使用同一接口)
-export const uploadChatFile = (params: FormData) => {
-  return runUploadMoreWithOptionalAi(params, () =>
-    httpApi.post<Upload.ResFileUrl>(`/alienStore/file/uploadMore`, params, { cancel: false })
-  );
-};
+// 聊天图片/视频上传
+export const uploadChatFile = (params: FormData) => uploadFormDataSimpleCompat(params);
 
 // 删除装修需求
 export const deleteDecoration = (params: { id: number | string }) => {
@@ -156,6 +147,21 @@ export const deleteOfficialImg = (params: any) => {
 export const getOfficialImgList = (businessId, storeId, imgType = 2) => {
   return httpApi.get(`/alienStore/img/getByBusinessId?businessId=${businessId}&imgType=${imgType}&storeId=${storeId}`);
 };
+/** 门店官方视频列表 GET /alienStore/video/getByStoreId?storeId= */
+export const getOfficialVideoByStoreId = (storeId: number | string) => {
+  return httpApi.get(`/alienStore/video/getByStoreId`, { storeId: Number(storeId) });
+};
+
+/** 官方相册视频保存/批量保存 POST /alienStore/video/saveOrSaveBatch(参数与商家端 albumDetail 一致) */
+export const saveOfficialVideoSaveOrSaveBatch = (params: {
+  videoUrls: string[];
+  videoIds: (number | string | null)[];
+  coverUrls: string[];
+  storeId: number;
+  businessId: number;
+}) => {
+  return httpApi.post(`/alienStore/video/saveOrSaveBatch`, params);
+};
 //新建或修改菜品
 export const createOrUpdateDish = (params: any) => {
   return httpApi.post(`/alienStorePlatform/menuPlatform/saveOrUpdate`, params);

+ 5 - 62
src/api/modules/upload.ts

@@ -1,67 +1,10 @@
-import { Upload } from "@/api/interface/index";
-import { runUploadMoreWithOptionalAi } from "@/api/modules/aiImageUpload";
-import { PORT1 } from "@/api/config/servicePort";
-import { PORT_NONE } from "@/api/config/servicePort";
-import httpStore from "@/api/indexStore";
-import http from "@/api";
-import axios from "axios";
-import { ResultEnum } from "@/enums/httpEnum";
-import { useUserStore } from "@/stores/modules/user";
-import { ElMessage } from "element-plus";
-import { LOGIN_URL } from "@/config";
-import router from "@/routers";
-
-// 使用 alienStore 前缀的 axios 实例(用于价目表等页面上传)
-const httpStoreAlienStore = axios.create({
-  baseURL: import.meta.env.VITE_API_URL_STORE as string,
-  timeout: ResultEnum.TIMEOUT as number,
-  withCredentials: true
-});
-httpStoreAlienStore.interceptors.request.use(
-  config => {
-    const userStore = useUserStore();
-    if (config.headers) (config.headers as any).Authorization = userStore.token;
-    return config;
-  },
-  error => Promise.reject(error)
-);
-httpStoreAlienStore.interceptors.response.use(
-  response => {
-    const data = response.data;
-    const userStore = useUserStore();
-    if (data.code == ResultEnum.OVERDUE) {
-      userStore.setToken("");
-      router.replace(LOGIN_URL);
-      ElMessage.error(data.msg);
-      return Promise.reject(data);
-    }
-    if (data.code && data.code !== ResultEnum.SUCCESS) {
-      ElMessage.error(data.msg);
-      return Promise.reject(data);
-    }
-    return data;
-  },
-  error => Promise.reject(error)
-);
+import { uploadFormDataSimpleCompat } from "@/api/upload.js";
 
 /**
- * @name 文件上传模块
+ * @name 文件上传模块(图片/视频统一走 @/api/upload.js → /upload/simple)
  */
-// 图片上传(默认使用 alienStorePlatform)
-export const uploadImg = (params: FormData) => {
-  return runUploadMoreWithOptionalAi(params, () =>
-    httpStore.post<Upload.ResFileUrl>(PORT_NONE + `/file/uploadMore`, params, { cancel: false })
-  );
-};
+export const uploadImg = (params: FormData) => uploadFormDataSimpleCompat(params);
 
-// 图片上传(使用 alienStore 前缀,用于价目表等页面)
-export const uploadImgStore = (params: FormData) => {
-  return runUploadMoreWithOptionalAi(params, () =>
-    httpStoreAlienStore.post<Upload.ResFileUrl>(PORT_NONE + `/file/uploadMore`, params, { cancel: false })
-  );
-};
+export const uploadImgStore = (params: FormData) => uploadFormDataSimpleCompat(params);
 
-// 视频上传
-export const uploadVideo = (params: FormData) => {
-  return http.post<Upload.ResFileUrl>(PORT_NONE + `/file/upload/video`, params, { cancel: false });
-};
+export const uploadVideo = (params: FormData) => uploadFormDataSimpleCompat(params);

+ 386 - 100
src/api/upload.js

@@ -1,14 +1,9 @@
 import { useUserStore } from "@/stores/modules/user";
 import { ElMessage } from "element-plus";
-import { isAiImageModerationEnabled, uploadFileViaAi, isLikelyVideoFileForAiUpload } from "@/api/modules/aiImageUpload";
+import { AI_UPLOAD_FILES_PUBLIC_BASE, BASE_AI_URL } from "@/utils/config";
 
-/**
- * 门店/文件相关接口基址(与 src/api/modules/upload.ts 中 httpStoreAlienStore 一致)
- * 若签名接口部署在其它域名,可改为 VITE_API_URL_PLATFORM 等
- */
-function getStoreApiBase() {
-  return String(import.meta.env.VITE_API_URL_STORE || "").replace(/\/$/, "");
-}
+/** 非 TUS 简单上传接口路径(与 Apifox「上传文件-非TUS」一致;开发环境经 VITE_PROXY /ai-upload 转发) */
+const SIMPLE_UPLOAD_PATH = "/upload/simple";
 
 /** 从路径或 fileType 解析文件后缀(如 jpg、mp4),用于生成带格式的文件名 */
 export function getFileExtension(filePath, fileType) {
@@ -25,34 +20,6 @@ export function getFileExtension(filePath, fileType) {
 }
 
 /**
- * GET 获取 OSS 直传签名(与 uni 版 1.js 一致)
- * @returns {Promise<Record<string, any>>}
- */
-async function fetchOssSignature() {
-  const base = getStoreApiBase();
-  if (!base) {
-    throw new Error("未配置 VITE_API_URL_STORE");
-  }
-  const userStore = useUserStore();
-  const res = await fetch(`${base}/oss/direct/new/signature`, {
-    method: "GET",
-    headers: {
-      Authorization: userStore.token || ""
-    },
-    credentials: "include"
-  });
-  if (!res.ok) {
-    throw new Error("获取签名失败");
-  }
-  const body = await res.json();
-  const data = body.data ?? body;
-  if (!data || !data.host) {
-    throw new Error(body.msg || "获取签名失败");
-  }
-  return data;
-}
-
-/**
  * 将入参统一为 File[]
  * @param {File | File[] | FileList} input
  * @returns {File[]}
@@ -70,58 +37,379 @@ function normalizeFiles(input) {
 }
 
 /**
- * 单个 File 上传到 OSS(POST multipart,字段与 1.js / OSS4 直传一致)
- * @param {Record<string, any>} signRes 签名接口返回
+ * 常见「可直接用于 img/video src」的字段(含蛇形命名)
+ * 注意:勿把 upload_id / filename 放前面——/upload/simple 会返回 upload_id 与完整 URL 并存,
+ * 若先按「裸 id」拼到 /files/ 会得到错误地址,图片会裂图。
+ */
+const URL_LIKE_KEYS = [
+  "download_url",
+  "downloadUrl",
+  "preview_url",
+  "previewUrl",
+  "url",
+  "fileUrl",
+  "file_url",
+  "accessUrl",
+  "access_url",
+  "ossUrl",
+  "cdnUrl",
+  "path",
+  "filePath",
+  "file_path",
+  "objectKey",
+  "object_key",
+  "key",
+  "location",
+  "href"
+];
+
+/**
+ * 将接口返回的路径或 id 规范为可访问 URL
+ * @param {string} raw
+ * @returns {string}
+ */
+function normalizeToFileUrl(raw) {
+  const t = String(raw ?? "").trim();
+  if (!t) return "";
+  if (/^https?:\/\//i.test(t)) return t;
+  if (t.startsWith("//")) return `https:${t}`;
+
+  const filesBase = String(AI_UPLOAD_FILES_PUBLIC_BASE || "").replace(/\/$/, "");
+  if (filesBase.startsWith("http")) {
+    try {
+      if (t.startsWith("/")) {
+        return new URL(t, new URL(`${filesBase}/`).origin).href;
+      }
+      if (/^files\//i.test(t)) {
+        return new URL(`/${t.replace(/^\/+/, "")}`, new URL(`${filesBase}/`).origin).href;
+      }
+      // 相对路径 dir/file.ext(无协议)
+      if (t.includes("/") && !t.startsWith("/") && !/^[a-z]+:/i.test(t)) {
+        return `${filesBase}/${t.replace(/^\/+/, "")}`;
+      }
+      // 仅返回 uuid/文件名 等,按「对外文件基址 + 片段」拼接
+      if (!t.includes("/") && !t.includes("\\") && t.length <= 512) {
+        return `${filesBase}/${encodeURIComponent(t)}`;
+      }
+    } catch {
+      /* ignore */
+    }
+  }
+  return "";
+}
+
+/**
+ * 从对象上按已知字段取「可能是地址」的字符串
+ * @param {unknown} o
+ * @returns {string}
+ */
+function pickUrlLikeFromObject(o) {
+  if (!o || typeof o !== "object" || Array.isArray(o)) return "";
+  const r = /** @type {Record<string, unknown>} */ (o);
+  for (const k of URL_LIKE_KEYS) {
+    const v = r[k];
+    if (typeof v === "string" && v.trim()) {
+      const abs = normalizeToFileUrl(v);
+      if (abs) return abs;
+    }
+  }
+  return "";
+}
+
+/**
+ * 深度查找第一个 http(s) 字符串
+ * @param {unknown} val
+ * @param {number} depth
+ * @param {number} maxDepth
+ * @returns {string}
+ */
+function deepFindHttpUrl(val, depth = 0, maxDepth = 8) {
+  if (depth > maxDepth || val == null) return "";
+  if (typeof val === "string") {
+    const t = val.trim();
+    return /^https?:\/\//i.test(t) ? t : "";
+  }
+  if (Array.isArray(val)) {
+    for (const item of val) {
+      const u = deepFindHttpUrl(item, depth + 1, maxDepth);
+      if (u) return u;
+    }
+    return "";
+  }
+  if (typeof val === "object") {
+    for (const k of Object.keys(val)) {
+      const u = deepFindHttpUrl(/** @type {Record<string, unknown>} */ (val)[k], depth + 1, maxDepth);
+      if (u) return u;
+    }
+  }
+  return "";
+}
+
+/**
+ * 从 JSON 体中解析文件访问地址(兼容多种后端约定)
+ * @param {unknown} body
+ * @returns {string}
+ */
+function pickUrlFromJsonBody(body) {
+  if (body == null) return "";
+
+  if (typeof body === "string") {
+    return normalizeToFileUrl(body) || (body.trim().startsWith("http") ? body.trim() : "");
+  }
+
+  if (typeof body !== "object" || Array.isArray(body)) {
+    return deepFindHttpUrl(body);
+  }
+
+  const b = /** @type {Record<string, unknown>} */ (body);
+  const unwrap = v => {
+    if (v == null) return null;
+    if (typeof v === "object" && !Array.isArray(v) && /** @type {Record<string, unknown>} */ (v).data !== undefined) {
+      return /** @type {Record<string, unknown>} */ (v).data;
+    }
+    return v;
+  };
+
+  const candidates = [unwrap(b.data), b.data, b.result, unwrap(b.result), b.payload, b.body, b];
+
+  for (const d of candidates) {
+    if (d == null) continue;
+    if (typeof d === "string") {
+      const u = normalizeToFileUrl(d) || (d.trim().startsWith("http") ? d.trim() : "");
+      if (u) return u;
+    } else if (typeof d === "object") {
+      let u = pickUrlLikeFromObject(d);
+      if (u) return u;
+      u = deepFindHttpUrl(d);
+      if (u) return u;
+    }
+  }
+
+  return deepFindHttpUrl(body);
+}
+
+/** 审核服务返回的违规大类 → 面向用户的短说明(与业务文案风格一致) */
+const VIOLATION_CATEGORY_USER_HINT = {
+  GAMBLING: "图片涉及赌博等违规内容",
+  PORNOGRAPHY: "图片涉及色情等违规内容",
+  PORN: "图片涉及色情等违规内容",
+  ADULT: "图片涉及色情等违规内容",
+  SEXUAL: "图片涉及色情等违规内容",
+  DRUGS: "图片涉及毒品等违规内容",
+  DRUG: "图片涉及毒品等违规内容",
+  VIOLENCE: "图片涉及暴力、血腥等违规内容",
+  GORE: "图片涉及暴力、血腥等违规内容",
+  POLITICAL: "图片涉及不当政治或敏感信息",
+  POLITICS: "图片涉及不当政治或敏感信息",
+  SPAM: "图片涉及违规推广或垃圾信息",
+  ABUSE: "图片涉及辱骂、人身攻击等不文明内容",
+  ILLEGAL: "图片含违法违规内容"
+};
+
+/**
+ * 根据 reason / 关键词生成友好提示(与 aiImageUpload 中审核话术对齐)
+ * @param {string} raw
+ * @returns {string}
+ */
+/** 仅返回「原因」短句,后缀由 formatSimpleUploadModerationMessage 统一拼接 */
+function mapSimpleModerationReasonToTip(raw) {
+  const s = raw == null ? "" : String(raw).trim();
+  if (!s) return "";
+  if (/赌|赌博|casino|gambl|GAMBLING/i.test(s)) return "图片涉及赌博等违规内容";
+  if (/色情|淫秽|porn|sex|PORNOGRAPHY|PORN/i.test(s)) return "图片涉及色情等违规内容";
+  if (/毒品|涉毒|drug|DRUG/i.test(s)) return "图片涉及毒品等违规内容";
+  if (/暴力|血腥|violence|gore|VIOLENCE/i.test(s)) return "图片涉及暴力、血腥等违规内容";
+  if (/辱骂|谩骂|人身攻击|abuse|insult/i.test(s)) return "图片涉及不文明内容";
+  if (/政治|谣言/i.test(s)) return "图片涉及违规信息";
+  if (/广告|spam|营销/i.test(s)) return "图片涉及违规推广内容";
+  if (/违法|违禁|illegal/i.test(s)) return "图片含违法违规内容";
+  if (/黄赌毒/i.test(s)) return "请勿上传涉黄、涉赌、涉毒等违规内容";
+  if (/审核|moderat|violation|flagged|VLM/i.test(s)) return "图片未通过内容审核";
+  return "";
+}
+
+/**
+ * 解析 /upload/simple 审核未通过等响应(saved: false + moderation)
+ * @param {unknown} parsed
+ * @returns {string} 非空则可直接作为 ElMessage / Error 文案
+ */
+function formatSimpleUploadModerationMessage(parsed) {
+  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return "";
+  const p = /** @type {Record<string, unknown>} */ (parsed);
+  const notSaved = p.saved === false;
+  const mod = p.moderation;
+  const hasModeration = mod != null && typeof mod === "object";
+  let flagged = false;
+  if (hasModeration) {
+    const results = /** @type {Record<string, unknown>} */ (mod).results;
+    if (Array.isArray(results) && results[0] && typeof results[0] === "object") {
+      flagged = /** @type {Record<string, unknown>} */ (results[0]).flagged === true;
+    }
+  }
+  if (!notSaved && !flagged) return "";
+
+  const apiMsg = String(p.message ?? p.msg ?? "").trim();
+
+  let firstResult = null;
+  if (hasModeration) {
+    const results = /** @type {Record<string, unknown>} */ (mod).results;
+    if (Array.isArray(results) && results[0] && typeof results[0] === "object") {
+      firstResult = /** @type {Record<string, unknown>} */ (results[0]);
+    }
+  }
+
+  let reason = "";
+  let firstCategory = "";
+  if (firstResult) {
+    reason = String(firstResult.reason ?? "").trim();
+    const cats = firstResult.violation_categories;
+    if (Array.isArray(cats) && cats[0] && typeof cats[0] === "object") {
+      firstCategory = String(/** @type {Record<string, unknown>} */ (cats[0]).category ?? "")
+        .trim()
+        .toUpperCase();
+    }
+  }
+
+  const suffix = "未通过审核,文件未保存。请更换后重试";
+
+  if (firstCategory && VIOLATION_CATEGORY_USER_HINT[firstCategory]) {
+    return `${VIOLATION_CATEGORY_USER_HINT[firstCategory]},${suffix}`;
+  }
+
+  const fromReason = mapSimpleModerationReasonToTip(reason);
+  if (fromReason) {
+    return `${fromReason.replace(/。$/, "")},${suffix}`;
+  }
+
+  if (apiMsg) {
+    if (/请更换|更换后|重新选择|换一张/i.test(apiMsg)) return apiMsg;
+    if (/未保存|未通过/.test(apiMsg)) return `${apiMsg.replace(/。$/, "")}。请更换后重试`;
+    return `${apiMsg.replace(/。$/, "")},${suffix}`;
+  }
+
+  return "图片未通过内容审核,文件未保存。请更换后重试";
+}
+
+/**
+ * 业务层 code / success(HTTP 200 但业务失败时常有)
+ * @param {unknown} parsed
+ */
+function assertSimpleUploadBusinessOk(parsed) {
+  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return;
+  const p = /** @type {Record<string, unknown>} */ (parsed);
+  if (p.success === false) {
+    throw new Error(String(p.msg ?? p.message ?? p.error ?? "上传失败"));
+  }
+  const c = p.code;
+  if (c === undefined || c === null) return;
+  const ok = c === 200 || c === 0 || c === "200" || c === "0";
+  if (!ok) {
+    throw new Error(String(p.msg ?? p.message ?? p.error ?? "上传失败"));
+  }
+}
+
+/**
+ * POST multipart:字段 file;可选 filename(此处不传则服务端用原名)
  * @param {File} file
- * @param {string} key 对象 key(含 dir 前缀)
  * @returns {Promise<string>} 文件访问 URL
  */
-async function postFileToOss(signRes, file, key) {
+async function postFileToSimpleUpload(file) {
+  const base = String(BASE_AI_URL || "").replace(/\/$/, "");
+  if (!base) {
+    throw new Error("未配置上传服务地址(VITE_AI_UPLOAD_BASE 或默认 /ai-upload)");
+  }
+
   const formData = new FormData();
-  formData.append("success_action_status", "200");
-  formData.append("policy", signRes.policy);
-  formData.append("x-oss-signature", signRes.signature);
-  formData.append("x-oss-signature-version", "OSS4-HMAC-SHA256");
-  formData.append("x-oss-credential", signRes.x_oss_credential || signRes["x-oss-credential"] || "");
-  formData.append("x-oss-date", signRes.x_oss_date || signRes["x-oss-date"] || "");
-  formData.append("key", key);
-  if (signRes.security_token != null) {
-    formData.append("x-oss-security-token", signRes.security_token);
-  }
-  formData.append("file", file, file.name);
-
-  const res = await fetch(signRes.host, {
+  formData.append("file", file, file.name || "file");
+
+  const userStore = useUserStore();
+  const headers = {};
+  const token = userStore.token || "";
+  if (token) {
+    headers.Authorization = token;
+  }
+
+  const res = await fetch(`${base}${SIMPLE_UPLOAD_PATH}`, {
     method: "POST",
+    headers,
+    /** 不带跨域 Cookie,减轻上传服务 CORS 要求(鉴权仅用 Authorization) */
+    credentials: "omit",
     body: formData
   });
 
-  if (res.status !== 200) {
-    let msg = "上传失败";
+  const rawText = await res.text();
+  let parsed = null;
+  const trimmed = rawText.trim();
+  if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
     try {
-      const t = await res.text();
-      if (t) msg = t.slice(0, 200);
+      parsed = JSON.parse(rawText);
     } catch (_) {
       /* ignore */
     }
+  }
+
+  if (!res.ok) {
+    const modHint = formatSimpleUploadModerationMessage(parsed);
+    let msg = modHint;
+    if (!msg && parsed && typeof parsed === "object") {
+      const p = /** @type {Record<string, unknown>} */ (parsed);
+      const m = p.msg ?? p.message ?? p.error;
+      if (m != null && String(m)) msg = String(m);
+    }
+    if (!msg && trimmed) msg = trimmed.slice(0, 200);
+    if (!msg) msg = "上传失败";
     throw new Error(msg);
   }
 
-  const text = await res.text();
-  if (text && typeof text === "string" && text.trim().startsWith("http")) {
-    return text.trim();
+  try {
+    assertSimpleUploadBusinessOk(parsed);
+  } catch (bizErr) {
+    console.error("[upload/simple] 业务失败", bizErr, trimmed.slice(0, 500));
+    throw bizErr;
+  }
+
+  const moderationUserTip = formatSimpleUploadModerationMessage(parsed);
+  if (moderationUserTip) {
+    throw new Error(moderationUserTip);
   }
-  return signRes.host.replace(/\/$/, "") + "/" + key;
+
+  let url = pickUrlFromJsonBody(parsed);
+  if (!url && trimmed.startsWith("http")) {
+    url = trimmed;
+  }
+  if (!url) {
+    const fromHeader =
+      res.headers.get("x-file-url") ||
+      res.headers.get("x-url") ||
+      res.headers.get("x-oss-url") ||
+      res.headers.get("file-url") ||
+      "";
+    if (fromHeader?.trim()) {
+      const h = fromHeader.trim();
+      url = normalizeToFileUrl(h) || (h.startsWith("http") ? h : "");
+    }
+  }
+  if (!url) {
+    const loc = res.headers.get("location") || res.headers.get("Location") || "";
+    if (loc) {
+      url = normalizeToFileUrl(loc.trim()) || (loc.trim().startsWith("http") ? loc.trim() : "");
+    }
+  }
+  if (!url) {
+    console.error("[upload/simple] 无法解析文件地址,响应片段:", trimmed.slice(0, 800));
+    throw new Error("上传失败,未返回文件地址");
+  }
+  return url;
 }
 
 /**
- * 上传文件:视频一律走 Tus + moderateVideo / 轮询(uploadFileViaAi),不走 OSS;
- * 开启 AI 时图片走 Tus+审核,否则图片与其它类型走 OSS 直传。
+ * 上传文件:图片与视频均走同一接口 POST /upload/simple,formData 键 file。
  * @param {File | File[] | FileList} files 浏览器文件对象;支持单个 File、数组或 FileList
- * @param {string} [fileType] 文件类型(如 'image' | 'video'),用于在文件名无后缀时推断格式
+ * @param {string} [_fileType] 保留参数,兼容旧调用(当前不参与分支)
  * @param {{ showLoading?: boolean }} [options] showLoading 为 true 时用 ElMessage 提示上传中(非阻塞)
  * @returns {Promise<string[]>} 上传成功后的文件 URL 列表
  */
-export async function uploadFilesToOss(files, fileType, options = {}) {
+export async function uploadFilesToOss(files, _fileType, options = {}) {
   const { showLoading = false } = options;
   const fileArr = normalizeFiles(files);
   if (fileArr.length === 0) {
@@ -135,41 +423,11 @@ export async function uploadFilesToOss(files, fileType, options = {}) {
   }
 
   try {
-    let signRes = null;
     const uploadedUrls = [];
-    const useAiImage = isAiImageModerationEnabled();
-
-    for (let i = 0; i < fileArr.length; i++) {
-      const file = fileArr[i];
-      const isImageBranch = fileType === "image" || (!fileType && file.type && String(file.type).startsWith("image/"));
-      const isVideoBranch = fileType === "video" || isLikelyVideoFileForAiUpload(file);
-
-      if (isVideoBranch) {
-        const url = await uploadFileViaAi(file, { skipModerate: !useAiImage });
-        uploadedUrls.push(url);
-        continue;
-      }
-
-      if (useAiImage && isImageBranch) {
-        const url = await uploadFileViaAi(file);
-        uploadedUrls.push(url);
-        continue;
-      }
-
-      if (!signRes) {
-        signRes = await fetchOssSignature();
-      }
-      const filePath = file.name || "";
-      const ext = getFileExtension(filePath, fileType);
-      const rawBase = filePath.split("/").pop() || "";
-      const baseName = rawBase.replace(/\.[^.]+$/, "") || `file_${Date.now()}_${i}`;
-      const fileName = baseName.includes(".") ? baseName : `${baseName}.${ext}`;
-      const key = (signRes.dir ? signRes.dir.replace(/\/$/, "") + "/" : "") + fileName;
-
-      const url = await postFileToOss(signRes, file, key);
+    for (const file of fileArr) {
+      const url = await postFileToSimpleUpload(file);
       uploadedUrls.push(url);
     }
-
     closeLoading();
     return uploadedUrls;
   } catch (e) {
@@ -196,15 +454,43 @@ export async function uploadFileToOss(file, fileType, options) {
 /**
  * 兼容 UploadImg 等组件的 api 格式:接收 FormData,返回 { data: { fileUrl } }
  * @param {FormData} formData 包含 file 字段
- * @param {string} [fileType] 文件类型,默认 'image'
+ * @param {string} [fileType] 不传或传空时按 file.type 自动区分 image / video
  * @param {{ showLoading?: boolean }} [options]
  * @returns {Promise<{ data: { fileUrl: string } }>}
  */
-export async function uploadFormDataToOss(formData, fileType = "image", options = {}) {
+export async function uploadFormDataToOss(formData, fileType, options = {}) {
   const file = formData.get("file");
   if (!file || !(file instanceof File)) {
     throw new Error("请选择要上传的文件");
   }
-  const url = await uploadFileToOss(file, fileType, options);
+  const inferred =
+    fileType === undefined || fileType === null || fileType === ""
+      ? String(file.type || "").startsWith("video/")
+        ? "video"
+        : "image"
+      : fileType;
+  const url = await uploadFileToOss(file, inferred, options);
   return { data: { fileUrl: url } };
 }
+
+/**
+ * FormData 走 `/upload/simple` 后,包装成旧组件习惯的 `{ code, msg, data: { fileUrl }, fileUrl }`(并非请求 `/file/uploadMore`)
+ * @param {FormData} formData
+ * @param {{ showLoading?: boolean }} [options]
+ * @returns {Promise<{ code: number; msg: string; data: { fileUrl: string }; fileUrl: string }>}
+ */
+export async function uploadFormDataSimpleCompat(formData, options = {}) {
+  const file = formData.get("file");
+  if (!file || !(file instanceof File)) {
+    throw new Error("请选择要上传的文件");
+  }
+  const isVideo = String(file.type || "").startsWith("video/");
+  const inner = await uploadFormDataToOss(formData, isVideo ? "video" : "image", options);
+  const fileUrl = inner.data.fileUrl;
+  return {
+    code: 200,
+    msg: "success",
+    data: inner.data,
+    fileUrl
+  };
+}

+ 21 - 3
src/components/Upload/Imgs.vue

@@ -24,7 +24,15 @@
         </slot>
       </div>
       <template #file="{ file }">
-        <video v-if="isVideoFile(file) && file.url" :src="file.url" class="upload-image" muted preload="metadata" playsinline />
+        <video
+          v-if="isVideoFile(file) && file.url"
+          :src="file.url"
+          :poster="(file as any).coverUrl || undefined"
+          class="upload-image"
+          muted
+          preload="metadata"
+          playsinline
+        />
         <img
           v-else-if="file.url && file.uid !== undefined && !imageLoadError.has(file.uid)"
           :src="file.url"
@@ -201,14 +209,21 @@ const handleHttpUpload = async (options: UploadRequestOptions) => {
   try {
     const api = props.api ?? uploadImg;
     const response = await api(formData);
-    // 从 response.fileUrl 取值
-    const fileUrl = response?.fileUrl || "";
+    // 从 response.fileUrl / response.data.fileUrl 取值
+    const fileUrl = response?.fileUrl || response?.data?.fileUrl || "";
 
     if (fileUrl) {
+      const coverUrlRaw =
+        (response as any)?.coverUrl ?? (response as any)?.data?.coverUrl ?? (response as any)?.data?.cover ?? "";
+      const coverUrl = typeof coverUrlRaw === "string" && coverUrlRaw.trim() ? coverUrlRaw.trim() : "";
+
       // 更新 options.file(Element Plus 传入的文件对象)
       (options.file as unknown as UploadFile).url = fileUrl;
       (options.file as unknown as UploadFile).status = "success";
       (options.file as unknown as UploadFile).response = response;
+      if (coverUrl) {
+        (options.file as any).coverUrl = coverUrl;
+      }
 
       // 同步更新 _fileList 中对应的文件
       const fileIndex = _fileList.value.findIndex(item => item.uid === fileUid);
@@ -216,6 +231,9 @@ const handleHttpUpload = async (options: UploadRequestOptions) => {
         _fileList.value[fileIndex].url = fileUrl;
         _fileList.value[fileIndex].status = "success";
         (_fileList.value[fileIndex] as any).response = response;
+        if (coverUrl) {
+          (_fileList.value[fileIndex] as any).coverUrl = coverUrl;
+        }
       }
     }
 

+ 1 - 1
src/stores/modules/auth.ts

@@ -76,7 +76,7 @@ export const useAuthStore = defineStore({
                 break;
               case "设施与服务":
                 // 特色美食(1) 不显示设施与服务
-                menu.meta.isHide = businessSection !== 1;
+                menu.meta.isHide = businessSection == 1;
                 break;
               case "门店基础信息":
               case "门店头图":

+ 4 - 6
src/typings/global.d.ts

@@ -62,14 +62,12 @@ declare interface ViteEnv {
   VITE_CRYPTO_KEY: string;
   VITE_CRYPTO_IV: string;
   VITE_WS_BASE?: string;
-  /** Tus 上传服务根(可选,不配则开发走 /ai-upload 代理、生产走 upload.ailien.shop) */
+  /** 上传 API 请求根(可选;不配则默认同源 /ai-upload,依赖 Vite/Nginx 反代) */
   VITE_AI_UPLOAD_BASE?: string;
-  /** AI 审核服务根(可选,不配则开发走 /ai-moderate 代理) */
-  VITE_AI_MODERATE_BASE?: string;
-  /** 上传完成后对外访问 URL 前缀,默认 http://upload.ailien.shop:8088/files */
+  /** 上传完成后对外访问 URL 前缀 */
   VITE_AI_FILES_PUBLIC_BASE?: string;
-  /** 设为 false 时图片仍走原 OSS / uploadMore,不经过 Tus + AI 审核 */
-  VITE_UPLOAD_IMAGE_AI_MODERATE?: string;
+  /** 可选;官方相册视频 simple 上传服务根,不配则开发走相对路径 /dev-upload-ailien/...、生产默认 upload.ailien.shop:8443 */
+  VITE_DEV_UPLOAD_SIMPLE_BASE?: string;
 }
 
 interface ImportMetaEnv extends ViteEnv {

+ 13 - 11
src/utils/config.ts

@@ -1,20 +1,22 @@
 /**
- * AI Tus 上传与内容审核服务基址(与 uni 端逻辑对齐,适配 Web)
- * 开发环境默认走 Vite 代理前缀,需在 .env.development 的 VITE_PROXY 中配置 /ai-upload、/ai-moderate
+ * Tus / simple 上传服务「请求」基址(与 uni 端对齐)
+ * - 未配置 VITE_AI_UPLOAD_BASE 时默认 **同源相对路径 `/ai-upload`**,由 Vite 开发代理或 Nginx 反代到实际上传域,避免浏览器直连 upload 域产生跨域。
+ * - 若必须浏览器直连上传 HTTPS 域名,请在 .env 设置 VITE_AI_UPLOAD_BASE,并确保该域已正确配置 CORS(含 Authorization、OPTIONS)。
  */
 const trimSlash = (s: string) => s.replace(/\/$/, "");
 
-export const BASE_AI_URL = trimSlash(
-  String(import.meta.env.VITE_AI_UPLOAD_BASE || "").trim() ||
-    (import.meta.env.DEV ? "/ai-upload" : "https://upload.ailien.shop:8443")
-);
-
-export const BASE_AI_MODERATE_URL = trimSlash(
-  String(import.meta.env.VITE_AI_MODERATE_BASE || "").trim() ||
-    (import.meta.env.DEV ? "/ai-moderate" : "https://verify.ailien.shop:8444")
-);
+export const BASE_AI_URL = trimSlash(String(import.meta.env.VITE_AI_UPLOAD_BASE || "").trim() || "/ai-upload");
 
 /** 上传完成后对外可访问的文件 URL:`${AI_UPLOAD_FILES_PUBLIC_BASE}/${uploadId}` */
 export const AI_UPLOAD_FILES_PUBLIC_BASE = trimSlash(
   String(import.meta.env.VITE_AI_FILES_PUBLIC_BASE || "").trim() || "https://upload.ailien.shop:8443/files"
 );
+
+/**
+ * 官方相册视频等:POST multipart 至 `/dev-upload-ailien/upload/simple`(非 Tus)
+ * 不配时:开发环境请求同源相对路径(需在 VITE_PROXY 中把 `/dev-upload-ailien` 指到上传服务);生产默认 upload.ailien.shop:8443
+ */
+export const BASE_DEV_UPLOAD_SIMPLE = trimSlash(
+  String(import.meta.env.VITE_DEV_UPLOAD_SIMPLE_BASE || "").trim() ||
+    (import.meta.env.DEV ? "" : "https://upload.ailien.shop:8443")
+);

Разница между файлами не показана из-за своего большого размера
+ 535 - 209
src/views/storeDecoration/officialPhotoAlbum/index.vue


Некоторые файлы не были показаны из-за большого количества измененных файлов