From 7b6f62f1bc51bcdc6aea7f827d792e85c5a2b7de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BB=84=E8=87=AA=E8=BE=BE?= <980324341@qq.com> Date: Tue, 8 Sep 2026 13:18:23 +0800 Subject: [PATCH] =?UTF-8?q?fix(brand):=20=E5=93=81=E7=89=8C=E6=A3=80?= =?UTF-8?q?=E6=B5=8B=E8=A1=A5=E4=B8=8A=E4=BC=A0=E9=93=BE+=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=20/api/brand/run=20=E7=AB=AF=E7=82=B9+=E6=96=87?= =?UTF-8?q?=E4=BB=B6=E5=A4=B9=E5=B1=95=E5=BC=80=E6=A1=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - BrandBrandTab 对齐其它工具页:选文件/文件夹后先 upload_file_to_java 再提交(服务器可读路径),修复本地路径直传导致的「请先上传待处理文件/没有有效的xlsx文件路径」 - runBrandNow/createBrandTask 改为 files 契约(fileUrl=临时路径 localPath)+ 传 userId - Java 新增 POST /api/brand/run(等价 /tasks 并返回待爬取数据),修复前端「立即运行」No static resource - 选择文件夹改走新版桌面客户端桥 expand_brand_folder(本机展开);旧客户端提示改用文件多选 - 生产 .env 已另配 AIIMAGE_BRAND_CHECK_TOKEN(上游 16890 鉴权),非代码变更 --- .../brand/controller/BrandTaskController.java | 11 +++ .../pages/brand/components/BrandBrandTab.vue | 86 +++++++++++++------ .../src/shared/api/types/modules/brand.ts | 15 +++- frontend-vue/src/shared/api/types/upload.ts | 2 + frontend-vue/src/shared/bridges/pywebview.ts | 4 + 5 files changed, 86 insertions(+), 32 deletions(-) diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/brand/controller/BrandTaskController.java b/backend-java/src/main/java/com/nanri/aiimage/modules/brand/controller/BrandTaskController.java index 46580cdf..6e0a131e 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/brand/controller/BrandTaskController.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/brand/controller/BrandTaskController.java @@ -63,6 +63,17 @@ public class BrandTaskController { return ApiResponse.success(brandTaskService.createTaskAndBuildPayload(userId, request)); } + @PostMapping("/run") + @Operation( + summary = "立即执行品牌检查(新工具台'立即运行')", + description = "等价于 /tasks:创建任务并返回待爬取数据。前端已通过 /api/files/upload 上传文件(fileUrl=服务器临时路径)。" + ) + public ApiResponse run( + @Parameter(description = "用户 ID", required = true) @RequestParam Long userId, + @Valid @RequestBody BrandTaskCreateRequest request) { + return ApiResponse.success(brandTaskService.createTaskAndBuildPayload(userId, request)); + } + @GetMapping("/tasks") @Operation( summary = "获取品牌任务列表", diff --git a/frontend-vue/src/pages/brand/components/BrandBrandTab.vue b/frontend-vue/src/pages/brand/components/BrandBrandTab.vue index 085e4b5f..20615357 100644 --- a/frontend-vue/src/pages/brand/components/BrandBrandTab.vue +++ b/frontend-vue/src/pages/brand/components/BrandBrandTab.vue @@ -123,21 +123,18 @@ import { cancelBrandTask, createBrandTask, deleteBrandTask, - expandBrandFolder, getBrandTaskDownloadUrl, getBrandTasks, runBrandNow, type BrandTaskItem, } from '@/shared/api/brand' +import { checkSelectedFiles, EXCEL_EXTENSIONS } from '@/shared/dispatch-guard.ts' +import { passGuard } from '@/shared/dispatch-guard-ui' +import type { UploadFileVo } from '@/shared/api/upload.ts' +import type { UploadedFileRef } from '@/shared/api/types/upload.ts' +import type { BrandExpandFolderItem } from '@/shared/api/types/modules/brand' -/** 扩展文件夹接口响应(向后端真实字段 paths 兼容,shared 类型仅声明 items 时以本接口为准) */ -interface BrandExpandFolderPathsResponse { - success: boolean - paths?: string[] - error?: string -} - -const selectedPaths = ref([]) +const uploadedFiles = ref([]) const strategy = ref<'Terms' | 'Simple'>('Terms') const runMode = ref<'immediate' | 'queue'>('immediate') const submitting = ref(false) @@ -151,6 +148,10 @@ const hasUid = computed(() => { const numeric = Number(raw.trim()) return Number.isFinite(numeric) && numeric > 0 }) +// 展示用:已上传文件列表的展示名(优先相对路径,其次原文件名) +const selectedPaths = computed(() => + uploadedFiles.value.map((f) => f.relativePath || f.originalFilename || f.fileKey), +) const displayPaths = computed(() => selectedPaths.value.slice(0, 8)) function baseName(path: string) { @@ -275,10 +276,23 @@ function formatDateTime(value?: string) { return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}` } -function normalizeSelected(paths: string[]) { - selectedPaths.value = paths - .filter((path) => /\.xlsx$/i.test(path)) - .filter((path, index, array) => array.indexOf(path) === index) +/** 上传本地 xlsx 到 Java 临时目录,成功后回填 fileUrl(服务器本地路径,Java 直接读取) */ +async function uploadBrandPathsToJava(paths: Array) { + const api = getPywebviewApi() + if (!api?.upload_file_to_java) { + throw new Error('当前环境未提供文件上传能力') + } + const files: UploadFileVo[] = [] + for (const item of paths) { + const filePath = typeof item === 'string' ? item : item.absolutePath + const relativePath = typeof item === 'string' ? undefined : item.relativePath + const uploaded = await api.upload_file_to_java(filePath, relativePath) + if (!uploaded?.success || !uploaded.data) { + throw new Error(uploaded?.error || uploaded?.message || `上传失败:${filePath}`) + } + files.push(uploaded.data) + } + return files } async function selectFiles() { @@ -287,13 +301,15 @@ async function selectFiles() { ElMessage.warning('当前环境不支持文件选择,请在本机客户端中打开') return } + const paths = await bridge.select_brand_xlsx_files() + if (!paths?.length) return + if (!(await passGuard(checkSelectedFiles(paths, { allowedExtensions: EXCEL_EXTENSIONS })))) return try { - const paths = await bridge.select_brand_xlsx_files() - if (!paths?.length) return - normalizeSelected(paths) - ElMessage.success(`已选择 ${selectedPaths.value.length} 个 Excel 文件`) + const files = await uploadBrandPathsToJava(paths) + uploadedFiles.value = files + ElMessage.success(`已选择并上传 ${files.length} 个 Excel 文件`) } catch (error) { - ElMessage.error(error instanceof Error ? error.message : '文件选择失败') + ElMessage.error(error instanceof Error ? error.message : '文件上传失败') } } @@ -303,16 +319,23 @@ async function selectFolder() { ElMessage.warning('当前环境不支持文件夹选择,请在本机客户端中打开') return } + const folder = await bridge.select_brand_folder() + if (!folder) return + // 新版桌面客户端桥在本机展开文件夹(服务器无法访问用户本机目录) + if (!bridge.expand_brand_folder) { + ElMessage.warning('当前桌面客户端版本不支持文件夹展开,请使用"选择文件"(可多选)') + return + } try { - const folder = await bridge.select_brand_folder() - if (!folder) return - const res = (await expandBrandFolder(folder)) as BrandExpandFolderPathsResponse - if (!res.success || !res.paths?.length) { + const res = await bridge.expand_brand_folder(folder) + if (!res.success || !res.items?.length) { ElMessage.warning(res.error || '该文件夹下没有 xlsx 文件') return } - normalizeSelected(res.paths) - ElMessage.success(`已选择文件夹内 ${selectedPaths.value.length} 个 Excel 文件`) + if (!(await passGuard(checkSelectedFiles(res.items.map((i) => i.absolutePath), { allowedExtensions: EXCEL_EXTENSIONS })))) return + const files = await uploadBrandPathsToJava(res.items) + uploadedFiles.value = files + ElMessage.success(`已选择并上传文件夹内 ${files.length} 个 Excel 文件`) } catch (error) { ElMessage.error(error instanceof Error ? error.message : '文件夹选择失败') } @@ -327,28 +350,35 @@ async function submitRun() { ElMessage.warning('未获取到登录用户,请先在本机客户端登录后再试') return } - if (!selectedPaths.value.length) { + if (!uploadedFiles.value.length) { ElMessage.warning('请先选择待检 Excel 文件') return } submitting.value = true try { + // 上传后以服务器可读的 localPath 作为 fileUrl(Java resolveSourceFile 直接读临时文件) + const files: UploadedFileRef[] = uploadedFiles.value.map((f) => ({ + fileKey: f.fileKey, + originalFilename: f.originalFilename, + relativePath: f.relativePath, + fileUrl: f.localPath, + })) if (runMode.value === 'immediate') { - const res = await runBrandNow(selectedPaths.value, strategy.value) + const res = await runBrandNow(files, strategy.value) if (res.success && res.task_id) { ElMessage.success(`已创建任务 ${res.task_id},客户端开始检测`) } else { throw new Error((res as { error?: string }).error || '运行失败') } } else { - const res = await createBrandTask(selectedPaths.value, strategy.value) + const res = await createBrandTask(files, strategy.value) if (res.success && res.task_id) { ElMessage.success(`任务 ${res.task_id} 已添加到队列`) } else { throw new Error((res as { error?: string }).error || '添加失败') } } - selectedPaths.value = [] + uploadedFiles.value = [] await loadTasks() } catch (error) { ElMessage.error(error instanceof Error ? error.message : '提交失败') diff --git a/frontend-vue/src/shared/api/types/modules/brand.ts b/frontend-vue/src/shared/api/types/modules/brand.ts index 70c5e683..148f2d38 100644 --- a/frontend-vue/src/shared/api/types/modules/brand.ts +++ b/frontend-vue/src/shared/api/types/modules/brand.ts @@ -1,4 +1,5 @@ import { requestDeleteJson, requestGetJson, requestPostJson } from '../../http.ts' +import type { UploadedFileRef } from '../../upload.ts' function getCurrentUserId() { const raw = typeof window === 'undefined' ? '' : window.localStorage.getItem('uid') || '' @@ -82,12 +83,18 @@ export function expandBrandFolderRecursive(folder: string) { return requestPostJson(`${API_PREFIX}/api/brand/expand-folder-recursive`, { folder }) } -export function runBrandNow(paths: string[], strategy: string) { - return requestPostJson(`${API_PREFIX}/api/brand/run`, { paths, strategy }) +export function runBrandNow(files: UploadedFileRef[], strategy: string) { + return requestPostJson( + `${API_PREFIX}/api/brand/run?userId=${encodeURIComponent(String(getCurrentUserId()))}`, + { files, strategy, taskType: 1 }, + ) } -export function createBrandTask(paths: string[], strategy: string) { - return requestPostJson(`${API_PREFIX}/api/brand/tasks?userId=${encodeURIComponent(String(getCurrentUserId()))}`, { paths, strategy }) +export function createBrandTask(files: UploadedFileRef[], strategy: string) { + return requestPostJson( + `${API_PREFIX}/api/brand/tasks?userId=${encodeURIComponent(String(getCurrentUserId()))}`, + { files, strategy, taskType: 2 }, + ) } export function getBrandTasks() { diff --git a/frontend-vue/src/shared/api/types/upload.ts b/frontend-vue/src/shared/api/types/upload.ts index 11b0bc27..84e64664 100644 --- a/frontend-vue/src/shared/api/types/upload.ts +++ b/frontend-vue/src/shared/api/types/upload.ts @@ -2,4 +2,6 @@ export interface UploadedFileRef { fileKey: string; originalFilename?: string; relativePath?: string; + /** 服务器本地临时文件路径(Java 可读),品牌检测等模块用它作为源文件地址 */ + fileUrl?: string; } diff --git a/frontend-vue/src/shared/bridges/pywebview.ts b/frontend-vue/src/shared/bridges/pywebview.ts index 146f6f37..5eabd2c4 100644 --- a/frontend-vue/src/shared/bridges/pywebview.ts +++ b/frontend-vue/src/shared/bridges/pywebview.ts @@ -86,6 +86,10 @@ export interface PywebviewApi { select_folder?: () => Promise; select_brand_xlsx_files?: () => Promise; select_brand_folder?: () => Promise; + /** 本机展开文件夹下的 xlsx 文件(供品牌检测"选择文件夹");新版桌面客户端提供 */ + expand_brand_folder?: ( + folder: string, + ) => Promise<{ success: boolean; items?: Array<{ absolutePath: string; relativePath: string }>; error?: string }>; upload_file_to_java?: ( filePath: string, relativePath?: string,