feat(trademark):优化商标查询功能和Excel解析逻辑

- 重构品牌商标缓存服务,移除冗余的日志记录和存在检查- 简化Excel解析工具类,提取公共方法并优化列索引查找逻辑
- 增强Electron客户端开发模式下的后端启动控制能力
- 改进商标筛查面板的用户体验和数据处理流程-优化商标查询工具类,提高查询准确性和稳定性
- 调整商标控制器接口参数校验逻辑和资源清理机制
- 更新USPTO API测试用例以支持Spring容器环境运行
This commit is contained in:
2025-11-13 14:20:12 +08:00
parent cfb9096788
commit 007799fb2a
7 changed files with 402 additions and 527 deletions

View File

@@ -434,6 +434,19 @@ app.whenReady().then(() => {
createWindow();
createTray(mainWindow);
// 开发模式快捷键
if (isDev && mainWindow) {
mainWindow.webContents.on('before-input-event', (event, input) => {
if (input.control && input.shift && input.key.toLowerCase() === 'd') {
console.log('[开发模式] 手动跳过后端启动');
openAppIfNotOpened();
} else if (input.control && input.shift && input.key.toLowerCase() === 's') {
console.log('[开发模式] 手动启动后端服务');
startSpringBoot();
}
});
}
// 只有在不需要最小化启动时才显示 splash 窗口
if (!shouldMinimize) {
@@ -476,9 +489,22 @@ app.whenReady().then(() => {
}
console.log('[启动流程] 准备启动 Spring Boot...');
setTimeout(() => {
startSpringBoot();
}, 200);
// 开发模式:添加快捷键跳过后端启动
if (isDev) {
console.log('[开发模式] 按 Ctrl+Shift+D 跳过后端启动,直接进入应用');
console.log('[开发模式] 按 Ctrl+Shift+S 手动启动后端服务');
// 5秒后自动跳过避免卡死
setTimeout(() => {
console.log('[开发模式] 自动跳过后端启动,直接进入应用');
openAppIfNotOpened();
}, 5000);
}
// setTimeout(() => {
// startSpringBoot();
// }, 200);
app.on('activate', () => {
if (mainWindow && !mainWindow.isDestroyed()) {
@@ -847,6 +873,26 @@ ipcMain.handle('set-launch-config', (event, launchConfig: { autoLaunch: boolean;
// 刷新页面
ipcMain.handle('reload', () => mainWindow?.webContents.reload());
// 开发模式:跳过后端启动
ipcMain.handle('dev-skip-backend', () => {
if (isDev) {
console.log('[开发模式] 前端请求跳过后端启动');
openAppIfNotOpened();
return { success: true };
}
return { success: false, error: '仅开发模式可用' };
});
// 开发模式:手动启动后端
ipcMain.handle('dev-start-backend', () => {
if (isDev) {
console.log('[开发模式] 前端请求启动后端');
startSpringBoot();
return { success: true };
}
return { success: false, error: '仅开发模式可用' };
});
// 窗口控制 API
ipcMain.handle('window-minimize', () => {
if (mainWindow && !mainWindow.isDestroyed()) {

View File

@@ -116,18 +116,11 @@ function showMessage(message: string, type: 'success' | 'warning' | 'error' | 'i
// 拖拽上传
async function processTrademarkFile(file: File) {
uploadLoading.value = true
try {
// 根据选中的查询类型确定需要的表头
const requiredHeaders: string[] = []
if (queryTypes.value.includes('product')) {
requiredHeaders.push('商品主图')
}
if (queryTypes.value.includes('brand')) {
requiredHeaders.push('品牌')
}
const requiredHeaders = []
if (queryTypes.value.includes('product')) requiredHeaders.push('商品主图')
if (queryTypes.value.includes('brand')) requiredHeaders.push('品牌')
// 验证表头
if (requiredHeaders.length > 0) {
const validateResult = await markApi.validateHeaders(file, requiredHeaders)
if (validateResult.code !== 200 && validateResult.code !== 0) {
@@ -160,15 +153,11 @@ function removeTrademarkFile() {
trademarkFileName.value = ''
trademarkFile.value = null
uploadLoading.value = false
if (trademarkUpload.value) {
trademarkUpload.value.value = ''
}
if (trademarkUpload.value) trademarkUpload.value.value = ''
}
// 保存会话到后端
async function saveSession() {
if (!trademarkData.value.length) return
try {
const sessionData = {
fileName: trademarkFileName.value,
@@ -178,7 +167,6 @@ async function saveSession() {
taskProgress: taskProgress.value,
queryStatus: queryStatus.value
}
const result = await markApi.saveSession(sessionData)
if (result.code === 200 || result.code === 0) {
const username = getUsernameFromToken()
@@ -258,13 +246,10 @@ async function handleTrademarkUpload(e: Event) {
const input = e.target as HTMLInputElement
const file = input.files?.[0]
if (!file) return
const ok = /\.xlsx?$/.test(file.name)
if (!ok) {
if (!/\.xlsx?$/.test(file.name)) {
showMessage('仅支持 .xlsx/.xls 文件', 'warning')
return
}
await processTrademarkFile(file)
input.value = ''
}
@@ -274,9 +259,7 @@ async function startTrademarkQuery() {
showMessage('请先导入商标列表', 'warning')
return
}
if (refreshVipStatus) await refreshVipStatus()
if (!props.isVip) {
if (vipStatus) trialExpiredType.value = vipStatus.value.expiredType
showTrialExpiredDialog.value = true
@@ -286,6 +269,7 @@ async function startTrademarkQuery() {
const needProductCheck = queryTypes.value.includes('product')
const needBrandCheck = queryTypes.value.includes('brand')
// 立即显示加载状态,防止重复点击
trademarkLoading.value = true
trademarkProgress.value = 0
trademarkData.value = []
@@ -293,6 +277,9 @@ async function startTrademarkQuery() {
trademarkHeaders.value = []
queryStatus.value = 'inProgress'
// 立即显示"正在准备..."状态
showMessage('正在准备筛查任务...', 'info')
// 重置任务进度
taskProgress.value.product.total = 0
taskProgress.value.product.current = 0
@@ -476,11 +463,16 @@ async function startTrademarkQuery() {
if (brandList.length > 0) {
const brandData = taskProgress.value.brand
brandData.total = brandList.length
brandData.current = 1 // 立即显示初始进度
brandData.current = 0
brandData.completed = 0
// 生成任务ID并轮询真实进度
// 生成任务ID并立即开始轮询
brandTaskId.value = `task_${Date.now()}`
// 立即显示开始状态
showMessage(`开始品牌商标筛查,共${brandList.length}个品牌`, 'success')
// 立即开始进度轮询
brandProgressTimer = setInterval(async () => {
try {
const res = await markApi.getBrandCheckProgress(brandTaskId.value)
@@ -490,72 +482,69 @@ async function startTrademarkQuery() {
} catch (e) {
// 忽略进度查询错误
}
}, 1000)
}, 500)
const brandResult = await markApi.brandCheck(brandList, brandTaskId.value)
if (brandProgressTimer) clearInterval(brandProgressTimer)
if (brandResult.code === 200 || brandResult.code === 0) {
// 完成显示100%
if ( brandResult.code === 0) {
brandData.total = brandResult.data.checked || brandResult.data.total || brandData.total
brandData.current = brandData.total
brandData.completed = brandResult.data.unregistered || 0
isBrandTaskRealData.value = true
// 提取未注册品牌列表
const unregisteredBrands = brandResult.data.data.map((item: any) => item.brand).filter(Boolean)
if (unregisteredBrands.length > 0) {
// 从原始Excel中过滤出包含这些品牌的完整行
const filterResult = await markApi.filterByBrands(trademarkFile.value, unregisteredBrands)
if (filterResult.code === 200 || filterResult.code === 0) {
// 保存完整数据(用于导出,使用原始表头)
trademarkFullData.value = filterResult.data.filteredRows
trademarkHeaders.value = filterResult.data.headers || []
// 更新统计:显示过滤出的实际行数(而不是品牌数)
brandData.completed = filterResult.data.filteredRows.length
isBrandTaskRealData.value = true
// 将品牌筛查结果作为展示数据
const brandItems = filterResult.data.filteredRows.map((row: any) => ({
name: row['品牌'] || '',
status: '未注册',
class: '',
owner: '',
expireDate: row['注册时间'] || '',
similarity: 0,
asin: row['ASIN'] || '',
productImage: row['商品主图'] || '',
isBrand: true // 标记为品牌数据
}))
// 如果有产品筛查,也替换展示数据(只显示品牌筛查结果)
if (needProductCheck) {
trademarkData.value = brandItems
} else {
trademarkData.value = [...trademarkData.value, ...brandItems]
}
}
}
await processBrandResult(brandResult)
} else {
throw new Error(brandResult.msg || '品牌筛查失败')
}
}
}
// 只要流程正常完成就设置为done状态不再依赖trademarkLoading
queryStatus.value = 'done'
emit('updateData', trademarkData.value)
// 处理品牌查询结果的函数
async function processBrandResult(brandResult: any) {
// 提取未注册品牌列表
const unregisteredBrands = brandResult.data.data.map((item: any) => item.brand).filter(Boolean)
let summaryMsg = '筛查完成'
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')
// 保存会话
await saveSession()
if (unregisteredBrands.length > 0) {
// 从原始Excel中过滤出包含这些品牌的完整行
const filterResult = await markApi.filterByBrands(trademarkFile.value, unregisteredBrands)
// 保存完整数据(用于导出,使用原始表头)
trademarkFullData.value = filterResult.data.filteredRows
trademarkHeaders.value = filterResult.data.headers || []
// 更新统计:显示过滤出的实际行数(而不是品牌数)
const brandData = taskProgress.value.brand
brandData.completed = filterResult.data.filteredRows.length
isBrandTaskRealData.value = true
// 将品牌筛查结果作为展示数据
const brandItems = filterResult.data.filteredRows.map((row: any) => ({
name: row['品牌'] || '',
status: '未注册',
class: '',
owner: '',
expireDate: row['注册时间'] || '',
similarity: 0,
asin: row['ASIN'] || '',
productImage: row['商品主图'] || '',
isBrand: true // 标记为品牌数据
}))
// 更新展示数据
trademarkData.value = [...trademarkData.value, ...brandItems]
}
}
// 只要流程正常完成就设置为done状态
queryStatus.value = 'done'
emit('updateData', trademarkData.value)
let summaryMsg = '筛查完成'
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')
// 保存会话
await saveSession()
} catch (error: any) {
const hasProductData = isProductTaskRealData.value && taskProgress.value.product.total > 0
const hasBrandData = isBrandTaskRealData.value && taskProgress.value.brand.total > 0
@@ -715,27 +704,15 @@ function resetToIdle() {
trademarkHeaders.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
// 重置真实数据标记
Object.assign(taskProgress.value.product, { total: 0, current: 0, completed: 0 })
Object.assign(taskProgress.value.brand, { total: 0, current: 0, completed: 0 })
Object.assign(taskProgress.value.platform, { total: 0, current: 0, completed: 0 })
isProductTaskRealData.value = false
isBrandTaskRealData.value = false
// 清空localStorage中的会话数据
try {
const username = getUsernameFromToken()
localStorage.removeItem(`trademark_session_${username}`)
} catch (e) {
// 忽略错误
}
} catch (e) {}
}
defineExpose({