feat(trademark): 支持商标筛查任务取消状态及优化错误处理- 新增商标筛查取消状态的UI展示和处理逻辑

-优化错误提示信息,区分网络错误、超时和风控场景
- 改进任务进度计算逻辑,支持更准确的完成状态判断
- 调整品牌提取逻辑,从Excel中直接读取品牌列数据
- 增强后端403错误检测和代理自动切换机制
- 更新前端组件样式和交互逻辑以匹配新状态
-修复部分条件判断逻辑以提升稳定性- 调整文件上传大小限制至50MB以支持更大文件- 优化Excel解析工具类,支持自动识别表头行位置
This commit is contained in:
2025-11-05 10:16:14 +08:00
parent a62d7b6147
commit 4e2ce48934
9 changed files with 264 additions and 179 deletions

View File

@@ -16,6 +16,13 @@ export const markApi = {
// 品牌商标筛查
brandCheck(brands: string[]) {
return http.post<{ code: number, data: { total: number, filtered: number, passed: number, data: any[] }, msg: string }>('/tool/mark/brandCheck', brands)
},
// 从Excel提取品牌列表客户端本地接口
extractBrands(file: File) {
const formData = new FormData()
formData.append('file', file)
return http.upload<{ code: number, data: { total: number, brands: string[] }, msg: string }>('/api/trademark/extractBrands', formData)
}
}

View File

