fix(brand): 品牌检测补上传链+新增 /api/brand/run 端点+文件夹展开桥
- 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 鉴权),非代码变更
This commit is contained in:
+11
@@ -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<BrandCrawlPayloadVo> run(
|
||||
@Parameter(description = "用户 ID", required = true) @RequestParam Long userId,
|
||||
@Valid @RequestBody BrandTaskCreateRequest request) {
|
||||
return ApiResponse.success(brandTaskService.createTaskAndBuildPayload(userId, request));
|
||||
}
|
||||
|
||||
@GetMapping("/tasks")
|
||||
@Operation(
|
||||
summary = "获取品牌任务列表",
|
||||
|
||||
@@ -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<string[]>([])
|
||||
const uploadedFiles = ref<UploadFileVo[]>([])
|
||||
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<string | BrandExpandFolderItem>) {
|
||||
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
|
||||
}
|
||||
try {
|
||||
const paths = await bridge.select_brand_xlsx_files()
|
||||
if (!paths?.length) return
|
||||
normalizeSelected(paths)
|
||||
ElMessage.success(`已选择 ${selectedPaths.value.length} 个 Excel 文件`)
|
||||
if (!(await passGuard(checkSelectedFiles(paths, { allowedExtensions: EXCEL_EXTENSIONS })))) return
|
||||
try {
|
||||
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
|
||||
}
|
||||
try {
|
||||
const folder = await bridge.select_brand_folder()
|
||||
if (!folder) return
|
||||
const res = (await expandBrandFolder(folder)) as BrandExpandFolderPathsResponse
|
||||
if (!res.success || !res.paths?.length) {
|
||||
// 新版桌面客户端桥在本机展开文件夹(服务器无法访问用户本机目录)
|
||||
if (!bridge.expand_brand_folder) {
|
||||
ElMessage.warning('当前桌面客户端版本不支持文件夹展开,请使用"选择文件"(可多选)')
|
||||
return
|
||||
}
|
||||
try {
|
||||
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 : '提交失败')
|
||||
|
||||
@@ -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<BrandExpandFolderResponse>(`${API_PREFIX}/api/brand/expand-folder-recursive`, { folder })
|
||||
}
|
||||
|
||||
export function runBrandNow(paths: string[], strategy: string) {
|
||||
return requestPostJson<BrandTaskMutationResponse>(`${API_PREFIX}/api/brand/run`, { paths, strategy })
|
||||
export function runBrandNow(files: UploadedFileRef[], strategy: string) {
|
||||
return requestPostJson<BrandTaskMutationResponse>(
|
||||
`${API_PREFIX}/api/brand/run?userId=${encodeURIComponent(String(getCurrentUserId()))}`,
|
||||
{ files, strategy, taskType: 1 },
|
||||
)
|
||||
}
|
||||
|
||||
export function createBrandTask(paths: string[], strategy: string) {
|
||||
return requestPostJson<BrandTaskMutationResponse>(`${API_PREFIX}/api/brand/tasks?userId=${encodeURIComponent(String(getCurrentUserId()))}`, { paths, strategy })
|
||||
export function createBrandTask(files: UploadedFileRef[], strategy: string) {
|
||||
return requestPostJson<BrandTaskMutationResponse>(
|
||||
`${API_PREFIX}/api/brand/tasks?userId=${encodeURIComponent(String(getCurrentUserId()))}`,
|
||||
{ files, strategy, taskType: 2 },
|
||||
)
|
||||
}
|
||||
|
||||
export function getBrandTasks() {
|
||||
|
||||
@@ -2,4 +2,6 @@ export interface UploadedFileRef {
|
||||
fileKey: string;
|
||||
originalFilename?: string;
|
||||
relativePath?: string;
|
||||
/** 服务器本地临时文件路径(Java 可读),品牌检测等模块用它作为源文件地址 */
|
||||
fileUrl?: string;
|
||||
}
|
||||
|
||||
@@ -86,6 +86,10 @@ export interface PywebviewApi {
|
||||
select_folder?: () => Promise<string | null>;
|
||||
select_brand_xlsx_files?: () => Promise<string[]>;
|
||||
select_brand_folder?: () => Promise<string | null>;
|
||||
/** 本机展开文件夹下的 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,
|
||||
|
||||
Reference in New Issue
Block a user