@@ -105,7 +105,7 @@ function handleRetryTask() {
<div class="body-layout">
<!-- 左侧步骤栏 (商标筛查有数据时隐藏) -->
<aside v-show="!(currentTab === 'trademark' && (trademarkPanelRef?.queryStatus === 'inProgress' || trademarkPanelRef?.queryStatus === 'done' || trademarkPanelRef?.queryStatus === 'error' || trademarkPanelRef?.queryStatus === 'networkError'))" :class="['steps-sidebar', currentTab === 'trademark' ? 'wide' : 'narrow']">
<aside v-show="!(currentTab === 'trademark' && (trademarkPanelRef?.queryStatus === 'inProgress' || trademarkPanelRef?.queryStatus === 'done' || trademarkPanelRef?.queryStatus === 'error' || trademarkPanelRef?.queryStatus === 'networkError' || trademarkPanelRef?.queryStatus === 'cancel'))" :class="['steps-sidebar', currentTab === 'trademark' ? 'wide' : 'narrow']">
<div class="steps-title">操作流程</div>
<!-- ASIN查询面板 -->
@@ -201,7 +201,7 @@ function handleRetryTask() {
</table>
<!-- 商标筛查进度显示 -->
<div v-if="currentTab === 'trademark' && (trademarkPanelRef?.queryStatus === 'inProgress' || trademarkPanelRef?.queryStatus === 'done' || trademarkPanelRef?.queryStatus === 'error' || trademarkPanelRef?.queryStatus === 'networkError')" class="trademark-progress-container">
<div v-if="currentTab === 'trademark' && (trademarkPanelRef?.queryStatus === 'inProgress' || trademarkPanelRef?.queryStatus === 'done' || trademarkPanelRef?.queryStatus === 'error' || trademarkPanelRef?.queryStatus === 'networkError' || trademarkPanelRef?.queryStatus === 'cancel')" class="trademark-progress-container">
<!-- 进行中状态横幅 -->
<div v-if="trademarkPanelRef?.queryStatus === 'inProgress'" class="status-banner progress-banner">
<div class="banner-icon">
@@ -216,18 +216,34 @@ function handleRetryTask() {
</div>
</div>
<!-- 完成/失败状态横幅样式统一 -->
<div v-if="trademarkPanelRef?.queryStatus === 'done' || trademarkPanelRef?.queryStatus === 'error' || trademarkPanelRef?.queryStatus === 'networkError'" class="status-banner done-banner">
<!-- 取消状态横幅 -->
<div v-if="trademarkPanelRef?.queryStatus === 'cancel'" class="status-banner cancel-banner">
<div class="banner-icon">
<img src="/icon/done1.png" alt="完成" class="icon-image" />
<img src="/icon/cancel.png" alt="已取消" class="icon-image" />
</div>
<div class="banner-content">
<div class="banner-title">筛查已完成</div>
<div class="banner-desc">点击"导出数据"按钮可导出为 Excel 表格文件如出现异常请检查账号地区设置</div>
<div class="banner-title">已取消查询</div>
<div class="banner-desc">您已取消本次查询任务</div>
</div>
<div class="banner-actions">
<el-button size="default" @click="handleNewTask">新建任务</el-button>
<el-button type="primary" size="default">导出数据</el-button>
<el-button type="primary" size="default" @click="handleRetryTask">重新筛查</el-button>
</div>
</div>
<!-- 完成/失败状态横幅 -->
<div v-if="trademarkPanelRef?.queryStatus === 'done' || trademarkPanelRef?.queryStatus === 'error' || trademarkPanelRef?.queryStatus === 'networkError'" class="status-banner done-banner">
<div class="banner-icon">
<img :src="trademarkPanelRef.queryStatus === 'done' ? '/icon/done1.png' : '/icon/error.png'" alt="完成" class="icon-image" />
</div>
<div class="banner-content">
<div class="banner-title">{{ trademarkPanelRef.queryStatus === 'done' ? '筛查已完成' : '数据筛查失败' }}</div>
<div class="banner-desc">{{ trademarkPanelRef.queryStatus === 'done' ? '点击"导出数据"按钮,可导出为 Excel 表格文件。' : (trademarkPanelRef.errorMessage || '请稍后重试') }}</div>
</div>
<div class="banner-actions">
<el-button size="default" @click="handleNewTask">新建任务</el-button>
<el-button v-if="trademarkPanelRef.queryStatus === 'done'" type="primary" size="default">导出数据</el-button>
<el-button v-else type="primary" size="default" @click="handleRetryTask">重新筛查</el-button>
</div>
</div>
@@ -240,22 +256,27 @@ function handleRetryTask() {
<div class="status-column">
<!-- 任务1产品商标筛查 -->
<div class="status-item">
<img v-if="trademarkPanelRef?.queryStatus === 'error' || trademarkPanelRef?.queryStatus === 'networkError'" src="/icon/error.png" alt="筛查失败" class="status-indicator-icon" />
<img v-else-if="(trademarkPanelRef?.taskProgress?.product?.current || 0) >= (trademarkPanelRef?.taskProgress?.product?.total || 1) && (trademarkPanelRef?.taskProgress?.product?.total || 0) > 0" src="/icon/done.png" alt="采集完成" class="status-indicator-icon" />
<img v-else-if="trademarkPanelRef?.queryStatus === 'inProgress'" src="/icon/inProgress.png" alt="采集中" class="status-indicator-icon" />
<img v-if="(trademarkPanelRef?.taskProgress?.product?.total || 0) > 100" src="/icon/done.png" alt="采集完成" class="status-indicator-icon" />
<img v-else-if="((trademarkPanelRef?.taskProgress?.product?.total || 0) > 0 || (trademarkPanelRef?.taskProgress?.product?.current || 0) > 0) && (trademarkPanelRef?.queryStatus === 'error' || trademarkPanelRef?.queryStatus === 'networkError')" src="/icon/error.png" alt="筛查失败" class="status-indicator-icon" />
<img v-else-if="((trademarkPanelRef?.taskProgress?.product?.total || 0) > 0 || (trademarkPanelRef?.taskProgress?.product?.current || 0) > 0) && trademarkPanelRef?.queryStatus === 'cancel'" src="/icon/cancel.png" alt="已取消" class="status-indicator-icon" />
<img v-else-if="(trademarkPanelRef?.taskProgress?.product?.current || 0) > 0" src="/icon/inProgress.png" alt="采集中" class="status-indicator-icon" />
<img v-else src="/icon/acquisition.png" alt="待采集" class="status-indicator-icon" />
</div>
<div class="status-connector"></div>
<!-- 任务2品牌商标筛查 -->
<div class="status-item">
<img v-if="(trademarkPanelRef?.taskProgress?.brand?.current || 0) >= (trademarkPanelRef?.taskProgress?.brand?.total || 1)" src="/icon/done.png" alt="采集完成" class="status-indicator-icon" />
<img v-if="(trademarkPanelRef?.taskProgress?.brand?.total || 0) > 0 && (trademarkPanelRef?.taskProgress?.brand?.current || 0) >= trademarkPanelRef.taskProgress.brand.total" src="/icon/done.png" alt="采集完成" class="status-indicator-icon" />
<img v-else-if="((trademarkPanelRef?.taskProgress?.brand?.total || 0) > 0 || (trademarkPanelRef?.taskProgress?.brand?.current || 0) > 0) && (trademarkPanelRef?.queryStatus === 'error' || trademarkPanelRef?.queryStatus === 'networkError')" src="/icon/error.png" alt="筛查失败" class="status-indicator-icon" />
<img v-else-if="((trademarkPanelRef?.taskProgress?.brand?.total || 0) > 0 || (trademarkPanelRef?.taskProgress?.brand?.current || 0) > 0) && trademarkPanelRef?.queryStatus === 'cancel'" src="/icon/cancel.png" alt="已取消" class="status-indicator-icon" />
<img v-else-if="(trademarkPanelRef?.taskProgress?.brand?.current || 0) > 0" src="/icon/inProgress.png" alt="采集中" class="status-indicator-icon" />
<img v-else src="/icon/acquisition.png" alt="待采集" class="status-indicator-icon" />
</div>
<div class="status-connector"></div>
<!-- 任务3跟卖许可筛查 -->
<div class="status-item">
<img v-if="(trademarkPanelRef?.taskProgress?.platform?.current || 0) >= (trademarkPanelRef?.taskProgress?.platform?.total || 1)" src="/icon/done.png" alt="采集完成" class="status-indicator-icon" />
<img v-if="(trademarkPanelRef?.taskProgress?.platform?.total || 0) > 0 && (trademarkPanelRef?.taskProgress?.platform?.current || 0) >= trademarkPanelRef.taskProgress.platform.total" src="/icon/done.png" alt="采集完成" class="status-indicator-icon" />
<img v-else-if="((trademarkPanelRef?.taskProgress?.platform?.total || 0) > 0 || (trademarkPanelRef?.taskProgress?.platform?.current || 0) > 0) && (trademarkPanelRef?.queryStatus === 'error' || trademarkPanelRef?.queryStatus === 'networkError')" src="/icon/error.png" alt="筛查失败" class="status-indicator-icon" />
<img v-else-if="((trademarkPanelRef?.taskProgress?.platform?.total || 0) > 0 || (trademarkPanelRef?.taskProgress?.platform?.current || 0) > 0) && trademarkPanelRef?.queryStatus === 'cancel'" src="/icon/cancel.png" alt="已取消" class="status-indicator-icon" />
<img v-else-if="(trademarkPanelRef?.taskProgress?.platform?.current || 0) > 0" src="/icon/inProgress.png" alt="采集中" class="status-indicator-icon" />
<img v-else src="/icon/acquisition.png" alt="待采集" class="status-indicator-icon" />
</div>
@@ -267,7 +288,7 @@ function handleRetryTask() {
<div class="task-title-row">
<div class="task-info">
<div class="task-name">{{ trademarkPanelRef?.taskProgress?.product?.label || '未注册/TM商标筛查' }}</div>
<div class="task-desc">{{ trademarkPanelRef?.taskProgress?.product?.desc || '筛查未注册商标或TM标的产品' }}<span v-if="trademarkPanelRef?.queryStatus !== 'inProgress'"> (已完成)</span></div>
<div class="task-desc">{{ trademarkPanelRef?.taskProgress?.product?.desc || '筛查未注册商标或TM标的产品' }}<span v-if="(trademarkPanelRef?.taskProgress?.product?.total || 0) > 0"> (已完成)</span></div>
</div>
</div>
<div class="task-progress-wrapper">
@@ -279,15 +300,15 @@ function handleRetryTask() {
<div class="task-stats">
<div class="task-stat">
<span class="stat-label">查询数量</span>
<span class="stat-value">{{ (trademarkPanelRef?.taskProgress?.product?.total || 0) === 0 ? '-' : trademarkPanelRef.taskProgress.product.total }}</span>
<span class="stat-value">{{ (trademarkPanelRef?.taskProgress?.product?.total || 0) > 100 ? trademarkPanelRef.taskProgress.product.total : '-' }}</span>
</div>
<div class="task-stat highlight">
<span class="stat-label">未注册/TM标</span>
<span class="stat-value">{{ (trademarkPanelRef?.taskProgress?.product?.total || 0) > 0 ? (trademarkPanelRef?.taskProgress?.product?.completed || 0) : '-' }}</span>
<span class="stat-value">{{ (trademarkPanelRef?.taskProgress?.product?.total || 0) > 100 ? (trademarkPanelRef?.taskProgress?.product?.completed || 0) : '-' }}</span>
</div>
<div class="task-stat">
<span class="stat-label">已过滤</span>
<span class="stat-value">{{ (trademarkPanelRef?.taskProgress?.product?.total || 0) > 0 ? ((trademarkPanelRef?.taskProgress?.product?.total || 0) - (trademarkPanelRef?.taskProgress?.product?.completed || 0)) : '-' }}</span>
<span class="stat-value">{{ (trademarkPanelRef?.taskProgress?.product?.total || 0) > 100 ? ((trademarkPanelRef?.taskProgress?.product?.total || 0) - (trademarkPanelRef?.taskProgress?.product?.completed || 0)) : '-' }}</span>
</div>
</div>
</div>
@@ -296,7 +317,7 @@ function handleRetryTask() {
<div class="task-title-row">
<div class="task-info">
<div class="task-name">{{ trademarkPanelRef?.taskProgress?.brand?.label || '品牌商标筛查' }}</div>
<div class="task-desc">{{ trademarkPanelRef?.taskProgress?.brand?.desc || '筛查未注册商标的品牌' }}<span v-if="trademarkPanelRef?.queryStatus !== 'inProgress'"> (已完成)</span></div>
<div class="task-desc">{{ trademarkPanelRef?.taskProgress?.brand?.desc || '筛查未注册商标的品牌' }}<span v-if="(trademarkPanelRef?.taskProgress?.brand?.total || 0) > 0 && (trademarkPanelRef?.taskProgress?.brand?.current || 0) >= trademarkPanelRef.taskProgress.brand.total"> (已完成)</span></div>
</div>
</div>
<div class="task-progress-wrapper">
@@ -312,11 +333,11 @@ function handleRetryTask() {
</div>
<div class="task-stat highlight">
<span class="stat-label">未注册</span>
<span class="stat-value">{{ (trademarkPanelRef?.taskProgress?.brand?.total || 0) > 0 ? (trademarkPanelRef?.taskProgress?.brand?.completed || 0) : '-' }}</span>
<span class="stat-value">{{ ((trademarkPanelRef?.taskProgress?.brand?.current || 0) >= (trademarkPanelRef?.taskProgress?.brand?.total || 1)) ? (trademarkPanelRef?.taskProgress?.brand?.completed || 0) : '-' }}</span>
</div>
<div class="task-stat">
<span class="stat-label">已注册</span>
<span class="stat-value">{{ (trademarkPanelRef?.taskProgress?.brand?.total || 0) > 0 ? ((trademarkPanelRef?.taskProgress?.brand?.total || 0) - (trademarkPanelRef?.taskProgress?.brand?.completed || 0)) : '-' }}</span>
<span class="stat-value">{{ ((trademarkPanelRef?.taskProgress?.brand?.current || 0) >= (trademarkPanelRef?.taskProgress?.brand?.total || 1)) ? ((trademarkPanelRef?.taskProgress?.brand?.total || 0) - (trademarkPanelRef?.taskProgress?.brand?.completed || 0)) : '-' }}</span>
</div>
</div>
</div>
@@ -342,11 +363,11 @@ function handleRetryTask() {
</div>
<div class="task-stat highlight">
<span class="stat-label">可跟卖</span>
<span class="stat-value">{{ (trademarkPanelRef?.taskProgress?.platform?.total || 0) > 0 ? (trademarkPanelRef?.taskProgress?.platform?.completed || 0) : '-' }}</span>
<span class="stat-value">{{ ((trademarkPanelRef?.taskProgress?.platform?.current || 0) >= (trademarkPanelRef?.taskProgress?.platform?.total || 1)) ? (trademarkPanelRef?.taskProgress?.platform?.completed || 0) : '-' }}</span>
</div>
<div class="task-stat">
<span class="stat-label">已过滤</span>
<span class="stat-value">{{ (trademarkPanelRef?.taskProgress?.platform?.total || 0) > 0 ? ((trademarkPanelRef?.taskProgress?.platform?.total || 0) - (trademarkPanelRef?.taskProgress?.platform?.completed || 0)) : '-' }}</span>
<span class="stat-value">{{ ((trademarkPanelRef?.taskProgress?.platform?.current || 0) >= (trademarkPanelRef?.taskProgress?.platform?.total || 1)) ? ((trademarkPanelRef?.taskProgress?.platform?.total || 0) - (trademarkPanelRef?.taskProgress?.platform?.completed || 0)) : '-' }}</span>
</div>
</div>
</div>
@@ -355,9 +376,9 @@ function handleRetryTask() {
</div>
</div>
</div>
<div v-if="paginatedData.length === 0 && !(currentTab === 'trademark' && (trademarkPanelRef?.queryStatus === 'inProgress' || trademarkPanelRef?.queryStatus === 'done' || trademarkPanelRef?.queryStatus === 'error' || trademarkPanelRef?.queryStatus === 'networkError'))" class="empty-abs">
<div v-if="paginatedData.length === 0 && !(currentTab === 'trademark' && (trademarkPanelRef?.queryStatus === 'inProgress' || trademarkPanelRef?.queryStatus === 'done' || trademarkPanelRef?.queryStatus === 'error' || trademarkPanelRef?.queryStatus === 'networkError' || trademarkPanelRef?.queryStatus === 'cancel'))" class="empty-abs">
<!-- 商标筛查状态显示 -->
<div v-if="currentTab === 'trademark' && trademarkPanelRef?.queryStatus && trademarkPanelRef.queryStatus !== 'inProgress' && trademarkPanelRef.queryStatus !== 'done' && trademarkPanelRef.queryStatus !== 'error' && trademarkPanelRef.queryStatus !== 'networkError'" class="empty-container">
<div v-if="currentTab === 'trademark' && trademarkPanelRef?.queryStatus && trademarkPanelRef.queryStatus !== 'inProgress' && trademarkPanelRef.queryStatus !== 'done' && trademarkPanelRef.queryStatus !== 'error' && trademarkPanelRef.queryStatus !== 'networkError' && trademarkPanelRef.queryStatus !== 'cancel'" class="empty-container">
<img
:src="trademarkPanelRef.statusConfig[trademarkPanelRef.queryStatus].icon"
:alt="trademarkPanelRef.statusConfig[trademarkPanelRef.queryStatus].title"
@@ -787,15 +808,12 @@ function handleRetryTask() {
flex: none;
margin-bottom: 16px;
}
.status-banner.progress-banner {
.status-banner.progress-banner,
.status-banner.done-banner,
.status-banner.error-banner,
.status-banner.cancel-banner {
box-shadow: 0px 8px 20px rgba(22, 119, 255, 0.2);
}
.status-banner.done-banner {
box-shadow: 0px 8px 20px rgba(103, 194, 58, 0.2);
}
.status-banner.error-banner {
box-shadow: 0px 8px 20px rgba(245, 108, 108, 0.2);
}
.banner-icon {
flex-shrink: 0;
width: 64px;
@@ -878,7 +896,7 @@ function handleRetryTask() {
width: 24px;
height: 24px;
object-fit: contain;
}
}
.status-indicator-icon[src*="inProgress"] {
animation: spin 1.5s linear infinite;
}

View File

@@ -131,12 +131,15 @@ async function startTrademarkQuery() {
return
}
const needProductCheck = queryTypes.value.includes('product')
const needBrandCheck = queryTypes.value.includes('brand')
trademarkLoading.value = true
trademarkProgress.value = 0
trademarkData.value = []
queryStatus.value = 'inProgress'
// 完全重置任务进度包括total
// 重置任务进度
taskProgress.value.product.total = 0
taskProgress.value.product.current = 0
taskProgress.value.product.completed = 0
@@ -154,10 +157,6 @@ async function startTrademarkQuery() {
let productResult: any = null
let brandList: string[] = []
// 判断是否需要执行产品商标筛查
const needProductCheck = queryTypes.value.includes('product')
const needBrandCheck = queryTypes.value.includes('brand')
if (needProductCheck) {
// 步骤1: 产品商标筛查 - 调用新建任务接口
showMessage('正在上传文件...', 'info')
@@ -169,132 +168,113 @@ async function startTrademarkQuery() {
showMessage('文件上传成功,正在处理...', 'success')
// 步骤2: 轮询检查任务状态最多等待60秒
const taskData = taskProgress.value.product
const maxWaitTime = 60000 // 最多等60秒
const pollInterval = 3000 // 每3秒检查一次
const startTime = Date.now()
taskData.total = 100 // 设置临时总数以显示进度动画
taskData.current = 5 // 立即显示初始进度
let taskResult: any = null
while (Date.now() - startTime < maxWaitTime) {
if (!trademarkLoading.value) return
// 等待一段时间
await new Promise(resolve => setTimeout(resolve, pollInterval))
if (!trademarkLoading.value) return
// 尝试获取任务结果
try {
taskResult = await markApi.getTask()
if (taskResult.code === 200 || taskResult.code === 0) {
// 检查是否有 download_url有则说明任务完成
if (taskResult.data.original?.download_url) {
break
}
}
} catch (err) {
// 继续等待
console.log('等待任务处理中...', err)
}
// 启动进度动画(5-95%)
const progressTimer = setInterval(() => {
if (taskData.current < 95) {
taskData.current = Math.min(taskData.current + 3, 95)
}
}, 500)
if (!trademarkLoading.value) return
// 步骤3: 检查是否成功获取到结果
if (!taskResult || (taskResult.code !== 200 && taskResult.code !== 0)) {
throw new Error('获取任务超时或失败,请重试')
}
if (!taskResult.data.original?.download_url) {
throw new Error('任务处理超时,请稍后重试')
}
productResult = taskResult
// 从后端获取真实的统计数据
taskData.total = taskResult.data.original?.total || 0
taskData.completed = taskResult.data.filtered.length
taskData.current = taskData.total
// 映射后端数据到前端格式
trademarkData.value = taskResult.data.filtered.map((item: any) => ({
name: item['品牌'] || '',
status: item['商标类型'] || '',
class: '',
owner: '',
expireDate: item['注册时间'] || '',
similarity: 0,
// 保留原始数据供后续使用
asin: item['ASIN'],
productImage: item['商品主图']
}))
// 如果需要品牌筛查,从产品结果中提取品牌列表
if (needBrandCheck) {
brandList = taskResult.data.filtered
.map((item: any) => item['品牌'])
.filter((brand: string) => brand && brand.trim())
}
showMessage(`产品筛查完成,共查询 ${taskData.total} 条,筛查出 ${taskData.completed} 条未注册/TM标`, 'success')
}
// 品牌商标筛查
if (needBrandCheck) {
if (!trademarkLoading.value) return
// 如果没有执行产品筛查,需要先上传文件获取品牌列表
if (!needProductCheck) {
showMessage('正在上传文件提取品牌列表...', 'info')
// 调用新建任务接口获取处理后的数据
const createResult = await markApi.newTask(trademarkFile.value)
if (createResult.code !== 200 && createResult.code !== 0) {
throw new Error(createResult.msg || '创建任务失败')
}
// 轮询获取任务结果
// 轮询检查任务状态
const pollTask = async () => {
const maxWaitTime = 60000
const pollInterval = 3000
const startTime = Date.now()
let taskResult: any = null
while (Date.now() - startTime < maxWaitTime) {
if (!trademarkLoading.value) return
if (!trademarkLoading.value) {
clearInterval(progressTimer)
return null
}
await new Promise(resolve => setTimeout(resolve, pollInterval))
if (!trademarkLoading.value) return
if (!trademarkLoading.value) {
clearInterval(progressTimer)
return null
}
try {
taskResult = await markApi.getTask()
if (taskResult.code === 200 || taskResult.code === 0) {
if (taskResult.data.original?.download_url) {
break
clearInterval(progressTimer)
return taskResult
}
}
} catch (err) {
console.log('等待任务处理中...', err)
// 继续等待
}
}
clearInterval(progressTimer)
return taskResult
}
try {
productResult = await pollTask()
if (!trademarkLoading.value) return
if (!taskResult || (taskResult.code !== 200 && taskResult.code !== 0)) {
if (!productResult || (productResult.code !== 200 && productResult.code !== 0)) {
throw new Error('获取任务超时或失败,请重试')
}
if (!taskResult.data.original?.download_url) {
if (!productResult.data.original?.download_url) {
throw new Error('任务处理超时,请稍后重试')
}
// 从结果中提取品牌列表包括TM和未注册的品牌
brandList = taskResult.data.filtered
// 设置真实统计数据
taskData.total = productResult.data.original?.total || 0
taskData.current = taskData.total
taskData.completed = productResult.data.filtered.length
} finally {
clearInterval(progressTimer)
}
// 映射后端数据到前端格式
trademarkData.value = productResult.data.filtered.map((item: any) => ({
name: item['品牌'] || '',
status: item['商标类型'] || '',
class: '',
owner: '',
expireDate: item['注册时间'] || '',
similarity: 0,
asin: item['ASIN'],
productImage: item['商品主图']
}))
// 如果需要品牌筛查,从产品结果中提取品牌列表
if (needBrandCheck) {
brandList = productResult.data.filtered
.map((item: any) => item['品牌'])
.filter((brand: string) => brand && brand.trim())
}
showMessage(`产品筛查完成,共 ${taskData.total} 条,筛查出 ${taskData.completed}`, 'success')
}
// 品牌商标筛查
if (needBrandCheck) {
if (!trademarkLoading.value) return
// 如果没有执行产品筛查需要先从Excel提取品牌列表
if (!needProductCheck) {
showMessage('正在从Excel提取品牌列表...', 'info')
const extractResult = await markApi.extractBrands(trademarkFile.value)
if (extractResult.code !== 200 && extractResult.code !== 0) {
throw new Error(extractResult.msg || '提取品牌列表失败')
}
if (!extractResult.data.brands || extractResult.data.brands.length === 0) {
throw new Error('未能从文件中提取到品牌数据请确保Excel包含"品牌"列')
}
brandList = extractResult.data.brands
showMessage(`品牌列表提取成功,共 ${brandList.length} 个品牌`, 'success')
}
@@ -308,15 +288,13 @@ async function startTrademarkQuery() {
showMessage(`开始品牌商标筛查,共 ${brandList.length} 个品牌...`, 'info')
// 模拟进度动画每秒增加20个
const batchSize = 20
// 模拟进度动画
brandProgressTimer = setInterval(() => {
if (brandData.current < brandList.length * 0.95) {
brandData.current = Math.min(brandData.current + batchSize, brandList.length * 0.95)
brandData.current = Math.min(brandData.current + 20, brandList.length * 0.95)
}
}, 1000)
// 调用品牌筛查接口(浏览器内并发,速度快)
const brandResult = await markApi.brandCheck(brandList)
if (brandProgressTimer) clearInterval(brandProgressTimer)
@@ -352,29 +330,37 @@ async function startTrademarkQuery() {
emit('updateData', trademarkData.value)
let summaryMsg = '筛查完成'
if (needProductCheck) {
const taskData = taskProgress.value.product
summaryMsg += `,产品:${taskData.completed}/${taskData.total}`
}
if (needBrandCheck && brandList.length > 0) {
const brandData = taskProgress.value.brand
summaryMsg += `,品牌:${brandData.completed}/${brandData.total}`
}
if (needProductCheck) summaryMsg += `,产品:${taskProgress.value.product.completed}/${taskProgress.value.product.total}`
if (needBrandCheck && brandList.length > 0) summaryMsg += `,品牌:${taskProgress.value.brand.completed}/${taskProgress.value.brand.total}`
showMessage(summaryMsg, 'success')
}
} catch (error: any) {
if (error.message && error.message.includes('网络')) {
const hasProductData = taskProgress.value.product.total > 0
// 优化错误信息 - 只显示友好提示
let msg = error.message || ''
if (msg.includes('网络') || msg.includes('network')) {
queryStatus.value = 'networkError'
errorMessage.value = '网络连接失败'
errorMessage.value = '网络不可用,请检查你的网络或代理设置'
} else if (msg.includes('超时') || msg.includes('timeout')) {
queryStatus.value = 'error'
errorMessage.value = '数据库维护中,请稍后重试'
} else if (msg.includes('403') || msg.includes('风控')) {
queryStatus.value = 'error'
errorMessage.value = '网站风控限制,请稍后重试'
} else {
queryStatus.value = 'error'
errorMessage.value = error.message || '查询失败'
errorMessage.value = '数据库维护中,请稍后重试'
}
showMessage(errorMessage.value, 'error')
// 失败时清空数据,不显示侧边栏
// 仅在第1步失败时清空数据
if (!hasProductData) {
trademarkData.value = []
emit('updateData', [])
} else {
emit('updateData', trademarkData.value)
}
showMessage(errorMessage.value, 'error')
} finally {
// 清除定时器
if (brandProgressTimer) {
@@ -466,10 +452,13 @@ function resetToIdle() {
trademarkData.value = []
trademarkFileName.value = ''
trademarkFile.value = null
taskProgress.value.product.total = 0
taskProgress.value.product.current = 0
taskProgress.value.product.completed = 0
taskProgress.value.brand.total = 0
taskProgress.value.brand.current = 0
taskProgress.value.brand.completed = 0
taskProgress.value.platform.total = 0
taskProgress.value.platform.current = 0
taskProgress.value.platform.completed = 0
}
@@ -481,8 +470,10 @@ defineExpose({
queryStatus,
statusConfig,
taskProgress,
errorMessage,
resetToIdle,
stopTrademarkQuery
stopTrademarkQuery,
startTrademarkQuery
})
</script>
@@ -493,8 +484,8 @@ defineExpose({
<div class="flow-item">
<div class="step-index">1</div>
<div class="step-card">
<div class="step-header"><div class="title">导入卖家精灵选品表格</div></div>
<div class="desc">在卖家精灵导出文档时必须要勾选导出主图具体操作请<span class="link" @click.prevent="viewTrademarkExample">点击查看</span></div>
<div class="step-header"><div class="title">导入Excel表格</div></div>
<div class="desc">产品筛查需导入卖家精灵选品表格并勾选"导出主图"品牌筛查Excel需包含"品牌"</div>
<div class="dropzone" @click="openTrademarkUpload">
<div class="dz-icon">📤</div>
<div class="dz-text">点击或将文件拖拽到这里上传</div>
@@ -798,6 +789,12 @@ defineExpose({
font-weight: 500;
border-radius: 6px;
}
.start-btn:disabled {
background: #d9d9d9;
border-color: #d9d9d9;
color: #ffffff;
cursor: not-allowed;
}
.stop-btn {
background: #f56c6c;
border-color: #f56c6c;

View File

@@ -1,19 +1,20 @@
package com.tashow.erp.controller;
import com.tashow.erp.utils.ExcelParseUtil;
import com.tashow.erp.utils.JsonData;
import com.tashow.erp.utils.LoggerUtil;
import com.tashow.erp.utils.TrademarkCheckUtil;
import org.slf4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.util.*;
import java.util.concurrent.ThreadLocalRandom;
import java.util.stream.Collectors;
/**
* 商标检查控制器 - 极速版(浏览器内并发)
*/
@RestController
@RequestMapping("/api/trademark")
@CrossOrigin
public class TrademarkController {
private static final Logger logger = LoggerUtil.getLogger(TrademarkController.class);
@@ -31,10 +32,7 @@ public class TrademarkController {
.map(String::trim)
.distinct()
.collect(Collectors.toList());
logger.info("开始检查 {}个品牌", list.size());
long start = System.currentTimeMillis();
// 串行查询(不加延迟)
List<Map<String, Object>> unregistered = new ArrayList<>();
int checkedCount = 0;
@@ -87,4 +85,21 @@ public class TrademarkController {
util.closeDriver();
}
}
/**
* 从Excel提取品牌列表
*/
@PostMapping("/extractBrands")
public JsonData extractBrands(@RequestParam("file") MultipartFile file) {
try {
List<String> brands = ExcelParseUtil.parseColumnByName(file, "品牌");
if (brands.isEmpty()) return JsonData.buildError("未找到品牌列或品牌数据为空");
Map<String, Object> result = new HashMap<>();
result.put("total", brands.size());
result.put("brands", brands);
return JsonData.buildSuccess(result);
} catch (Exception e) {
return JsonData.buildError("提取失败: " + e.getMessage());
}
}
}

View File

@@ -56,7 +56,6 @@ public class ExcelParseUtil {
log.error("解析 Excel 文件失败: {}, 文件名: {}", e.getMessage(),
file.getOriginalFilename(), e);
}
return result;
}
@@ -101,4 +100,45 @@ public class ExcelParseUtil {
return result;
}
/**
* 根据列名解析数据自动适配第1行或第2行为表头
*/
public static List<String> parseColumnByName(MultipartFile file, String columnName) {
List<String> result = new ArrayList<>();
try (InputStream in = file.getInputStream()) {
ExcelReader reader = ExcelUtil.getReader(in, 0);
List<List<Object>> rows = reader.read();
if (rows.isEmpty()) return result;
// 查找表头行和列索引
int headerRow = -1, colIdx = -1;
for (int r = 0; r < Math.min(2, rows.size()); r++) {
for (int c = 0; c < rows.get(r).size(); c++) {
String col = rows.get(r).get(c).toString().replaceAll("\\s+", "");
if (col.equals(columnName)) {
headerRow = r;
colIdx = c;
break;
}
}
if (colIdx != -1) break;
}
if (colIdx == -1) return result;
// 从表头下一行开始读数据
for (int i = headerRow + 1; i < rows.size(); i++) {
List<Object> row = rows.get(i);
if (row.size() > colIdx && row.get(colIdx) != null) {
String val = row.get(colIdx).toString().trim();
if (!val.isEmpty()) result.add(val);
}
}
} catch (Exception e) {
log.error("解析失败", e);
}
return result;
}
}

View File

@@ -6,26 +6,23 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;
/**
* 商标检查工具
* 支持批量并发查询每100次切换代理
* 检测到403时自动切换代理并重试
*/
@Component
public class TrademarkCheckUtil {
@Autowired
private ProxyPool proxyPool;
private ChromeDriver driver;
private final AtomicInteger checkCount = new AtomicInteger(0);
private static final int PROXY_SWITCH_THRESHOLD = 100;
private synchronized void ensureInit() {
if (driver == null) {
for (int i = 0; i < 3; i++) {
for (int i = 0; i < 5; i++) {
try {
driver = SeleniumUtil.createDriver(false, proxyPool.getProxy());
driver.get("https://tmsearch.uspto.gov/search/search-results");
Thread.sleep(5000);
Thread.sleep(6000);
return; // 成功则返回
} catch (Exception e) {
System.err.println("初始化失败(尝试" + (i+1) + "/3: " + e.getMessage());
@@ -42,14 +39,6 @@ public class TrademarkCheckUtil {
public synchronized Map<String, Boolean> batchCheck(List<String> brands) {
ensureInit();
// 每100个切换代理
if (checkCount.addAndGet(brands.size()) >= PROXY_SWITCH_THRESHOLD) {
try { driver.quit(); } catch (Exception e) {}
driver = null;
checkCount.set(0);
ensureInit();
}
// 构建批量查询脚本(带错误诊断)
String script = """
const brands = arguments[0];
@@ -91,6 +80,27 @@ public class TrademarkCheckUtil {
List<Map<String, Object>> results = (List<Map<String, Object>>)
((JavascriptExecutor) driver).executeAsyncScript(script, brands);
// 检测是否有403错误
boolean has403 = results.stream()
.anyMatch(item -> {
String error = (String) item.get("error");
return error != null && error.contains("HTTP 403");
});
// 如果有403切换代理并重试
if (has403) {
System.err.println("检测到403切换代理并重试...");
try { driver.quit(); } catch (Exception e) {}
driver = null;
ensureInit();
// 重新执行查询
@SuppressWarnings("unchecked")
List<Map<String, Object>> retryResults = (List<Map<String, Object>>)
((JavascriptExecutor) driver).executeAsyncScript(script, brands);
results = retryResults;
}
Map<String, Boolean> resultMap = new HashMap<>();
for (Map<String, Object> item : results) {
String brand = (String) item.get("brand");
@@ -117,7 +127,6 @@ public class TrademarkCheckUtil {
if (driver != null) {
try { driver.quit(); } catch (Exception e) {}
driver = null;
checkCount.set(0);
}
}

View File

@@ -10,6 +10,10 @@ javafx:
spring:
main:
lazy-initialization: true
servlet:
multipart:
max-file-size: 50MB
max-request-size: 50MB
datasource:
url: jdbc:sqlite:./data/erp-cache.db?journal_mode=WAL&synchronous=NORMAL&cache_size=10000&temp_store=memory&busy_timeout=30000
driver-class-name: org.sqlite.JDBC

View File

@@ -20,9 +20,7 @@ import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* @Author liwq
* @Date 2025年03月14日 14:38

View File

@@ -10,19 +10,16 @@ import com.ruoyi.system.domain.ClientAccount;
import com.ruoyi.system.mapper.GenmaiAccountMapper;
import com.ruoyi.system.mapper.ClientAccountMapper;
import com.ruoyi.system.service.IGenmaiAccountService;
@RestController
@RequestMapping("/tool/genmai")
@Anonymous
public class GenmaiAccountController {
@Autowired
private IGenmaiAccountService accountService;
@Autowired
private ClientAccountMapper clientAccountMapper;
@Autowired
private GenmaiAccountMapper genmaiAccountMapper;
@GetMapping("/accounts")
public R<?> listAccounts(String name) {
return R.ok(accountService.listSimple(name));