feat(amazon): 实现商标筛查功能并优化用户体验
- 添加商标筛查面板和相关API接口- 实现Excel文件解析和数据过滤功能 - 添加文件上传进度跟踪和错误处理-优化空状态显示和操作引导- 实现tab状态持久化存储 - 添加订阅会员弹窗和付费入口 -优化文件选择和删除功能 - 改进UI样式和响应式布局
This commit is contained in:
Binary file not shown.
|
Before Width: | Height: | Size: 3.1 MiB After Width: | Height: | Size: 4.6 KiB |
@@ -211,7 +211,7 @@ function startSpringBoot() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// startSpringBoot();
|
startSpringBoot();
|
||||||
function stopSpringBoot() {
|
function stopSpringBoot() {
|
||||||
if (!springProcess) return;
|
if (!springProcess) return;
|
||||||
try {
|
try {
|
||||||
@@ -347,10 +347,10 @@ app.whenReady().then(() => {
|
|||||||
splashWindow.loadFile(splashPath);
|
splashWindow.loadFile(splashPath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
//666
|
||||||
setTimeout(() => {
|
// setTimeout(() => {
|
||||||
openAppIfNotOpened();
|
// openAppIfNotOpened();
|
||||||
}, 100);
|
// }, 100);
|
||||||
|
|
||||||
app.on('activate', () => {
|
app.on('activate', () => {
|
||||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
export type HttpMethod = 'GET' | 'POST' | 'DELETE';
|
export type HttpMethod = 'GET' | 'POST' | 'DELETE';
|
||||||
export const CONFIG = {
|
export const CONFIG = {
|
||||||
CLIENT_BASE: 'http://localhost:8081',
|
CLIENT_BASE: 'http://localhost:8081',
|
||||||
// RUOYI_BASE: 'http://8.138.23.49:8085',
|
RUOYI_BASE: 'http://8.138.23.49:8085',
|
||||||
RUOYI_BASE: 'http://192.168.1.89:8085',
|
//RUOYI_BASE: 'http://192.168.1.89:8085',
|
||||||
SSE_URL: 'http://192.168.1.89:8085/monitor/account/events'
|
SSE_URL: 'http://8.138.23.49:8085/monitor/account/events'
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
function resolveBase(path: string): string {
|
function resolveBase(path: string): string {
|
||||||
@@ -31,8 +31,18 @@ async function getToken(): Promise<string> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function getUsername(): Promise<string> {
|
||||||
|
try {
|
||||||
|
const tokenModule = await import('../utils/token');
|
||||||
|
return tokenModule.getUsernameFromToken() || '';
|
||||||
|
} catch {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function request<T>(path: string, options: RequestInit & { signal?: AbortSignal }): Promise<T> {
|
async function request<T>(path: string, options: RequestInit & { signal?: AbortSignal }): Promise<T> {
|
||||||
const token = await getToken();
|
const token = await getToken();
|
||||||
|
const username = await getUsername();
|
||||||
let res: Response;
|
let res: Response;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -43,6 +53,7 @@ async function request<T>(path: string, options: RequestInit & { signal?: AbortS
|
|||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json;charset=UTF-8',
|
'Content-Type': 'application/json;charset=UTF-8',
|
||||||
...(token ? { 'Authorization': `Bearer ${token}` } : {}),
|
...(token ? { 'Authorization': `Bearer ${token}` } : {}),
|
||||||
|
...(username ? { 'username': username } : {}),
|
||||||
...options.headers
|
...options.headers
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -90,6 +101,7 @@ export const http = {
|
|||||||
|
|
||||||
async upload<T>(path: string, form: FormData, signal?: AbortSignal) {
|
async upload<T>(path: string, form: FormData, signal?: AbortSignal) {
|
||||||
const token = await getToken();
|
const token = await getToken();
|
||||||
|
const username = await getUsername();
|
||||||
let res: Response;
|
let res: Response;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -98,7 +110,10 @@ export const http = {
|
|||||||
body: form,
|
body: form,
|
||||||
credentials: 'omit',
|
credentials: 'omit',
|
||||||
cache: 'no-store',
|
cache: 'no-store',
|
||||||
headers: token ? { 'Authorization': `Bearer ${token}` } : {},
|
headers: {
|
||||||
|
...(token ? { 'Authorization': `Bearer ${token}` } : {}),
|
||||||
|
...(username ? { 'username': username } : {})
|
||||||
|
},
|
||||||
signal
|
signal
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
@@ -8,21 +8,108 @@ export const markApi = {
|
|||||||
return http.upload<{ code: number, data: any, msg: string }>('/tool/mark/newTask', formData)
|
return http.upload<{ code: number, data: any, msg: string }>('/tool/mark/newTask', formData)
|
||||||
},
|
},
|
||||||
|
|
||||||
// 获取任务列表及筛选数据
|
// 获取任务列表及筛选数据(返回完整行数据和表头)
|
||||||
getTask() {
|
getTask() {
|
||||||
return http.get<{ code: number, data: { original: any, filtered: any[] }, msg: string }>('/tool/mark/task')
|
return http.get<{
|
||||||
|
code: number,
|
||||||
|
data: {
|
||||||
|
original: any,
|
||||||
|
filtered: Record<string, any>[], // 完整的行数据(Map格式)
|
||||||
|
headers: string[] // 表头
|
||||||
|
},
|
||||||
|
msg: string
|
||||||
|
}>('/tool/mark/task')
|
||||||
},
|
},
|
||||||
|
|
||||||
// 品牌商标筛查
|
// 品牌商标筛查
|
||||||
brandCheck(brands: string[]) {
|
brandCheck(brands: string[], taskId?: string) {
|
||||||
return http.post<{ code: number, data: { total: number, filtered: number, passed: number, data: any[] }, msg: string }>('/tool/mark/brandCheck', brands)
|
return http.post<{ code: number, data: { total: number, checked: number, registered: number, unregistered: number, failed: number, data: any[], duration: string }, msg: string }>('/api/trademark/brandCheck', { brands, taskId })
|
||||||
},
|
},
|
||||||
|
|
||||||
// 从Excel提取品牌列表(客户端本地接口)
|
// 查询品牌筛查进度
|
||||||
|
getBrandCheckProgress(taskId: string) {
|
||||||
|
return http.get<{ code: number, data: { current: number }, msg: string }>('/api/trademark/brandCheckProgress', { taskId })
|
||||||
|
},
|
||||||
|
|
||||||
|
// 取消品牌筛查任务
|
||||||
|
cancelBrandCheck(taskId: string) {
|
||||||
|
return http.post<{ code: number, data: string, msg: string }>('/api/trademark/cancelBrandCheck', { taskId })
|
||||||
|
},
|
||||||
|
|
||||||
|
// 验证Excel表头
|
||||||
|
validateHeaders(file: File, requiredHeaders?: string[]) {
|
||||||
|
const formData = new FormData()
|
||||||
|
formData.append('file', file)
|
||||||
|
if (requiredHeaders && requiredHeaders.length > 0) {
|
||||||
|
formData.append('requiredHeaders', JSON.stringify(requiredHeaders))
|
||||||
|
}
|
||||||
|
return http.upload<{
|
||||||
|
code: number,
|
||||||
|
data: {
|
||||||
|
headers: string[],
|
||||||
|
valid?: boolean,
|
||||||
|
missing?: string[]
|
||||||
|
},
|
||||||
|
msg: string
|
||||||
|
}>('/api/trademark/validateHeaders', formData)
|
||||||
|
},
|
||||||
|
|
||||||
|
// 从Excel提取品牌列表(客户端本地接口,返回完整Excel数据)
|
||||||
extractBrands(file: File) {
|
extractBrands(file: File) {
|
||||||
const formData = new FormData()
|
const formData = new FormData()
|
||||||
formData.append('file', file)
|
formData.append('file', file)
|
||||||
return http.upload<{ code: number, data: { total: number, brands: string[] }, msg: string }>('/api/trademark/extractBrands', formData)
|
return http.upload<{
|
||||||
|
code: number,
|
||||||
|
data: {
|
||||||
|
total: number,
|
||||||
|
brands: string[],
|
||||||
|
headers: string[],
|
||||||
|
allRows: Record<string, any>[]
|
||||||
|
},
|
||||||
|
msg: string
|
||||||
|
}>('/api/trademark/extractBrands', formData)
|
||||||
|
},
|
||||||
|
|
||||||
|
// 根据ASIN列表从Excel中过滤完整行数据(客户端本地接口)
|
||||||
|
filterByAsins(file: File, asins: string[]) {
|
||||||
|
const formData = new FormData()
|
||||||
|
formData.append('file', file)
|
||||||
|
formData.append('asins', JSON.stringify(asins))
|
||||||
|
return http.upload<{
|
||||||
|
code: number,
|
||||||
|
data: {
|
||||||
|
headers: string[],
|
||||||
|
filteredRows: Record<string, any>[],
|
||||||
|
total: number
|
||||||
|
},
|
||||||
|
msg: string
|
||||||
|
}>('/api/trademark/filterByAsins', formData)
|
||||||
|
},
|
||||||
|
|
||||||
|
// 根据品牌列表从Excel中过滤完整行数据(客户端本地接口)
|
||||||
|
filterByBrands(file: File, brands: string[]) {
|
||||||
|
const formData = new FormData()
|
||||||
|
formData.append('file', file)
|
||||||
|
formData.append('brands', JSON.stringify(brands))
|
||||||
|
return http.upload<{
|
||||||
|
code: number,
|
||||||
|
data: {
|
||||||
|
headers: string[],
|
||||||
|
filteredRows: Record<string, any>[],
|
||||||
|
total: number
|
||||||
|
},
|
||||||
|
msg: string
|
||||||
|
}>('/api/trademark/filterByBrands', formData)
|
||||||
|
},
|
||||||
|
|
||||||
|
// 保存查询会话
|
||||||
|
saveSession(sessionData: any) {
|
||||||
|
return http.post<{ code: number, data: { sessionId: string }, msg: string }>('/api/trademark/saveSession', sessionData)
|
||||||
|
},
|
||||||
|
|
||||||
|
// 恢复查询会话
|
||||||
|
getSession(sessionId: string) {
|
||||||
|
return http.get<{ code: number, data: any, msg: string }>('/api/trademark/getSession', { sessionId })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,18 +1,48 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed } from 'vue'
|
import { ref, computed, watch, defineAsyncComponent } from 'vue'
|
||||||
|
import { getUsernameFromToken } from '../../utils/token'
|
||||||
import AsinQueryPanel from './AsinQueryPanel.vue'
|
import AsinQueryPanel from './AsinQueryPanel.vue'
|
||||||
import GenmaiSpiritPanel from './GenmaiSpiritPanel.vue'
|
import GenmaiSpiritPanel from './GenmaiSpiritPanel.vue'
|
||||||
import TrademarkCheckPanel from './TrademarkCheckPanel.vue'
|
import TrademarkCheckPanel from './TrademarkCheckPanel.vue'
|
||||||
|
|
||||||
|
const TrialExpiredDialog = defineAsyncComponent(() => import('../common/TrialExpiredDialog.vue'))
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
isVip: boolean
|
isVip: boolean
|
||||||
}>()
|
}>()
|
||||||
const currentTab = ref<'asin' | 'genmai' | 'trademark'>('asin')
|
|
||||||
|
// 从localStorage恢复tab状态
|
||||||
|
function getInitialTab(): 'asin' | 'genmai' | 'trademark' {
|
||||||
|
try {
|
||||||
|
const username = getUsernameFromToken()
|
||||||
|
const saved = localStorage.getItem(`amazon_tab_${username}`)
|
||||||
|
if (saved && ['asin', 'genmai', 'trademark'].includes(saved)) {
|
||||||
|
return saved as 'asin' | 'genmai' | 'trademark'
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// 忽略错误
|
||||||
|
}
|
||||||
|
return 'asin'
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentTab = ref<'asin' | 'genmai' | 'trademark'>(getInitialTab())
|
||||||
|
|
||||||
|
// 监听tab切换,保存到localStorage
|
||||||
|
watch(currentTab, (newTab) => {
|
||||||
|
try {
|
||||||
|
const username = getUsernameFromToken()
|
||||||
|
localStorage.setItem(`amazon_tab_${username}`, newTab)
|
||||||
|
} catch (e) {
|
||||||
|
// 忽略错误
|
||||||
|
}
|
||||||
|
})
|
||||||
const asinPanelRef = ref<any>(null)
|
const asinPanelRef = ref<any>(null)
|
||||||
const trademarkPanelRef = ref<any>(null)
|
const trademarkPanelRef = ref<any>(null)
|
||||||
const currentPage = ref(1)
|
const currentPage = ref(1)
|
||||||
const pageSize = ref(15)
|
const pageSize = ref(15)
|
||||||
const localProductData = ref<any[]>([])
|
const localProductData = ref<any[]>([])
|
||||||
const trademarkData = ref<any[]>([])
|
const trademarkData = ref<any[]>([])
|
||||||
|
const showSubscribeDialog = ref(false)
|
||||||
const paginatedData = computed(() => {
|
const paginatedData = computed(() => {
|
||||||
const start = (currentPage.value - 1) * pageSize.value
|
const start = (currentPage.value - 1) * pageSize.value
|
||||||
const end = start + pageSize.value
|
const end = start + pageSize.value
|
||||||
@@ -82,6 +112,16 @@ function handleRetryTask() {
|
|||||||
trademarkPanelRef.value.startTrademarkQuery()
|
trademarkPanelRef.value.startTrademarkQuery()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function openSubscribeDialog() {
|
||||||
|
showSubscribeDialog.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleExportData() {
|
||||||
|
if (trademarkPanelRef.value && typeof trademarkPanelRef.value.exportTrademarkData === 'function') {
|
||||||
|
trademarkPanelRef.value.exportTrademarkData()
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -170,7 +210,7 @@ function handleRetryTask() {
|
|||||||
|
|
||||||
<div class="info-box">
|
<div class="info-box">
|
||||||
<div class="info-title">收费标准</div>
|
<div class="info-title">收费标准</div>
|
||||||
<div class="info-text">目前免费试用3天,付费会员无使用限制。<a href="#" class="info-link">点击付费会员</a></div>
|
<div class="info-text">目前免费试用3天,付费会员无使用限制。<a href="#" class="info-link" @click.prevent="openSubscribeDialog">点击付费会员</a></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -242,7 +282,7 @@ function handleRetryTask() {
|
|||||||
</div>
|
</div>
|
||||||
<div class="banner-actions">
|
<div class="banner-actions">
|
||||||
<el-button size="default" @click="handleNewTask">新建任务</el-button>
|
<el-button size="default" @click="handleNewTask">新建任务</el-button>
|
||||||
<el-button v-if="trademarkPanelRef.queryStatus === 'done'" type="primary" size="default">导出数据</el-button>
|
<el-button v-if="trademarkPanelRef.queryStatus === 'done'" type="primary" size="default" @click="handleExportData">导出数据</el-button>
|
||||||
<el-button v-else type="primary" size="default" @click="handleRetryTask">重新筛查</el-button>
|
<el-button v-else type="primary" size="default" @click="handleRetryTask">重新筛查</el-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -378,15 +418,27 @@ function handleRetryTask() {
|
|||||||
</div>
|
</div>
|
||||||
<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="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' && trademarkPanelRef.queryStatus !== 'cancel'" 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="trademark-empty-wrapper">
|
||||||
<img
|
<div class="trademark-empty-content">
|
||||||
:src="trademarkPanelRef.statusConfig[trademarkPanelRef.queryStatus].icon"
|
<img src="/image/img.png" alt="暂无数据" class="empty-image" />
|
||||||
:alt="trademarkPanelRef.statusConfig[trademarkPanelRef.queryStatus].title"
|
<div class="empty-text">暂无数据,请按左侧流程操作</div>
|
||||||
class="status-image"
|
</div>
|
||||||
/>
|
|
||||||
<div class="empty-title">{{ trademarkPanelRef.statusConfig[trademarkPanelRef.queryStatus].title }}</div>
|
<div class="trademark-info-boxes">
|
||||||
<div v-if="trademarkPanelRef.statusConfig[trademarkPanelRef.queryStatus].desc" class="empty-desc">
|
<div class="info-box">
|
||||||
{{ trademarkPanelRef.statusConfig[trademarkPanelRef.queryStatus].desc }}
|
<div class="info-title">功能说明</div>
|
||||||
|
<div class="info-text">从卖家精灵平台导出的产品表格,通过此功能,可以快速筛查文档中所有产品、品牌的商标注册状态以及是否允许跟卖。</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="info-box">
|
||||||
|
<div class="info-title">需要注意</div>
|
||||||
|
<div class="info-text">请确保卖家精灵选品表、亚马逊账号与选择的专利地区一致,如不一致可能导致查询结果不准确或失败。</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="info-box">
|
||||||
|
<div class="info-title">收费标准</div>
|
||||||
|
<div class="info-text">目前免费用户可以体验3天,付费会员无使用限制,点击<a href="#" class="info-link" @click.prevent="openSubscribeDialog">加入付费会员</a></div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- 其他tab的加载状态 -->
|
<!-- 其他tab的加载状态 -->
|
||||||
@@ -394,8 +446,8 @@ function handleRetryTask() {
|
|||||||
<div class="spinner">⟳</div>
|
<div class="spinner">⟳</div>
|
||||||
<div>加载中...</div>
|
<div>加载中...</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- 默认空状态 -->
|
<!-- 默认空状态(跟卖精灵tab不显示) -->
|
||||||
<div v-else class="empty-container">
|
<div v-else-if="currentTab !== 'genmai'" class="empty-container">
|
||||||
<img src="/image/img.png" alt="暂无数据" class="empty-image" />
|
<img src="/image/img.png" alt="暂无数据" class="empty-image" />
|
||||||
<div class="empty-text">暂无数据,请按左侧流程操作</div>
|
<div class="empty-text">暂无数据,请按左侧流程操作</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -416,6 +468,9 @@ function handleRetryTask() {
|
|||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 订阅会员弹窗 -->
|
||||||
|
<TrialExpiredDialog v-model="showSubscribeDialog" expired-type="subscribe" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -713,6 +768,36 @@ function handleRetryTask() {
|
|||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 商标筛查空状态样式 */
|
||||||
|
.trademark-empty-wrapper {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
.trademark-empty-content {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.trademark-info-boxes {
|
||||||
|
display: flex;
|
||||||
|
text-align: left;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: flex-start;
|
||||||
|
padding: 8px 0;
|
||||||
|
gap: 10px;
|
||||||
|
width: 90%;
|
||||||
|
max-width: 872px;
|
||||||
|
margin: 0 auto;
|
||||||
|
background: rgba(0, 0, 0, 0.02);
|
||||||
|
border: 1px solid rgba(0, 0, 0, 0.06);
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
.genmai-image-wrapper {
|
.genmai-image-wrapper {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -44,6 +44,14 @@ function showMessage(message: string, type: 'success' | 'warning' | 'error' | 'i
|
|||||||
ElMessage({ message, type })
|
ElMessage({ message, type })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function removeSelectedFile() {
|
||||||
|
selectedFileName.value = ''
|
||||||
|
pendingAsins.value = []
|
||||||
|
if (amazonUpload.value) {
|
||||||
|
amazonUpload.value.value = ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function processExcelFile(file: File) {
|
async function processExcelFile(file: File) {
|
||||||
try {
|
try {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
@@ -275,6 +283,7 @@ defineExpose({
|
|||||||
<div v-if="selectedFileName" class="file-chip">
|
<div v-if="selectedFileName" class="file-chip">
|
||||||
<span class="dot"></span>
|
<span class="dot"></span>
|
||||||
<span class="name">{{ selectedFileName }}</span>
|
<span class="name">{{ selectedFileName }}</span>
|
||||||
|
<span class="delete-btn" @click="removeSelectedFile" title="删除文件">🗑️</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -352,7 +361,7 @@ defineExpose({
|
|||||||
.steps-flow:before { content: ''; position: absolute; left: 13px; top: 26px; bottom: 0; width: 2px; background: rgba(229, 231, 235, 0.6); }
|
.steps-flow:before { content: ''; position: absolute; left: 13px; top: 26px; bottom: 0; width: 2px; background: rgba(229, 231, 235, 0.6); }
|
||||||
.flow-item { position: relative; display: grid; grid-template-columns: 28px 1fr; gap: 12px; padding: 10px 0; }
|
.flow-item { position: relative; display: grid; grid-template-columns: 28px 1fr; gap: 12px; padding: 10px 0; }
|
||||||
.flow-item .step-index { position: static; width: 28px; height: 28px; line-height: 28px; text-align: center; border-radius: 50%; background: #1677FF; color: #fff; font-size: 14px; font-weight: 600; margin-top: 2px; }
|
.flow-item .step-index { position: static; width: 28px; height: 28px; line-height: 28px; text-align: center; border-radius: 50%; background: #1677FF; color: #fff; font-size: 14px; font-weight: 600; margin-top: 2px; }
|
||||||
.step-card { border: none; border-radius: 0; padding: 0; background: transparent; }
|
.step-card { border: none; border-radius: 0; padding: 0; background: transparent; min-width: 0; }
|
||||||
.step-header { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; }
|
.step-header { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; }
|
||||||
.title { font-size: 14px; font-weight: 600; color: #303133; text-align: left; }
|
.title { font-size: 14px; font-weight: 600; color: #303133; text-align: left; }
|
||||||
.desc { font-size: 12px; color: #909399; margin-bottom: 10px; text-align: left; line-height: 1.5; }
|
.desc { font-size: 12px; color: #909399; margin-bottom: 10px; text-align: left; line-height: 1.5; }
|
||||||
@@ -364,9 +373,11 @@ defineExpose({
|
|||||||
.dz-el-icon { font-size: 18px; margin-bottom: 4px; color: #909399; }
|
.dz-el-icon { font-size: 18px; margin-bottom: 4px; color: #909399; }
|
||||||
.dz-text { color: #303133; font-size: 13px; }
|
.dz-text { color: #303133; font-size: 13px; }
|
||||||
.dz-sub { color: #909399; font-size: 12px; }
|
.dz-sub { color: #909399; font-size: 12px; }
|
||||||
.file-chip { display: flex; align-items: center; gap: 6px; padding: 6px 8px; background: #f5f7fa; border-radius: 4px; font-size: 12px; color: #606266; margin-top: 6px; }
|
.file-chip { display: flex; align-items: center; gap: 6px; padding: 6px 8px; background: #f5f7fa; border-radius: 4px; font-size: 12px; color: #606266; margin-top: 6px; width: 100%; box-sizing: border-box; }
|
||||||
.file-chip .dot { width: 6px; height: 6px; background: #409EFF; border-radius: 50%; display: inline-block; }
|
.file-chip .dot { width: 6px; height: 6px; background: #409EFF; border-radius: 50%; flex-shrink: 0; }
|
||||||
.file-chip .name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
.file-chip .name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||||
|
.file-chip .delete-btn { cursor: pointer; opacity: 0.6; flex-shrink: 0; }
|
||||||
|
.file-chip .delete-btn:hover { opacity: 1; }
|
||||||
.action-buttons.column { display: flex; flex-direction: column; gap: 8px; }
|
.action-buttons.column { display: flex; flex-direction: column; gap: 8px; }
|
||||||
.btn-blue { background: #1677FF; border-color: #1677FF; color: #fff; }
|
.btn-blue { background: #1677FF; border-color: #1677FF; color: #fff; }
|
||||||
.btn-blue:disabled { background: #a6c8ff; border-color: #a6c8ff; color: #fff; }
|
.btn-blue:disabled { background: #a6c8ff; border-color: #a6c8ff; color: #fff; }
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, inject, defineAsyncComponent } from 'vue'
|
import { ref, inject, defineAsyncComponent, onMounted, computed } from 'vue'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
import { handlePlatformFileExport } from '../../utils/settings'
|
import { handlePlatformFileExport } from '../../utils/settings'
|
||||||
import { getUsernameFromToken } from '../../utils/token'
|
import { getUsernameFromToken } from '../../utils/token'
|
||||||
@@ -16,16 +16,20 @@ const emit = defineEmits<{
|
|||||||
updateData: [data: any[]]
|
updateData: [data: any[]]
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const trademarkData = ref<any[]>([])
|
const trademarkData = ref<any[]>([]) // 用于显示的数据(可能是简化版)
|
||||||
|
const trademarkFullData = ref<Record<string, any>[]>([]) // 完整的行数据(用于导出)
|
||||||
|
const trademarkHeaders = ref<string[]>([]) // 表头
|
||||||
const trademarkFileName = ref('')
|
const trademarkFileName = ref('')
|
||||||
const trademarkFile = ref<File | null>(null)
|
const trademarkFile = ref<File | null>(null)
|
||||||
const trademarkUpload = ref<HTMLInputElement | null>(null)
|
const trademarkUpload = ref<HTMLInputElement | null>(null)
|
||||||
const trademarkLoading = ref(false)
|
const trademarkLoading = ref(false)
|
||||||
|
const uploadLoading = ref(false)
|
||||||
const trademarkProgress = ref(0)
|
const trademarkProgress = ref(0)
|
||||||
const exportLoading = ref(false)
|
const exportLoading = ref(false)
|
||||||
const currentStep = ref(0)
|
const currentStep = ref(0)
|
||||||
const totalSteps = ref(0)
|
const totalSteps = ref(0)
|
||||||
let brandProgressTimer: any = null
|
let brandProgressTimer: any = null
|
||||||
|
const brandTaskId = ref('')
|
||||||
|
|
||||||
// 三个任务的进度数据
|
// 三个任务的进度数据
|
||||||
const taskProgress = ref({
|
const taskProgress = ref({
|
||||||
@@ -82,8 +86,18 @@ const regionOptions = [
|
|||||||
{ label: '美国', value: '美国', flag: '🇺🇸' }
|
{ label: '美国', value: '美国', flag: '🇺🇸' }
|
||||||
]
|
]
|
||||||
|
|
||||||
// 查询类型多选
|
// 查询类型多选(默认全部勾选)
|
||||||
const queryTypes = ref<string[]>(['product'])
|
const queryTypes = ref<string[]>(['product', 'brand'])
|
||||||
|
|
||||||
|
// 计算已完成的配置步骤数
|
||||||
|
const completedSteps = computed(() => {
|
||||||
|
let count = 0
|
||||||
|
if (trademarkFileName.value) count++
|
||||||
|
if (selectedAccount.value) count++
|
||||||
|
if (region.value) count++
|
||||||
|
if (queryTypes.value.length > 0) count++
|
||||||
|
return count
|
||||||
|
})
|
||||||
|
|
||||||
const showTrialExpiredDialog = ref(false)
|
const showTrialExpiredDialog = ref(false)
|
||||||
const trialExpiredType = ref<'device' | 'account' | 'both' | 'subscribe'>('account')
|
const trialExpiredType = ref<'device' | 'account' | 'both' | 'subscribe'>('account')
|
||||||
@@ -93,6 +107,80 @@ function showMessage(message: string, type: 'success' | 'warning' | 'error' | 'i
|
|||||||
ElMessage({ message, type })
|
ElMessage({ message, type })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function removeTrademarkFile() {
|
||||||
|
trademarkFileName.value = ''
|
||||||
|
trademarkFile.value = null
|
||||||
|
uploadLoading.value = false
|
||||||
|
if (trademarkUpload.value) {
|
||||||
|
trademarkUpload.value.value = ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存会话到后端
|
||||||
|
async function saveSession() {
|
||||||
|
if (!trademarkData.value.length) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
const sessionData = {
|
||||||
|
fileName: trademarkFileName.value,
|
||||||
|
resultData: trademarkData.value,
|
||||||
|
fullData: trademarkFullData.value,
|
||||||
|
headers: trademarkHeaders.value,
|
||||||
|
taskProgress: taskProgress.value,
|
||||||
|
queryStatus: queryStatus.value
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await markApi.saveSession(sessionData)
|
||||||
|
if (result.code === 200 || result.code === 0) {
|
||||||
|
const username = getUsernameFromToken()
|
||||||
|
localStorage.setItem(`trademark_session_${username}`, JSON.stringify({
|
||||||
|
sessionId: result.data.sessionId,
|
||||||
|
timestamp: Date.now()
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('保存会话失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从后端恢复会话
|
||||||
|
async function restoreSession() {
|
||||||
|
try {
|
||||||
|
const username = getUsernameFromToken()
|
||||||
|
const saved = localStorage.getItem(`trademark_session_${username}`)
|
||||||
|
if (!saved) return
|
||||||
|
|
||||||
|
const { sessionId, timestamp } = JSON.parse(saved)
|
||||||
|
|
||||||
|
// 检查是否在7天内
|
||||||
|
if (Date.now() - timestamp > 7 * 24 * 60 * 60 * 1000) {
|
||||||
|
localStorage.removeItem(`trademark_session_${username}`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await markApi.getSession(sessionId)
|
||||||
|
if (result.code === 200 || result.code === 0) {
|
||||||
|
trademarkFileName.value = result.data.fileName || ''
|
||||||
|
trademarkData.value = result.data.resultData || []
|
||||||
|
trademarkFullData.value = result.data.fullData || []
|
||||||
|
trademarkHeaders.value = result.data.headers || []
|
||||||
|
queryStatus.value = result.data.queryStatus || 'idle'
|
||||||
|
|
||||||
|
if (result.data.taskProgress) {
|
||||||
|
Object.assign(taskProgress.value, result.data.taskProgress)
|
||||||
|
}
|
||||||
|
|
||||||
|
emit('updateData', trademarkData.value)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('恢复会话失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
restoreSession()
|
||||||
|
})
|
||||||
|
|
||||||
function openTrademarkUpload() {
|
function openTrademarkUpload() {
|
||||||
trademarkUpload.value?.click()
|
trademarkUpload.value?.click()
|
||||||
}
|
}
|
||||||
@@ -108,13 +196,41 @@ async function handleTrademarkUpload(e: Event) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
trademarkFileName.value = file.name
|
uploadLoading.value = true
|
||||||
trademarkFile.value = file
|
|
||||||
queryStatus.value = 'idle' // 重置状态
|
try {
|
||||||
trademarkData.value = []
|
// 根据选中的查询类型确定需要的表头
|
||||||
emit('updateData', [])
|
const requiredHeaders: string[] = []
|
||||||
showMessage(`文件已准备:${file.name}`, 'success')
|
if (queryTypes.value.includes('product')) {
|
||||||
input.value = ''
|
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) {
|
||||||
|
showMessage(validateResult.msg || '表头验证失败', 'error')
|
||||||
|
input.value = ''
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
trademarkFileName.value = file.name
|
||||||
|
trademarkFile.value = file
|
||||||
|
queryStatus.value = 'idle'
|
||||||
|
trademarkData.value = []
|
||||||
|
trademarkFullData.value = []
|
||||||
|
trademarkHeaders.value = []
|
||||||
|
emit('updateData', [])
|
||||||
|
} catch (error: any) {
|
||||||
|
showMessage('表头验证失败: ' + error.message, 'error')
|
||||||
|
} finally {
|
||||||
|
uploadLoading.value = false
|
||||||
|
input.value = ''
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function startTrademarkQuery() {
|
async function startTrademarkQuery() {
|
||||||
@@ -137,6 +253,8 @@ async function startTrademarkQuery() {
|
|||||||
trademarkLoading.value = true
|
trademarkLoading.value = true
|
||||||
trademarkProgress.value = 0
|
trademarkProgress.value = 0
|
||||||
trademarkData.value = []
|
trademarkData.value = []
|
||||||
|
trademarkFullData.value = []
|
||||||
|
trademarkHeaders.value = []
|
||||||
queryStatus.value = 'inProgress'
|
queryStatus.value = 'inProgress'
|
||||||
|
|
||||||
// 重置任务进度
|
// 重置任务进度
|
||||||
@@ -159,15 +277,12 @@ async function startTrademarkQuery() {
|
|||||||
|
|
||||||
if (needProductCheck) {
|
if (needProductCheck) {
|
||||||
// 步骤1: 产品商标筛查 - 调用新建任务接口
|
// 步骤1: 产品商标筛查 - 调用新建任务接口
|
||||||
showMessage('正在上传文件...', 'info')
|
|
||||||
const createResult = await markApi.newTask(trademarkFile.value)
|
const createResult = await markApi.newTask(trademarkFile.value)
|
||||||
|
|
||||||
if (createResult.code !== 200 && createResult.code !== 0) {
|
if (createResult.code !== 200 && createResult.code !== 0) {
|
||||||
throw new Error(createResult.msg || '创建任务失败')
|
throw new Error(createResult.msg || '创建任务失败')
|
||||||
}
|
}
|
||||||
|
|
||||||
showMessage('文件上传成功,正在处理...', 'success')
|
|
||||||
|
|
||||||
const taskData = taskProgress.value.product
|
const taskData = taskProgress.value.product
|
||||||
taskData.total = 100 // 设置临时总数以显示进度动画
|
taskData.total = 100 // 设置临时总数以显示进度动画
|
||||||
taskData.current = 5 // 立即显示初始进度
|
taskData.current = 5 // 立即显示初始进度
|
||||||
@@ -235,35 +350,56 @@ async function startTrademarkQuery() {
|
|||||||
clearInterval(progressTimer)
|
clearInterval(progressTimer)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 映射后端数据到前端格式
|
// 第三方API返回的筛查结果(TM和未注册的商品)
|
||||||
trademarkData.value = productResult.data.filtered.map((item: any) => ({
|
const thirdPartyFiltered = productResult.data.filtered
|
||||||
name: item['品牌'] || '',
|
|
||||||
status: item['商标类型'] || '',
|
// 提取ASIN列表
|
||||||
|
const filteredAsins = thirdPartyFiltered
|
||||||
|
.map((item: any) => item['ASIN'])
|
||||||
|
.filter((asin: string) => asin && asin.trim())
|
||||||
|
|
||||||
|
if (filteredAsins.length === 0) {
|
||||||
|
showMessage('第三方筛查结果为空', 'warning')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从原始Excel中过滤出这些ASIN的完整行(保持原始表头和列)
|
||||||
|
const originalFilterResult = await markApi.filterByAsins(trademarkFile.value, filteredAsins)
|
||||||
|
|
||||||
|
if (originalFilterResult.code !== 200 && originalFilterResult.code !== 0) {
|
||||||
|
throw new Error(originalFilterResult.msg || '从原始文件过滤失败')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存完整数据(用于导出,使用原始表头)
|
||||||
|
trademarkFullData.value = originalFilterResult.data.filteredRows
|
||||||
|
trademarkHeaders.value = originalFilterResult.data.headers || []
|
||||||
|
|
||||||
|
// 映射后端数据到前端格式(用于显示)
|
||||||
|
trademarkData.value = originalFilterResult.data.filteredRows.map((row: any) => ({
|
||||||
|
name: row['品牌'] || '',
|
||||||
|
status: row['商标类型'] || '',
|
||||||
class: '',
|
class: '',
|
||||||
owner: '',
|
owner: '',
|
||||||
expireDate: item['注册时间'] || '',
|
expireDate: row['注册时间'] || '',
|
||||||
similarity: 0,
|
similarity: 0,
|
||||||
asin: item['ASIN'],
|
asin: row['ASIN'],
|
||||||
productImage: item['商品主图']
|
productImage: row['商品主图']
|
||||||
}))
|
}))
|
||||||
|
|
||||||
// 如果需要品牌筛查,从产品结果中提取品牌列表
|
// 如果需要品牌筛查,从产品结果中提取品牌列表(不去重,保持和产品数量一致)
|
||||||
if (needBrandCheck) {
|
if (needBrandCheck) {
|
||||||
brandList = productResult.data.filtered
|
brandList = thirdPartyFiltered
|
||||||
.map((item: any) => item['品牌'])
|
.map((item: any) => item['品牌'])
|
||||||
.filter((brand: string) => brand && brand.trim())
|
.filter((brand: string) => brand && brand.trim())
|
||||||
}
|
}
|
||||||
|
|
||||||
showMessage(`产品筛查完成,共 ${taskData.total} 条,筛查出 ${taskData.completed} 条`, 'success')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 品牌商标筛查
|
// 品牌商标筛查
|
||||||
if (needBrandCheck) {
|
if (needBrandCheck) {
|
||||||
if (!trademarkLoading.value) return
|
if (!trademarkLoading.value) return
|
||||||
|
|
||||||
// 如果没有执行产品筛查,需要先从Excel提取品牌列表
|
// 如果没有执行产品筛查,需要先从Excel提取品牌列表和完整数据
|
||||||
if (!needProductCheck) {
|
if (!needProductCheck) {
|
||||||
showMessage('正在从Excel提取品牌列表...', 'info')
|
|
||||||
const extractResult = await markApi.extractBrands(trademarkFile.value)
|
const extractResult = await markApi.extractBrands(trademarkFile.value)
|
||||||
|
|
||||||
if (extractResult.code !== 200 && extractResult.code !== 0) {
|
if (extractResult.code !== 200 && extractResult.code !== 0) {
|
||||||
@@ -275,7 +411,8 @@ async function startTrademarkQuery() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
brandList = extractResult.data.brands
|
brandList = extractResult.data.brands
|
||||||
showMessage(`品牌列表提取成功,共 ${brandList.length} 个品牌`, 'success')
|
// 保存表头和完整数据(如果没有产品筛查)
|
||||||
|
trademarkHeaders.value = extractResult.data.headers || []
|
||||||
}
|
}
|
||||||
|
|
||||||
if (brandList.length === 0) {
|
if (brandList.length === 0) {
|
||||||
@@ -283,42 +420,69 @@ async function startTrademarkQuery() {
|
|||||||
} else {
|
} else {
|
||||||
const brandData = taskProgress.value.brand
|
const brandData = taskProgress.value.brand
|
||||||
brandData.total = brandList.length
|
brandData.total = brandList.length
|
||||||
brandData.current = 0
|
brandData.current = 1 // 立即显示初始进度
|
||||||
brandData.completed = 0
|
brandData.completed = 0
|
||||||
|
|
||||||
showMessage(`开始品牌商标筛查,共 ${brandList.length} 个品牌...`, 'info')
|
// 生成任务ID并轮询真实进度
|
||||||
|
brandTaskId.value = `task_${Date.now()}`
|
||||||
// 模拟进度动画
|
brandProgressTimer = setInterval(async () => {
|
||||||
brandProgressTimer = setInterval(() => {
|
try {
|
||||||
if (brandData.current < brandList.length * 0.95) {
|
const res = await markApi.getBrandCheckProgress(brandTaskId.value)
|
||||||
brandData.current = Math.min(brandData.current + 20, brandList.length * 0.95)
|
if (res.code === 0 || res.code === 200) {
|
||||||
|
brandData.current = res.data.current || 0
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// 忽略进度查询错误
|
||||||
}
|
}
|
||||||
}, 1000)
|
}, 1000)
|
||||||
|
|
||||||
const brandResult = await markApi.brandCheck(brandList)
|
const brandResult = await markApi.brandCheck(brandList, brandTaskId.value)
|
||||||
if (brandProgressTimer) clearInterval(brandProgressTimer)
|
if (brandProgressTimer) clearInterval(brandProgressTimer)
|
||||||
|
|
||||||
if (!trademarkLoading.value) return
|
if (!trademarkLoading.value) return
|
||||||
|
|
||||||
if (brandResult.code === 200 || brandResult.code === 0) {
|
if (brandResult.code === 200 || brandResult.code === 0) {
|
||||||
// 完成,显示100%
|
// 完成,显示100%
|
||||||
|
brandData.total = brandResult.data.checked || brandResult.data.total || brandData.total
|
||||||
brandData.current = brandData.total
|
brandData.current = brandData.total
|
||||||
brandData.completed = brandResult.data.filtered
|
brandData.completed = brandResult.data.unregistered || 0
|
||||||
|
|
||||||
// 将品牌筛查结果追加到展示数据中
|
// 提取未注册品牌列表
|
||||||
const brandItems = brandResult.data.data.map((item: any) => ({
|
const unregisteredBrands = brandResult.data.data.map((item: any) => item.brand).filter(Boolean)
|
||||||
name: item.brand || '',
|
|
||||||
status: item.status || '未注册',
|
|
||||||
class: '',
|
|
||||||
owner: '',
|
|
||||||
expireDate: '',
|
|
||||||
similarity: 0,
|
|
||||||
isBrand: true // 标记为品牌数据
|
|
||||||
}))
|
|
||||||
|
|
||||||
trademarkData.value = [...trademarkData.value, ...brandItems]
|
if (unregisteredBrands.length > 0) {
|
||||||
|
// 从原始Excel中过滤出包含这些品牌的完整行
|
||||||
|
const filterResult = await markApi.filterByBrands(trademarkFile.value, unregisteredBrands)
|
||||||
|
|
||||||
showMessage(`品牌筛查完成,共查询 ${brandData.total} 个品牌,筛查出 ${brandData.completed} 个未注册品牌`, 'success')
|
if (filterResult.code === 200 || filterResult.code === 0) {
|
||||||
|
// 保存完整数据(用于导出,使用原始表头)
|
||||||
|
trademarkFullData.value = filterResult.data.filteredRows
|
||||||
|
trademarkHeaders.value = filterResult.data.headers || []
|
||||||
|
|
||||||
|
// 更新统计:显示过滤出的实际行数(而不是品牌数)
|
||||||
|
brandData.completed = filterResult.data.filteredRows.length
|
||||||
|
|
||||||
|
// 将品牌筛查结果作为展示数据
|
||||||
|
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]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
throw new Error(brandResult.msg || '品牌筛查失败')
|
throw new Error(brandResult.msg || '品牌筛查失败')
|
||||||
}
|
}
|
||||||
@@ -333,6 +497,9 @@ async function startTrademarkQuery() {
|
|||||||
if (needProductCheck) summaryMsg += `,产品:${taskProgress.value.product.completed}/${taskProgress.value.product.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}`
|
if (needBrandCheck && brandList.length > 0) summaryMsg += `,品牌:${taskProgress.value.brand.completed}/${taskProgress.value.brand.total}`
|
||||||
showMessage(summaryMsg, 'success')
|
showMessage(summaryMsg, 'success')
|
||||||
|
|
||||||
|
// 保存会话
|
||||||
|
await saveSession()
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
const hasProductData = taskProgress.value.product.total > 0
|
const hasProductData = taskProgress.value.product.total > 0
|
||||||
@@ -356,6 +523,8 @@ async function startTrademarkQuery() {
|
|||||||
// 仅在第1步失败时清空数据
|
// 仅在第1步失败时清空数据
|
||||||
if (!hasProductData) {
|
if (!hasProductData) {
|
||||||
trademarkData.value = []
|
trademarkData.value = []
|
||||||
|
trademarkFullData.value = []
|
||||||
|
trademarkHeaders.value = []
|
||||||
emit('updateData', [])
|
emit('updateData', [])
|
||||||
} else {
|
} else {
|
||||||
emit('updateData', trademarkData.value)
|
emit('updateData', trademarkData.value)
|
||||||
@@ -374,51 +543,64 @@ async function startTrademarkQuery() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function stopTrademarkQuery() {
|
function stopTrademarkQuery() {
|
||||||
// 清除进度动画定时器
|
// 通知后端取消任务
|
||||||
|
if (brandTaskId.value) {
|
||||||
|
markApi.cancelBrandCheck(brandTaskId.value).catch(() => {
|
||||||
|
// 忽略取消失败
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清除进度定时器
|
||||||
if (brandProgressTimer) {
|
if (brandProgressTimer) {
|
||||||
clearInterval(brandProgressTimer)
|
clearInterval(brandProgressTimer)
|
||||||
brandProgressTimer = null
|
brandProgressTimer = null
|
||||||
}
|
}
|
||||||
|
|
||||||
trademarkLoading.value = false
|
trademarkLoading.value = false
|
||||||
queryStatus.value = 'cancel'
|
queryStatus.value = 'cancel'
|
||||||
currentStep.value = 0
|
currentStep.value = 0
|
||||||
totalSteps.value = 0
|
totalSteps.value = 0
|
||||||
showMessage('已停止筛查', 'info')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function exportTrademarkData() {
|
async function exportTrademarkData() {
|
||||||
if (!trademarkData.value.length) {
|
if (!trademarkFullData.value.length || !trademarkHeaders.value.length) {
|
||||||
showMessage('没有数据可供导出', 'warning')
|
showMessage('没有数据可供导出', 'warning')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
exportLoading.value = true
|
exportLoading.value = true
|
||||||
|
|
||||||
let html = `<table>
|
try {
|
||||||
<tr><th>商标名称</th><th>状态</th><th>类别</th><th>权利人</th><th>到期日期</th><th>相似度</th></tr>`
|
let html = '<table border="1" style="border-collapse:collapse;"><tr>'
|
||||||
|
trademarkHeaders.value.forEach(header => {
|
||||||
|
html += `<th>${header || ''}</th>`
|
||||||
|
})
|
||||||
|
html += '</tr>'
|
||||||
|
|
||||||
trademarkData.value.forEach(item => {
|
trademarkFullData.value.forEach(row => {
|
||||||
html += `<tr>
|
html += '<tr>'
|
||||||
<td>${item.name || ''}</td>
|
trademarkHeaders.value.forEach(header => {
|
||||||
<td>${item.status || ''}</td>
|
const cellValue = row[header]
|
||||||
<td>${item.class || ''}</td>
|
html += `<td>${cellValue !== null && cellValue !== undefined ? cellValue : ''}</td>`
|
||||||
<td>${item.owner || ''}</td>
|
})
|
||||||
<td>${item.expireDate || ''}</td>
|
html += '</tr>'
|
||||||
<td>${item.similarity ? item.similarity + '%' : '-'}</td>
|
})
|
||||||
</tr>`
|
html += '</table>'
|
||||||
})
|
|
||||||
html += '</table>'
|
|
||||||
|
|
||||||
const blob = new Blob([html], { type: 'application/vnd.ms-excel' })
|
const blob = new Blob([html], { type: 'application/vnd.ms-excel' })
|
||||||
const fileName = `商标筛查结果_${new Date().toISOString().slice(0, 10)}.xls`
|
const fileName = `商标筛查结果_${new Date().toISOString().slice(0, 10)}.xls`
|
||||||
|
|
||||||
const username = getUsernameFromToken()
|
const username = getUsernameFromToken()
|
||||||
const success = await handlePlatformFileExport('amazon', blob, fileName, username)
|
const success = await handlePlatformFileExport('amazon', blob, fileName, username)
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
showMessage('Excel文件导出成功!', 'success')
|
showMessage('Excel文件导出成功!', 'success')
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
showMessage('导出失败: ' + error.message, 'error')
|
||||||
|
} finally {
|
||||||
|
exportLoading.value = false
|
||||||
}
|
}
|
||||||
exportLoading.value = false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleQueryType(type: string) {
|
function toggleQueryType(type: string) {
|
||||||
@@ -431,7 +613,7 @@ function toggleQueryType(type: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function viewTrademarkExample() {
|
function viewTrademarkExample() {
|
||||||
showMessage('商标列表应包含一列商标名称', 'info')
|
// 查看示例
|
||||||
}
|
}
|
||||||
|
|
||||||
function downloadTrademarkTemplate() {
|
function downloadTrademarkTemplate() {
|
||||||
@@ -450,6 +632,8 @@ function downloadTrademarkTemplate() {
|
|||||||
function resetToIdle() {
|
function resetToIdle() {
|
||||||
queryStatus.value = 'idle'
|
queryStatus.value = 'idle'
|
||||||
trademarkData.value = []
|
trademarkData.value = []
|
||||||
|
trademarkFullData.value = []
|
||||||
|
trademarkHeaders.value = []
|
||||||
trademarkFileName.value = ''
|
trademarkFileName.value = ''
|
||||||
trademarkFile.value = null
|
trademarkFile.value = null
|
||||||
taskProgress.value.product.total = 0
|
taskProgress.value.product.total = 0
|
||||||
@@ -461,6 +645,14 @@ function resetToIdle() {
|
|||||||
taskProgress.value.platform.total = 0
|
taskProgress.value.platform.total = 0
|
||||||
taskProgress.value.platform.current = 0
|
taskProgress.value.platform.current = 0
|
||||||
taskProgress.value.platform.completed = 0
|
taskProgress.value.platform.completed = 0
|
||||||
|
|
||||||
|
// 清空localStorage中的会话数据
|
||||||
|
try {
|
||||||
|
const username = getUsernameFromToken()
|
||||||
|
localStorage.removeItem(`trademark_session_${username}`)
|
||||||
|
} catch (e) {
|
||||||
|
// 忽略错误
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
@@ -473,7 +665,8 @@ defineExpose({
|
|||||||
errorMessage,
|
errorMessage,
|
||||||
resetToIdle,
|
resetToIdle,
|
||||||
stopTrademarkQuery,
|
stopTrademarkQuery,
|
||||||
startTrademarkQuery
|
startTrademarkQuery,
|
||||||
|
exportTrademarkData
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -486,16 +679,18 @@ defineExpose({
|
|||||||
<div class="step-card">
|
<div class="step-card">
|
||||||
<div class="step-header"><div class="title">导入Excel表格</div></div>
|
<div class="step-header"><div class="title">导入Excel表格</div></div>
|
||||||
<div class="desc">产品筛查:需导入卖家精灵选品表格,并勾选"导出主图";品牌筛查:Excel需包含"品牌"列</div>
|
<div class="desc">产品筛查:需导入卖家精灵选品表格,并勾选"导出主图";品牌筛查:Excel需包含"品牌"列</div>
|
||||||
<div class="dropzone" @click="openTrademarkUpload">
|
<div class="dropzone" :class="{ uploading: uploadLoading }" @click="!uploadLoading && openTrademarkUpload()">
|
||||||
<div class="dz-icon">📤</div>
|
<div v-if="!uploadLoading" class="dz-icon">📤</div>
|
||||||
<div class="dz-text">点击或将文件拖拽到这里上传</div>
|
<div v-else class="dz-icon spinner">⟳</div>
|
||||||
<div class="dz-sub">支持 .xls .xlsx</div>
|
<div class="dz-text">{{ uploadLoading ? '正在验证表头...' : '点击或将文件拖拽到这里上传' }}</div>
|
||||||
|
<div v-if="!uploadLoading" class="dz-sub">支持 .xls .xlsx</div>
|
||||||
</div>
|
</div>
|
||||||
<input ref="trademarkUpload" style="display:none" type="file" accept=".xls,.xlsx" @change="handleTrademarkUpload" :disabled="trademarkLoading" />
|
<input ref="trademarkUpload" style="display:none" type="file" accept=".xls,.xlsx" @change="handleTrademarkUpload" :disabled="trademarkLoading || uploadLoading" />
|
||||||
|
|
||||||
<div v-if="trademarkFileName" class="file-chip">
|
<div v-if="trademarkFileName" class="file-chip">
|
||||||
<span class="dot"></span>
|
<span class="dot"></span>
|
||||||
<span class="name">{{ trademarkFileName }}</span>
|
<span class="name">{{ trademarkFileName }}</span>
|
||||||
|
<span class="delete-btn" @click="removeTrademarkFile" title="删除文件">🗑️</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -590,7 +785,7 @@ defineExpose({
|
|||||||
<!-- 底部开始查询按钮 -->
|
<!-- 底部开始查询按钮 -->
|
||||||
<div class="bottom-action">
|
<div class="bottom-action">
|
||||||
<div class="action-header">
|
<div class="action-header">
|
||||||
<span class="step-indicator">{{ trademarkLoading ? currentStep : 1 }}/4</span>
|
<span class="step-indicator">{{ completedSteps }}/4</span>
|
||||||
<el-button v-if="!trademarkLoading" class="start-btn" type="primary" :disabled="!trademarkFileName || queryTypes.length === 0" @click="startTrademarkQuery">
|
<el-button v-if="!trademarkLoading" class="start-btn" type="primary" :disabled="!trademarkFileName || queryTypes.length === 0" @click="startTrademarkQuery">
|
||||||
开始筛查
|
开始筛查
|
||||||
</el-button>
|
</el-button>
|
||||||
@@ -625,39 +820,18 @@ defineExpose({
|
|||||||
.steps-flow:before { content: ''; position: absolute; left: 13px; top: 26px; bottom: 0; width: 2px; background: rgba(229, 231, 235, 0.6); }
|
.steps-flow:before { content: ''; position: absolute; left: 13px; top: 26px; bottom: 0; width: 2px; background: rgba(229, 231, 235, 0.6); }
|
||||||
.flow-item { position: relative; display: grid; grid-template-columns: 28px 1fr; gap: 12px; padding: 10px 0; }
|
.flow-item { position: relative; display: grid; grid-template-columns: 28px 1fr; gap: 12px; padding: 10px 0; }
|
||||||
.flow-item .step-index { position: static; width: 28px; height: 28px; line-height: 28px; text-align: center; border-radius: 50%; background: #1677FF; color: #fff; font-size: 14px; font-weight: 600; margin-top: 2px; }
|
.flow-item .step-index { position: static; width: 28px; height: 28px; line-height: 28px; text-align: center; border-radius: 50%; background: #1677FF; color: #fff; font-size: 14px; font-weight: 600; margin-top: 2px; }
|
||||||
.step-card { border: none; border-radius: 0; padding: 0; background: transparent; }
|
.step-card { border: none; border-radius: 0; padding: 0; background: transparent; min-width: 0; }
|
||||||
.step-header { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; }
|
.step-header { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; }
|
||||||
.title { font-size: 14px; font-weight: 600; color: #303133; text-align: left; }
|
.title { font-size: 14px; font-weight: 600; color: #303133; text-align: left; }
|
||||||
.desc { font-size: 12px; color: #909399; margin-bottom: 10px; text-align: left; line-height: 1.5; }
|
.desc { font-size: 12px; color: #909399; margin-bottom: 10px; text-align: left; line-height: 1.5; }
|
||||||
.links { display: flex; align-items: center; gap: 6px; margin-bottom: 8px; }
|
.links { display: flex; align-items: center; gap: 6px; margin-bottom: 8px; }
|
||||||
.link { color: #909399; cursor: pointer; font-size: 12px; }
|
.link { color: #909399; cursor: pointer; font-size: 12px; }
|
||||||
|
|
||||||
.file-chip {
|
.file-chip { display: flex; align-items: center; gap: 6px; padding: 6px 8px; background: #f5f7fa; border-radius: 4px; font-size: 12px; color: #606266; margin-top: 6px; width: 100%; box-sizing: border-box; }
|
||||||
display: flex;
|
.file-chip .dot { width: 6px; height: 6px; background: #409EFF; border-radius: 50%; flex-shrink: 0; }
|
||||||
align-items: center;
|
.file-chip .name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||||
gap: 6px;
|
.file-chip .delete-btn { cursor: pointer; opacity: 0.6; flex-shrink: 0; }
|
||||||
padding: 6px 8px;
|
.file-chip .delete-btn:hover { opacity: 1; }
|
||||||
background: #f5f7fa;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-size: 12px;
|
|
||||||
color: #606266;
|
|
||||||
margin-top: 6px;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
.file-chip .dot {
|
|
||||||
width: 6px;
|
|
||||||
height: 6px;
|
|
||||||
background: #409EFF;
|
|
||||||
border-radius: 50%;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
.file-chip .name {
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
flex: 1;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropzone {
|
.dropzone {
|
||||||
border: 1px dashed #c0c4cc;
|
border: 1px dashed #c0c4cc;
|
||||||
@@ -669,9 +843,13 @@ defineExpose({
|
|||||||
transition: all 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
}
|
}
|
||||||
.dropzone:hover { background: #f6fbff; border-color: #409EFF; }
|
.dropzone:hover { background: #f6fbff; border-color: #409EFF; }
|
||||||
|
.dropzone.uploading { cursor: not-allowed; opacity: 0.7; }
|
||||||
|
.dropzone.uploading:hover { background: #fafafa; border-color: #c0c4cc; }
|
||||||
.dz-icon { font-size: 20px; margin-bottom: 6px; color: #909399; }
|
.dz-icon { font-size: 20px; margin-bottom: 6px; color: #909399; }
|
||||||
.dz-text { color: #303133; font-size: 13px; margin-bottom: 2px; }
|
.dz-text { color: #303133; font-size: 13px; margin-bottom: 2px; }
|
||||||
.dz-sub { color: #909399; font-size: 12px; }
|
.dz-sub { color: #909399; font-size: 12px; }
|
||||||
|
.spinner { animation: spin 1s linear infinite; }
|
||||||
|
@keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
|
||||||
|
|
||||||
.query-options {
|
.query-options {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -363,6 +363,14 @@ function showMessage(message: string, type: 'info' | 'success' | 'warning' | 'er
|
|||||||
ElMessage({ message, type })
|
ElMessage({ message, type })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function removeSelectedFile() {
|
||||||
|
selectedFileName.value = ''
|
||||||
|
pendingFile.value = null
|
||||||
|
if (uploadInputRef.value) {
|
||||||
|
uploadInputRef.value.value = ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function exportToExcel() {
|
async function exportToExcel() {
|
||||||
if (!allProducts.value.length) {
|
if (!allProducts.value.length) {
|
||||||
showMessage('没有数据可供导出', 'warning')
|
showMessage('没有数据可供导出', 'warning')
|
||||||
@@ -481,6 +489,7 @@ onMounted(loadLatest)
|
|||||||
<div v-if="selectedFileName" class="file-chip">
|
<div v-if="selectedFileName" class="file-chip">
|
||||||
<span class="dot"></span>
|
<span class="dot"></span>
|
||||||
<span class="name">{{ selectedFileName }}</span>
|
<span class="name">{{ selectedFileName }}</span>
|
||||||
|
<span class="delete-btn" @click="removeSelectedFile" title="删除文件">🗑️</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -665,7 +674,7 @@ onMounted(loadLatest)
|
|||||||
.flow-item { position: relative; display: grid; grid-template-columns: 22px 1fr; gap: 10px; padding: 8px 0; }
|
.flow-item { position: relative; display: grid; grid-template-columns: 22px 1fr; gap: 10px; padding: 8px 0; }
|
||||||
.flow-item .step-index { position: static; width: 22px; height: 22px; line-height: 22px; text-align: center; border-radius: 50%; background: #1677FF; color: #fff; font-size: 12px; font-weight: 600; margin-top: 2px; }
|
.flow-item .step-index { position: static; width: 22px; height: 22px; line-height: 22px; text-align: center; border-radius: 50%; background: #1677FF; color: #fff; font-size: 12px; font-weight: 600; margin-top: 2px; }
|
||||||
.flow-item:after { display: none; }
|
.flow-item:after { display: none; }
|
||||||
.step-card { border: none; border-radius: 0; padding: 0; background: transparent; }
|
.step-card { border: none; border-radius: 0; padding: 0; background: transparent; min-width: 0; }
|
||||||
.step-header { display: flex; align-items: center; gap: 8px; margin-bottom: 6px; }
|
.step-header { display: flex; align-items: center; gap: 8px; margin-bottom: 6px; }
|
||||||
.title { font-size: 13px; font-weight: 600; color: #303133; text-align: left; }
|
.title { font-size: 13px; font-weight: 600; color: #303133; text-align: left; }
|
||||||
.desc { font-size: 12px; color: #909399; margin-bottom: 8px; text-align: left; }
|
.desc { font-size: 12px; color: #909399; margin-bottom: 8px; text-align: left; }
|
||||||
@@ -686,9 +695,11 @@ onMounted(loadLatest)
|
|||||||
.single-input.left { display: flex; gap: 8px; }
|
.single-input.left { display: flex; gap: 8px; }
|
||||||
.action-buttons.column { display: flex; flex-direction: column; gap: 8px; }
|
.action-buttons.column { display: flex; flex-direction: column; gap: 8px; }
|
||||||
|
|
||||||
.file-chip { display: flex; align-items: center; gap: 6px; padding: 6px 8px; background: #f5f7fa; border-radius: 4px; font-size: 12px; color: #606266; margin-top: 6px; }
|
.file-chip { display: flex; align-items: center; gap: 6px; padding: 6px 8px; background: #f5f7fa; border-radius: 4px; font-size: 12px; color: #606266; margin-top: 6px; width: 100%; box-sizing: border-box; }
|
||||||
.file-chip .dot { width: 6px; height: 6px; background: #409EFF; border-radius: 50%; display: inline-block; }
|
.file-chip .dot { width: 6px; height: 6px; background: #409EFF; border-radius: 50%; flex-shrink: 0; }
|
||||||
.file-chip .name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
.file-chip .name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||||
|
.file-chip .delete-btn { cursor: pointer; opacity: 0.6; flex-shrink: 0; }
|
||||||
|
.file-chip .delete-btn:hover { opacity: 1; }
|
||||||
|
|
||||||
.progress-section.left { margin-top: 10px; }
|
.progress-section.left { margin-top: 10px; }
|
||||||
.full { width: 100%; }
|
.full { width: 100%; }
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
</parent>
|
</parent>
|
||||||
<groupId>com.tashow.erp</groupId>
|
<groupId>com.tashow.erp</groupId>
|
||||||
<artifactId>erp_client_sb</artifactId>
|
<artifactId>erp_client_sb</artifactId>
|
||||||
<version>2.5.6</version>
|
<version>2.6.0</version>
|
||||||
<name>erp_client_sb</name>
|
<name>erp_client_sb</name>
|
||||||
<description>erp客户端</description>
|
<description>erp客户端</description>
|
||||||
<properties>
|
<properties>
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ public class ChromeDriverPreloader implements ApplicationRunner {
|
|||||||
@Bean
|
@Bean
|
||||||
public ChromeDriver chromeDriver() {
|
public ChromeDriver chromeDriver() {
|
||||||
// 为兼容性保留 Bean,但不自动创建
|
// 为兼容性保留 Bean,但不自动创建
|
||||||
if (globalDriver == null) globalDriver = SeleniumUtil.createDriver(false);
|
if (globalDriver == null) globalDriver = SeleniumUtil.createDriver(true);
|
||||||
return globalDriver;
|
return globalDriver;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
package com.tashow.erp.controller;
|
package com.tashow.erp.controller;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.tashow.erp.entity.TrademarkSessionEntity;
|
||||||
|
import com.tashow.erp.repository.TrademarkSessionRepository;
|
||||||
|
import com.tashow.erp.service.BrandTrademarkCacheService;
|
||||||
import com.tashow.erp.utils.ExcelParseUtil;
|
import com.tashow.erp.utils.ExcelParseUtil;
|
||||||
import com.tashow.erp.utils.JsonData;
|
import com.tashow.erp.utils.JsonData;
|
||||||
import com.tashow.erp.utils.LoggerUtil;
|
import com.tashow.erp.utils.LoggerUtil;
|
||||||
@@ -7,6 +11,7 @@ import org.slf4j.Logger;
|
|||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
/**
|
/**
|
||||||
@@ -17,52 +22,99 @@ import java.util.stream.Collectors;
|
|||||||
@CrossOrigin
|
@CrossOrigin
|
||||||
public class TrademarkController {
|
public class TrademarkController {
|
||||||
private static final Logger logger = LoggerUtil.getLogger(TrademarkController.class);
|
private static final Logger logger = LoggerUtil.getLogger(TrademarkController.class);
|
||||||
|
private static final ObjectMapper objectMapper = new ObjectMapper();
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private TrademarkCheckUtil util;
|
private TrademarkCheckUtil util;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private BrandTrademarkCacheService cacheService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private TrademarkSessionRepository sessionRepository;
|
||||||
|
|
||||||
|
// 进度追踪
|
||||||
|
private final Map<String, Integer> progressMap = new java.util.concurrent.ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
// 任务取消标志
|
||||||
|
private final Map<String, Boolean> cancelMap = new java.util.concurrent.ConcurrentHashMap<>();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 批量品牌商标筛查(浏览器内并发,极速版)
|
* 批量品牌商标筛查
|
||||||
*/
|
*/
|
||||||
@PostMapping("/brandCheck")
|
@PostMapping("/brandCheck")
|
||||||
public JsonData brandCheck(@RequestBody List<String> brands) {
|
public JsonData brandCheck(@RequestBody Map<String, Object> request) {
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
List<String> brands = (List<String>) request.get("brands");
|
||||||
|
String taskId = (String) request.get("taskId");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
List<String> list = brands.stream()
|
List<String> list = brands.stream()
|
||||||
.filter(b -> b != null && !b.trim().isEmpty())
|
.filter(b -> b != null && !b.trim().isEmpty())
|
||||||
.map(String::trim)
|
.map(String::trim)
|
||||||
.distinct()
|
.distinct()
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
long start = System.currentTimeMillis();
|
long start = System.currentTimeMillis();
|
||||||
// 串行查询(不加延迟)
|
|
||||||
|
// 1. 先从全局缓存获取
|
||||||
|
Map<String, Boolean> cached = cacheService.getCached(list);
|
||||||
|
|
||||||
|
// 2. 找出缓存未命中的品牌
|
||||||
|
List<String> toQuery = list.stream()
|
||||||
|
.filter(b -> !cached.containsKey(b))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
logger.info("全局缓存命中: {}/{},需查询: {}", cached.size(), list.size(), toQuery.size());
|
||||||
|
|
||||||
|
// 3. 查询未命中的品牌
|
||||||
|
Map<String, Boolean> queried = new HashMap<>();
|
||||||
|
if (!toQuery.isEmpty()) {
|
||||||
|
for (int i = 0; i < toQuery.size(); i++) {
|
||||||
|
// 检查任务是否被取消
|
||||||
|
if (taskId != null && cancelMap.getOrDefault(taskId, false)) {
|
||||||
|
logger.info("任务 {} 已被取消,停止查询", taskId);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
String brand = toQuery.get(i);
|
||||||
|
logger.info("处理第 {} 个: {}", i + 1, brand);
|
||||||
|
|
||||||
|
Map<String, Boolean> results = util.batchCheck(Collections.singletonList(brand), queried);
|
||||||
|
queried.putAll(results);
|
||||||
|
|
||||||
|
// 更新进度
|
||||||
|
if (taskId != null) {
|
||||||
|
progressMap.put(taskId, cached.size() + queried.size());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查询结束,保存所有品牌
|
||||||
|
if (!queried.isEmpty())
|
||||||
|
cacheService.saveResults(queried);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. 合并缓存和新查询结果
|
||||||
|
Map<String, Boolean> allResults = new HashMap<>(cached);
|
||||||
|
allResults.putAll(queried);
|
||||||
|
|
||||||
|
// 6. 统计结果
|
||||||
List<Map<String, Object>> unregistered = new ArrayList<>();
|
List<Map<String, Object>> unregistered = new ArrayList<>();
|
||||||
int checkedCount = 0;
|
|
||||||
int registeredCount = 0;
|
int registeredCount = 0;
|
||||||
|
|
||||||
for (int i = 0; i < list.size(); i++) {
|
for (Map.Entry<String, Boolean> entry : allResults.entrySet()) {
|
||||||
String brand = list.get(i);
|
if (!entry.getValue()) {
|
||||||
logger.info("处理第 {} 个: {}", i + 1, brand);
|
Map<String, Object> m = new HashMap<>();
|
||||||
|
m.put("brand", entry.getKey());
|
||||||
Map<String, Boolean> results = util.batchCheck(Collections.singletonList(brand));
|
m.put("status", "未注册");
|
||||||
|
unregistered.add(m);
|
||||||
results.forEach((b, isReg) -> {
|
} else {
|
||||||
if (!isReg) {
|
registeredCount++;
|
||||||
Map<String, Object> m = new HashMap<>();
|
|
||||||
m.put("brand", b);
|
|
||||||
m.put("status", "未注册");
|
|
||||||
unregistered.add(m);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 统计成功查询的数量
|
|
||||||
if (!results.isEmpty()) {
|
|
||||||
checkedCount++;
|
|
||||||
if (results.values().iterator().next()) {
|
|
||||||
registeredCount++;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
long t = (System.currentTimeMillis() - start) / 1000;
|
long t = (System.currentTimeMillis() - start) / 1000;
|
||||||
|
int checkedCount = allResults.size();
|
||||||
int failedCount = list.size() - checkedCount;
|
int failedCount = list.size() - checkedCount;
|
||||||
|
|
||||||
Map<String, Object> res = new HashMap<>();
|
Map<String, Object> res = new HashMap<>();
|
||||||
@@ -76,30 +128,281 @@ public class TrademarkController {
|
|||||||
|
|
||||||
logger.info("完成: 共{}个,成功查询{}个(已注册{}个,未注册{}个),查询失败{}个,耗时{}秒",
|
logger.info("完成: 共{}个,成功查询{}个(已注册{}个,未注册{}个),查询失败{}个,耗时{}秒",
|
||||||
list.size(), checkedCount, registeredCount, unregistered.size(), failedCount, t);
|
list.size(), checkedCount, registeredCount, unregistered.size(), failedCount, t);
|
||||||
|
|
||||||
|
// 30秒后清理进度和取消标志
|
||||||
|
if (taskId != null) {
|
||||||
|
String finalTaskId = taskId;
|
||||||
|
new Thread(() -> {
|
||||||
|
try { Thread.sleep(30000); } catch (InterruptedException ignored) {}
|
||||||
|
progressMap.remove(finalTaskId);
|
||||||
|
cancelMap.remove(finalTaskId);
|
||||||
|
}).start();
|
||||||
|
}
|
||||||
|
|
||||||
return JsonData.buildSuccess(res);
|
return JsonData.buildSuccess(res);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
logger.error("筛查失败", e);
|
logger.error("筛查失败", e);
|
||||||
return JsonData.buildError("筛查失败: " + e.getMessage());
|
return JsonData.buildError("筛查失败: " + e.getMessage());
|
||||||
} finally {
|
} finally {
|
||||||
// 采集完成或失败后关闭浏览器
|
|
||||||
util.closeDriver();
|
util.closeDriver();
|
||||||
|
cacheService.cleanExpired();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 从Excel提取品牌列表
|
* 查询品牌筛查进度
|
||||||
|
*/
|
||||||
|
@GetMapping("/brandCheckProgress")
|
||||||
|
public JsonData getBrandCheckProgress(@RequestParam("taskId") String taskId) {
|
||||||
|
Integer current = progressMap.get(taskId);
|
||||||
|
if (current == null) {
|
||||||
|
return JsonData.buildError("任务不存在或已完成");
|
||||||
|
}
|
||||||
|
Map<String, Integer> result = new HashMap<>();
|
||||||
|
result.put("current", current);
|
||||||
|
return JsonData.buildSuccess(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 取消品牌筛查任务
|
||||||
|
*/
|
||||||
|
@PostMapping("/cancelBrandCheck")
|
||||||
|
public JsonData cancelBrandCheck(@RequestBody Map<String, String> request) {
|
||||||
|
String taskId = request.get("taskId");
|
||||||
|
if (taskId != null) {
|
||||||
|
cancelMap.put(taskId, true);
|
||||||
|
logger.info("任务 {} 已标记为取消", taskId);
|
||||||
|
return JsonData.buildSuccess("任务已取消");
|
||||||
|
}
|
||||||
|
return JsonData.buildError("缺少taskId参数");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证Excel表头
|
||||||
|
*/
|
||||||
|
@PostMapping("/validateHeaders")
|
||||||
|
public JsonData validateHeaders(@RequestParam("file") MultipartFile file,
|
||||||
|
@RequestParam(value = "requiredHeaders", required = false) String requiredHeadersJson) {
|
||||||
|
try {
|
||||||
|
Map<String, Object> fullData = ExcelParseUtil.parseFullExcel(file);
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
List<String> headers = (List<String>) fullData.get("headers");
|
||||||
|
|
||||||
|
if (headers == null || headers.isEmpty()) {
|
||||||
|
return JsonData.buildError("无法读取Excel表头");
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, Object> result = new HashMap<>();
|
||||||
|
result.put("headers", headers);
|
||||||
|
|
||||||
|
// 如果提供了必需表头,进行验证
|
||||||
|
if (requiredHeadersJson != null && !requiredHeadersJson.trim().isEmpty()) {
|
||||||
|
List<String> requiredHeaders = objectMapper.readValue(requiredHeadersJson,
|
||||||
|
objectMapper.getTypeFactory().constructCollectionType(List.class, String.class));
|
||||||
|
|
||||||
|
List<String> missing = new ArrayList<>();
|
||||||
|
for (String required : requiredHeaders) {
|
||||||
|
if (!headers.contains(required)) {
|
||||||
|
missing.add(required);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result.put("valid", missing.isEmpty());
|
||||||
|
result.put("missing", missing);
|
||||||
|
|
||||||
|
if (!missing.isEmpty()) {
|
||||||
|
return JsonData.buildError("缺少必需的列: " + String.join(", ", missing));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return JsonData.buildSuccess(result);
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("验证表头失败", e);
|
||||||
|
return JsonData.buildError("验证失败: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从Excel提取品牌列表(同时返回完整Excel数据)
|
||||||
*/
|
*/
|
||||||
@PostMapping("/extractBrands")
|
@PostMapping("/extractBrands")
|
||||||
public JsonData extractBrands(@RequestParam("file") MultipartFile file) {
|
public JsonData extractBrands(@RequestParam("file") MultipartFile file) {
|
||||||
try {
|
try {
|
||||||
List<String> brands = ExcelParseUtil.parseColumnByName(file, "品牌");
|
List<String> brands = ExcelParseUtil.parseColumnByName(file, "品牌");
|
||||||
if (brands.isEmpty()) return JsonData.buildError("未找到品牌列或品牌数据为空");
|
if (brands.isEmpty()) return JsonData.buildError("未找到品牌列或品牌数据为空");
|
||||||
|
|
||||||
|
// 读取完整Excel数据
|
||||||
|
Map<String, Object> fullData = ExcelParseUtil.parseFullExcel(file);
|
||||||
|
|
||||||
Map<String, Object> result = new HashMap<>();
|
Map<String, Object> result = new HashMap<>();
|
||||||
result.put("total", brands.size());
|
result.put("total", brands.size());
|
||||||
result.put("brands", brands);
|
result.put("brands", brands);
|
||||||
|
result.put("headers", fullData.get("headers"));
|
||||||
|
result.put("allRows", fullData.get("rows"));
|
||||||
return JsonData.buildSuccess(result);
|
return JsonData.buildSuccess(result);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
return JsonData.buildError("提取失败: " + e.getMessage());
|
return JsonData.buildError("提取失败: " + e.getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据ASIN列表从Excel中过滤完整行数据
|
||||||
|
*/
|
||||||
|
@PostMapping("/filterByAsins")
|
||||||
|
public JsonData filterByAsins(@RequestParam("file") MultipartFile file, @RequestParam("asins") String asinsJson) {
|
||||||
|
try {
|
||||||
|
if (asinsJson == null || asinsJson.trim().isEmpty()) {
|
||||||
|
return JsonData.buildError("ASIN列表不能为空");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用Jackson解析JSON数组
|
||||||
|
List<String> asins;
|
||||||
|
try {
|
||||||
|
asins = objectMapper.readValue(asinsJson,
|
||||||
|
objectMapper.getTypeFactory().constructCollectionType(List.class, String.class));
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("解析ASIN列表JSON失败: {}", asinsJson, e);
|
||||||
|
return JsonData.buildError("ASIN列表格式错误: " + e.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (asins == null || asins.isEmpty()) {
|
||||||
|
return JsonData.buildError("ASIN列表不能为空");
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info("接收到ASIN过滤请求,ASIN数量: {}", asins.size());
|
||||||
|
|
||||||
|
Map<String, Object> result = ExcelParseUtil.filterExcelByAsins(file, asins);
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
List<Map<String, Object>> filteredRows = (List<Map<String, Object>>) result.get("filteredRows");
|
||||||
|
|
||||||
|
Map<String, Object> response = new HashMap<>();
|
||||||
|
response.put("headers", result.get("headers"));
|
||||||
|
response.put("filteredRows", filteredRows);
|
||||||
|
response.put("total", filteredRows.size());
|
||||||
|
|
||||||
|
logger.info("ASIN过滤完成,过滤出 {} 行数据", filteredRows.size());
|
||||||
|
|
||||||
|
return JsonData.buildSuccess(response);
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("根据ASIN过滤失败", e);
|
||||||
|
return JsonData.buildError("过滤失败: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据品牌列表从Excel中过滤完整行数据
|
||||||
|
*/
|
||||||
|
@PostMapping("/filterByBrands")
|
||||||
|
public JsonData filterByBrands(@RequestParam("file") MultipartFile file, @RequestParam("brands") String brandsJson) {
|
||||||
|
try {
|
||||||
|
if (brandsJson == null || brandsJson.trim().isEmpty()) {
|
||||||
|
return JsonData.buildError("品牌列表不能为空");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用Jackson解析JSON数组
|
||||||
|
List<String> brands;
|
||||||
|
try {
|
||||||
|
brands = objectMapper.readValue(brandsJson,
|
||||||
|
objectMapper.getTypeFactory().constructCollectionType(List.class, String.class));
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("解析品牌列表JSON失败: {}", brandsJson, e);
|
||||||
|
return JsonData.buildError("品牌列表格式错误: " + e.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (brands == null || brands.isEmpty()) {
|
||||||
|
return JsonData.buildError("品牌列表不能为空");
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info("接收到品牌过滤请求,品牌数量: {}", brands.size());
|
||||||
|
|
||||||
|
Map<String, Object> result = ExcelParseUtil.filterExcelByBrands(file, brands);
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
List<Map<String, Object>> filteredRows = (List<Map<String, Object>>) result.get("filteredRows");
|
||||||
|
|
||||||
|
Map<String, Object> response = new HashMap<>();
|
||||||
|
response.put("headers", result.get("headers"));
|
||||||
|
response.put("filteredRows", filteredRows);
|
||||||
|
response.put("total", filteredRows.size());
|
||||||
|
|
||||||
|
logger.info("品牌过滤完成,过滤出 {} 行数据", filteredRows.size());
|
||||||
|
|
||||||
|
return JsonData.buildSuccess(response);
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("根据品牌过滤失败", e);
|
||||||
|
return JsonData.buildError("过滤失败: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存商标查询会话
|
||||||
|
*/
|
||||||
|
@PostMapping("/saveSession")
|
||||||
|
public JsonData saveSession(@RequestBody Map<String, Object> sessionData,
|
||||||
|
@RequestHeader(value = "username", required = false) String username) {
|
||||||
|
try {
|
||||||
|
if (username == null || username.trim().isEmpty()) {
|
||||||
|
username = "default";
|
||||||
|
}
|
||||||
|
|
||||||
|
String sessionId = UUID.randomUUID().toString();
|
||||||
|
TrademarkSessionEntity entity = new TrademarkSessionEntity();
|
||||||
|
entity.setSessionId(sessionId);
|
||||||
|
entity.setUsername(username);
|
||||||
|
entity.setFileName((String) sessionData.get("fileName"));
|
||||||
|
entity.setResultData(objectMapper.writeValueAsString(sessionData.get("resultData")));
|
||||||
|
entity.setFullData(objectMapper.writeValueAsString(sessionData.get("fullData")));
|
||||||
|
entity.setHeaders(objectMapper.writeValueAsString(sessionData.get("headers")));
|
||||||
|
entity.setTaskProgress(objectMapper.writeValueAsString(sessionData.get("taskProgress")));
|
||||||
|
entity.setQueryStatus((String) sessionData.get("queryStatus"));
|
||||||
|
|
||||||
|
sessionRepository.save(entity);
|
||||||
|
|
||||||
|
// 清理7天前的数据
|
||||||
|
sessionRepository.deleteByCreatedAtBefore(LocalDateTime.now().minusDays(7));
|
||||||
|
|
||||||
|
logger.info("保存商标查询会话: {} (用户: {})", sessionId, username);
|
||||||
|
|
||||||
|
Map<String, String> result = new HashMap<>();
|
||||||
|
result.put("sessionId", sessionId);
|
||||||
|
return JsonData.buildSuccess(result);
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("保存会话失败", e);
|
||||||
|
return JsonData.buildError("保存失败: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据sessionId恢复查询会话
|
||||||
|
*/
|
||||||
|
@GetMapping("/getSession")
|
||||||
|
public JsonData getSession(@RequestParam("sessionId") String sessionId,
|
||||||
|
@RequestHeader(value = "username", required = false) String username) {
|
||||||
|
try {
|
||||||
|
if (username == null || username.trim().isEmpty()) {
|
||||||
|
username = "default";
|
||||||
|
}
|
||||||
|
|
||||||
|
Optional<TrademarkSessionEntity> opt = sessionRepository.findBySessionIdAndUsername(sessionId, username);
|
||||||
|
if (!opt.isPresent()) {
|
||||||
|
return JsonData.buildError("会话不存在或已过期");
|
||||||
|
}
|
||||||
|
|
||||||
|
TrademarkSessionEntity entity = opt.get();
|
||||||
|
Map<String, Object> result = new HashMap<>();
|
||||||
|
result.put("fileName", entity.getFileName());
|
||||||
|
result.put("resultData", objectMapper.readValue(entity.getResultData(), List.class));
|
||||||
|
result.put("fullData", objectMapper.readValue(entity.getFullData(), List.class));
|
||||||
|
result.put("headers", objectMapper.readValue(entity.getHeaders(), List.class));
|
||||||
|
result.put("taskProgress", objectMapper.readValue(entity.getTaskProgress(), Map.class));
|
||||||
|
result.put("queryStatus", entity.getQueryStatus());
|
||||||
|
|
||||||
|
logger.info("恢复商标查询会话: {} (用户: {})", sessionId, username);
|
||||||
|
return JsonData.buildSuccess(result);
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("恢复会话失败", e);
|
||||||
|
return JsonData.buildError("恢复失败: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package com.tashow.erp.entity;
|
||||||
|
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
import lombok.Data;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Table(name = "brand_trademark_cache",
|
||||||
|
uniqueConstraints = @UniqueConstraint(columnNames = {"brand"}))
|
||||||
|
@Data
|
||||||
|
public class BrandTrademarkCacheEntity {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@Column(nullable = false, unique = true)
|
||||||
|
private String brand;
|
||||||
|
|
||||||
|
@Column(nullable = false)
|
||||||
|
private Boolean registered;
|
||||||
|
|
||||||
|
@Column
|
||||||
|
private String username;
|
||||||
|
|
||||||
|
@Column(name = "created_at", nullable = false)
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
|
||||||
|
@PrePersist
|
||||||
|
protected void onCreate() {
|
||||||
|
createdAt = LocalDateTime.now();
|
||||||
|
username = "global"; // 全局缓存
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
package com.tashow.erp.entity;
|
||||||
|
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
import lombok.Data;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Table(name = "trademark_sessions")
|
||||||
|
@Data
|
||||||
|
public class TrademarkSessionEntity {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@Column(name = "session_id", unique = true, nullable = false)
|
||||||
|
private String sessionId;
|
||||||
|
|
||||||
|
@Column(nullable = false)
|
||||||
|
private String username;
|
||||||
|
|
||||||
|
@Column(name = "file_name")
|
||||||
|
private String fileName;
|
||||||
|
|
||||||
|
@Column(name = "result_data", columnDefinition = "TEXT")
|
||||||
|
private String resultData;
|
||||||
|
|
||||||
|
@Column(name = "full_data", columnDefinition = "TEXT")
|
||||||
|
private String fullData;
|
||||||
|
|
||||||
|
@Column(columnDefinition = "TEXT")
|
||||||
|
private String headers;
|
||||||
|
|
||||||
|
@Column(name = "task_progress", columnDefinition = "TEXT")
|
||||||
|
private String taskProgress;
|
||||||
|
|
||||||
|
@Column(name = "query_status")
|
||||||
|
private String queryStatus;
|
||||||
|
|
||||||
|
@Column(name = "created_at", nullable = false)
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
|
||||||
|
@PrePersist
|
||||||
|
protected void onCreate() {
|
||||||
|
createdAt = LocalDateTime.now();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package com.tashow.erp.repository;
|
||||||
|
|
||||||
|
import com.tashow.erp.entity.BrandTrademarkCacheEntity;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.data.jpa.repository.Modifying;
|
||||||
|
import org.springframework.data.jpa.repository.Query;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
public interface BrandTrademarkCacheRepository extends JpaRepository<BrandTrademarkCacheEntity, Long> {
|
||||||
|
|
||||||
|
boolean existsByBrand(String brand);
|
||||||
|
|
||||||
|
Optional<BrandTrademarkCacheEntity> findByBrandAndCreatedAtAfter(
|
||||||
|
String brand, LocalDateTime cutoffTime);
|
||||||
|
|
||||||
|
List<BrandTrademarkCacheEntity> findByBrandInAndCreatedAtAfter(
|
||||||
|
List<String> brands, LocalDateTime cutoffTime);
|
||||||
|
|
||||||
|
@Modifying
|
||||||
|
@Transactional
|
||||||
|
@Query("DELETE FROM BrandTrademarkCacheEntity WHERE createdAt < ?1")
|
||||||
|
void deleteByCreatedAtBefore(LocalDateTime cutoffTime);
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package com.tashow.erp.repository;
|
||||||
|
|
||||||
|
import com.tashow.erp.entity.TrademarkSessionEntity;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.data.jpa.repository.Modifying;
|
||||||
|
import org.springframework.data.jpa.repository.Query;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
public interface TrademarkSessionRepository extends JpaRepository<TrademarkSessionEntity, Long> {
|
||||||
|
|
||||||
|
Optional<TrademarkSessionEntity> findBySessionIdAndUsername(String sessionId, String username);
|
||||||
|
|
||||||
|
@Modifying
|
||||||
|
@Transactional
|
||||||
|
@Query("DELETE FROM TrademarkSessionEntity WHERE createdAt < ?1")
|
||||||
|
void deleteByCreatedAtBefore(LocalDateTime cutoffTime);
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package com.tashow.erp.service;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public interface BrandTrademarkCacheService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量获取缓存(1天内有效,全局共享)
|
||||||
|
*/
|
||||||
|
Map<String, Boolean> getCached(List<String> brands);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量保存查询结果(全局共享)
|
||||||
|
*/
|
||||||
|
void saveResults(Map<String, Boolean> results);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清理1天前的过期数据
|
||||||
|
*/
|
||||||
|
void cleanExpired();
|
||||||
|
}
|
||||||
|
|
||||||
@@ -30,6 +30,10 @@ public class CacheService {
|
|||||||
private CacheDataRepository cacheDataRepository;
|
private CacheDataRepository cacheDataRepository;
|
||||||
@Autowired
|
@Autowired
|
||||||
private UpdateStatusRepository updateStatusRepository;
|
private UpdateStatusRepository updateStatusRepository;
|
||||||
|
@Autowired
|
||||||
|
private BrandTrademarkCacheRepository brandTrademarkCacheRepository;
|
||||||
|
@Autowired
|
||||||
|
private TrademarkSessionRepository trademarkSessionRepository;
|
||||||
|
|
||||||
public void saveAuthToken(String service, String token, long expireTimeMillis) {
|
public void saveAuthToken(String service, String token, long expireTimeMillis) {
|
||||||
try {
|
try {
|
||||||
@@ -46,25 +50,14 @@ public class CacheService {
|
|||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
public void clearCache() {
|
public void clearCache() {
|
||||||
|
|
||||||
|
|
||||||
// 清理所有产品数据
|
|
||||||
rakutenProductRepository.deleteAll();
|
rakutenProductRepository.deleteAll();
|
||||||
|
|
||||||
|
|
||||||
amazonProductRepository.deleteAll();
|
amazonProductRepository.deleteAll();
|
||||||
|
|
||||||
|
|
||||||
alibaba1688ProductRepository.deleteAll();
|
alibaba1688ProductRepository.deleteAll();
|
||||||
|
|
||||||
|
|
||||||
// 清理所有订单数据
|
|
||||||
banmaOrderRepository.deleteAll();
|
banmaOrderRepository.deleteAll();
|
||||||
|
|
||||||
zebraOrderRepository.deleteAll();
|
zebraOrderRepository.deleteAll();
|
||||||
// 清理通用缓存和更新状态
|
|
||||||
cacheDataRepository.deleteAll();
|
cacheDataRepository.deleteAll();
|
||||||
|
brandTrademarkCacheRepository.deleteAll();
|
||||||
|
trademarkSessionRepository.deleteAll();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
package com.tashow.erp.service.impl;
|
||||||
|
|
||||||
|
import com.tashow.erp.entity.BrandTrademarkCacheEntity;
|
||||||
|
import com.tashow.erp.repository.BrandTrademarkCacheRepository;
|
||||||
|
import com.tashow.erp.service.BrandTrademarkCacheService;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
public class BrandTrademarkCacheServiceImpl implements BrandTrademarkCacheService {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private BrandTrademarkCacheRepository repository;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Map<String, Boolean> getCached(List<String> brands) {
|
||||||
|
LocalDateTime cutoffTime = LocalDateTime.now().minusDays(1);
|
||||||
|
List<BrandTrademarkCacheEntity> cached = repository.findByBrandInAndCreatedAtAfter(brands, cutoffTime);
|
||||||
|
|
||||||
|
Map<String, Boolean> result = new HashMap<>();
|
||||||
|
cached.forEach(e -> result.put(e.getBrand(), e.getRegistered()));
|
||||||
|
|
||||||
|
if (!result.isEmpty()) {
|
||||||
|
log.info("从全局缓存获取 {} 个品牌数据", result.size());
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void saveResults(Map<String, Boolean> results) {
|
||||||
|
results.forEach((brand, registered) -> {
|
||||||
|
if (!repository.existsByBrand(brand)) {
|
||||||
|
BrandTrademarkCacheEntity entity = new BrandTrademarkCacheEntity();
|
||||||
|
entity.setBrand(brand);
|
||||||
|
entity.setRegistered(registered);
|
||||||
|
repository.save(entity);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public void cleanExpired() {
|
||||||
|
LocalDateTime cutoffTime = LocalDateTime.now().minusDays(1);
|
||||||
|
repository.deleteByCreatedAtBefore(cutoffTime);
|
||||||
|
log.info("清理1天前的品牌商标缓存");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -6,8 +6,8 @@ import lombok.extern.slf4j.Slf4j;
|
|||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.util.ArrayList;
|
import java.util.*;
|
||||||
import java.util.List;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Excel 解析工具类
|
* Excel 解析工具类
|
||||||
@@ -18,6 +18,23 @@ import java.util.List;
|
|||||||
@Slf4j
|
@Slf4j
|
||||||
public class ExcelParseUtil {
|
public class ExcelParseUtil {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 自动查找表头行索引(在前2行中查找)
|
||||||
|
* @param rows Excel所有行
|
||||||
|
* @param columnName 列名(如"品牌")
|
||||||
|
* @return 表头行索引,未找到返回-1
|
||||||
|
*/
|
||||||
|
private static int findHeaderRow(List<List<Object>> rows, String columnName) {
|
||||||
|
for (int r = 0; r < Math.min(2, rows.size()); r++) {
|
||||||
|
for (Object cell : rows.get(r)) {
|
||||||
|
if (cell != null && columnName.equals(cell.toString().replaceAll("\\s+", ""))) {
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 解析 Excel 文件第一列数据
|
* 解析 Excel 文件第一列数据
|
||||||
* 通用方法,适用于店铺名、ASIN、订单号等标识符解析
|
* 通用方法,适用于店铺名、ASIN、订单号等标识符解析
|
||||||
@@ -102,7 +119,6 @@ public class ExcelParseUtil {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
||||||
* 根据列名解析数据(自动适配第1行或第2行为表头)
|
* 根据列名解析数据(自动适配第1行或第2行为表头)
|
||||||
*/
|
*/
|
||||||
public static List<String> parseColumnByName(MultipartFile file, String columnName) {
|
public static List<String> parseColumnByName(MultipartFile file, String columnName) {
|
||||||
@@ -112,23 +128,18 @@ public class ExcelParseUtil {
|
|||||||
List<List<Object>> rows = reader.read();
|
List<List<Object>> rows = reader.read();
|
||||||
if (rows.isEmpty()) return result;
|
if (rows.isEmpty()) return result;
|
||||||
|
|
||||||
// 查找表头行和列索引
|
int headerRow = findHeaderRow(rows, columnName);
|
||||||
int headerRow = -1, colIdx = -1;
|
if (headerRow < 0) return result;
|
||||||
for (int r = 0; r < Math.min(2, rows.size()); r++) {
|
|
||||||
for (int c = 0; c < rows.get(r).size(); c++) {
|
int colIdx = -1;
|
||||||
String col = rows.get(r).get(c).toString().replaceAll("\\s+", "");
|
for (int c = 0; c < rows.get(headerRow).size(); c++) {
|
||||||
if (col.equals(columnName)) {
|
if (rows.get(headerRow).get(c) != null &&
|
||||||
headerRow = r;
|
columnName.equals(rows.get(headerRow).get(c).toString().replaceAll("\\s+", ""))) {
|
||||||
colIdx = c;
|
colIdx = c;
|
||||||
break;
|
break;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (colIdx != -1) break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (colIdx == -1) return result;
|
|
||||||
|
|
||||||
// 从表头下一行开始读数据
|
|
||||||
for (int i = headerRow + 1; i < rows.size(); i++) {
|
for (int i = headerRow + 1; i < rows.size(); i++) {
|
||||||
List<Object> row = rows.get(i);
|
List<Object> row = rows.get(i);
|
||||||
if (row.size() > colIdx && row.get(colIdx) != null) {
|
if (row.size() > colIdx && row.get(colIdx) != null) {
|
||||||
@@ -141,4 +152,186 @@ public class ExcelParseUtil {
|
|||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 读取Excel的完整数据(包含表头和所有行,自动适配第1行或第2行为表头)
|
||||||
|
* @param file Excel文件
|
||||||
|
* @return Map包含headers(表头列表)和rows(数据行列表,每行是Map)
|
||||||
|
*/
|
||||||
|
public static Map<String, Object> parseFullExcel(MultipartFile file) {
|
||||||
|
Map<String, Object> result = new HashMap<>();
|
||||||
|
List<String> headers = new ArrayList<>();
|
||||||
|
List<Map<String, Object>> rows = new ArrayList<>();
|
||||||
|
|
||||||
|
try (InputStream in = file.getInputStream()) {
|
||||||
|
ExcelReader reader = ExcelUtil.getReader(in, 0);
|
||||||
|
List<List<Object>> allRows = reader.read();
|
||||||
|
|
||||||
|
if (allRows.isEmpty()) {
|
||||||
|
log.warn("Excel文件为空");
|
||||||
|
result.put("headers", headers);
|
||||||
|
result.put("rows", rows);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
int headerRowIndex = Math.max(0, findHeaderRow(allRows, "品牌"));
|
||||||
|
log.info("检测到表头行:第{}行", headerRowIndex + 1);
|
||||||
|
|
||||||
|
for (Object cell : allRows.get(headerRowIndex)) {
|
||||||
|
headers.add(cell != null ? cell.toString().trim() : "");
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = headerRowIndex + 1; i < allRows.size(); i++) {
|
||||||
|
List<Object> row = allRows.get(i);
|
||||||
|
Map<String, Object> rowMap = new HashMap<>();
|
||||||
|
for (int j = 0; j < Math.min(headers.size(), row.size()); j++) {
|
||||||
|
rowMap.put(headers.get(j), row.get(j));
|
||||||
|
}
|
||||||
|
rows.add(rowMap);
|
||||||
|
}
|
||||||
|
|
||||||
|
result.put("headers", headers);
|
||||||
|
result.put("rows", rows);
|
||||||
|
log.info("解析Excel: {}, 表头{}列, 数据{}行", file.getOriginalFilename(), headers.size(), rows.size());
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("解析Excel失败: {}", e.getMessage(), e);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据ASIN列表从Excel中过滤完整行数据(自动适配第1行或第2行为表头)
|
||||||
|
* @param file Excel文件
|
||||||
|
* @param asins ASIN列表
|
||||||
|
* @return Map包含headers(表头)和filteredRows(过滤后的完整行数据)
|
||||||
|
*/
|
||||||
|
public static Map<String, Object> filterExcelByAsins(MultipartFile file, List<String> asins) {
|
||||||
|
Map<String, Object> result = new HashMap<>();
|
||||||
|
List<String> headers = new ArrayList<>();
|
||||||
|
List<Map<String, Object>> filteredRows = new ArrayList<>();
|
||||||
|
|
||||||
|
try (InputStream in = file.getInputStream()) {
|
||||||
|
ExcelReader reader = ExcelUtil.getReader(in, 0);
|
||||||
|
List<List<Object>> allRows = reader.read();
|
||||||
|
|
||||||
|
if (allRows.isEmpty()) {
|
||||||
|
result.put("headers", headers);
|
||||||
|
result.put("filteredRows", filteredRows);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
int headerRowIndex = Math.max(0, findHeaderRow(allRows, "ASIN"));
|
||||||
|
if (headerRowIndex < 0) {
|
||||||
|
log.warn("未找到'ASIN'列");
|
||||||
|
result.put("headers", headers);
|
||||||
|
result.put("filteredRows", filteredRows);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
int asinColIndex = -1;
|
||||||
|
List<Object> headerRow = allRows.get(headerRowIndex);
|
||||||
|
for (int c = 0; c < headerRow.size(); c++) {
|
||||||
|
if (headerRow.get(c) != null && "ASIN".equals(headerRow.get(c).toString().replaceAll("\\s+", ""))) {
|
||||||
|
asinColIndex = c;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (Object cell : headerRow) {
|
||||||
|
headers.add(cell != null ? cell.toString().trim() : "");
|
||||||
|
}
|
||||||
|
|
||||||
|
Set<String> asinSet = asins.stream().map(String::trim).collect(Collectors.toSet());
|
||||||
|
|
||||||
|
for (int i = headerRowIndex + 1; i < allRows.size(); i++) {
|
||||||
|
List<Object> row = allRows.get(i);
|
||||||
|
if (row.size() > asinColIndex && row.get(asinColIndex) != null
|
||||||
|
&& asinSet.contains(row.get(asinColIndex).toString().trim())) {
|
||||||
|
Map<String, Object> rowMap = new HashMap<>();
|
||||||
|
for (int j = 0; j < Math.min(headers.size(), row.size()); j++) {
|
||||||
|
rowMap.put(headers.get(j), row.get(j));
|
||||||
|
}
|
||||||
|
filteredRows.add(rowMap);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result.put("headers", headers);
|
||||||
|
result.put("filteredRows", filteredRows);
|
||||||
|
log.info("ASIN过滤: {}, {}个ASIN -> {}行数据", file.getOriginalFilename(), asins.size(), filteredRows.size());
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("ASIN过滤失败: {}", e.getMessage(), e);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据品牌列表从Excel中过滤完整行数据(自动适配第1行或第2行为表头)
|
||||||
|
* @param file Excel文件
|
||||||
|
* @param brands 品牌列表
|
||||||
|
* @return Map包含headers(表头)和filteredRows(过滤后的完整行数据)
|
||||||
|
*/
|
||||||
|
public static Map<String, Object> filterExcelByBrands(MultipartFile file, List<String> brands) {
|
||||||
|
Map<String, Object> result = new HashMap<>();
|
||||||
|
List<String> headers = new ArrayList<>();
|
||||||
|
List<Map<String, Object>> filteredRows = new ArrayList<>();
|
||||||
|
|
||||||
|
try (InputStream in = file.getInputStream()) {
|
||||||
|
ExcelReader reader = ExcelUtil.getReader(in, 0);
|
||||||
|
List<List<Object>> allRows = reader.read();
|
||||||
|
|
||||||
|
if (allRows.isEmpty()) {
|
||||||
|
result.put("headers", headers);
|
||||||
|
result.put("filteredRows", filteredRows);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
int headerRowIndex = findHeaderRow(allRows, "品牌");
|
||||||
|
if (headerRowIndex < 0) {
|
||||||
|
log.warn("未找到'品牌'列");
|
||||||
|
result.put("headers", headers);
|
||||||
|
result.put("filteredRows", filteredRows);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
int brandColIndex = -1;
|
||||||
|
List<Object> headerRow = allRows.get(headerRowIndex);
|
||||||
|
for (int c = 0; c < headerRow.size(); c++) {
|
||||||
|
if (headerRow.get(c) != null && "品牌".equals(headerRow.get(c).toString().replaceAll("\\s+", ""))) {
|
||||||
|
brandColIndex = c;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (Object cell : headerRow) {
|
||||||
|
headers.add(cell != null ? cell.toString().trim() : "");
|
||||||
|
}
|
||||||
|
|
||||||
|
Set<String> brandSet = brands.stream().map(String::trim).collect(Collectors.toSet());
|
||||||
|
|
||||||
|
for (int i = headerRowIndex + 1; i < allRows.size(); i++) {
|
||||||
|
List<Object> row = allRows.get(i);
|
||||||
|
if (row.size() > brandColIndex && row.get(brandColIndex) != null
|
||||||
|
&& brandSet.contains(row.get(brandColIndex).toString().trim())) {
|
||||||
|
Map<String, Object> rowMap = new HashMap<>();
|
||||||
|
for (int j = 0; j < Math.min(headers.size(), row.size()); j++) {
|
||||||
|
rowMap.put(headers.get(j), row.get(j));
|
||||||
|
}
|
||||||
|
filteredRows.add(rowMap);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result.put("headers", headers);
|
||||||
|
result.put("filteredRows", filteredRows);
|
||||||
|
log.info("品牌过滤: {}, {}个品牌 -> {}行数据", file.getOriginalFilename(), brands.size(), filteredRows.size());
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("品牌过滤失败: {}", e.getMessage(), e);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -2,9 +2,6 @@ package com.tashow.erp.utils;
|
|||||||
import io.github.bonigarcia.wdm.WebDriverManager;
|
import io.github.bonigarcia.wdm.WebDriverManager;
|
||||||
import org.openqa.selenium.chrome.ChromeDriver;
|
import org.openqa.selenium.chrome.ChromeDriver;
|
||||||
import org.openqa.selenium.chrome.ChromeOptions;
|
import org.openqa.selenium.chrome.ChromeOptions;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.web.client.RestTemplate;
|
|
||||||
|
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
package com.tashow.erp.utils;
|
package com.tashow.erp.utils;
|
||||||
|
import com.tashow.erp.service.BrandTrademarkCacheService;
|
||||||
import jakarta.annotation.PreDestroy;
|
import jakarta.annotation.PreDestroy;
|
||||||
import org.openqa.selenium.JavascriptExecutor;
|
import org.openqa.selenium.JavascriptExecutor;
|
||||||
import org.openqa.selenium.chrome.ChromeDriver;
|
import org.openqa.selenium.chrome.ChromeDriver;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -15,12 +15,15 @@ import java.util.*;
|
|||||||
public class TrademarkCheckUtil {
|
public class TrademarkCheckUtil {
|
||||||
@Autowired
|
@Autowired
|
||||||
private ProxyPool proxyPool;
|
private ProxyPool proxyPool;
|
||||||
|
@Autowired
|
||||||
|
private BrandTrademarkCacheService cacheService;
|
||||||
private ChromeDriver driver;
|
private ChromeDriver driver;
|
||||||
|
|
||||||
private synchronized void ensureInit() {
|
private synchronized void ensureInit() {
|
||||||
if (driver == null) {
|
if (driver == null) {
|
||||||
for (int i = 0; i < 5; i++) {
|
for (int i = 0; i < 5; i++) {
|
||||||
try {
|
try {
|
||||||
driver = SeleniumUtil.createDriver(false, proxyPool.getProxy());
|
driver = SeleniumUtil.createDriver(true, proxyPool.getProxy());
|
||||||
driver.get("https://tmsearch.uspto.gov/search/search-results");
|
driver.get("https://tmsearch.uspto.gov/search/search-results");
|
||||||
Thread.sleep(6000);
|
Thread.sleep(6000);
|
||||||
return; // 成功则返回
|
return; // 成功则返回
|
||||||
@@ -36,7 +39,7 @@ public class TrademarkCheckUtil {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public synchronized Map<String, Boolean> batchCheck(List<String> brands) {
|
public synchronized Map<String, Boolean> batchCheck(List<String> brands, Map<String, Boolean> alreadyQueried) {
|
||||||
ensureInit();
|
ensureInit();
|
||||||
|
|
||||||
// 构建批量查询脚本(带错误诊断)
|
// 构建批量查询脚本(带错误诊断)
|
||||||
@@ -80,16 +83,32 @@ public class TrademarkCheckUtil {
|
|||||||
List<Map<String, Object>> results = (List<Map<String, Object>>)
|
List<Map<String, Object>> results = (List<Map<String, Object>>)
|
||||||
((JavascriptExecutor) driver).executeAsyncScript(script, brands);
|
((JavascriptExecutor) driver).executeAsyncScript(script, brands);
|
||||||
|
|
||||||
// 检测是否有403错误
|
// 检测是否有网络错误(包括403、Failed to fetch等)
|
||||||
boolean has403 = results.stream()
|
boolean hasNetworkError = results.stream()
|
||||||
.anyMatch(item -> {
|
.anyMatch(item -> {
|
||||||
String error = (String) item.get("error");
|
String error = (String) item.get("error");
|
||||||
return error != null && error.contains("HTTP 403");
|
return error != null && (
|
||||||
|
error.contains("HTTP 403") ||
|
||||||
|
error.contains("Failed to fetch") ||
|
||||||
|
error.contains("NetworkError") ||
|
||||||
|
error.contains("TypeError")
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
// 如果有403,切换代理并重试
|
// 如果有网络错误,切换代理并重试
|
||||||
if (has403) {
|
if (hasNetworkError) {
|
||||||
System.err.println("检测到403,切换代理并重试...");
|
System.err.println("检测到网络错误,切换代理并重试...");
|
||||||
|
|
||||||
|
// 切换代理前保存已查询的品牌
|
||||||
|
if (alreadyQueried != null && !alreadyQueried.isEmpty()) {
|
||||||
|
try {
|
||||||
|
cacheService.saveResults(alreadyQueried);
|
||||||
|
System.out.println("代理切换,已保存 " + alreadyQueried.size() + " 个品牌到缓存");
|
||||||
|
} catch (Exception e) {
|
||||||
|
System.err.println("保存缓存失败: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
try { driver.quit(); } catch (Exception e) {}
|
try { driver.quit(); } catch (Exception e) {}
|
||||||
driver = null;
|
driver = null;
|
||||||
ensureInit();
|
ensureInit();
|
||||||
|
|||||||
@@ -25,16 +25,13 @@ import java.util.*;
|
|||||||
@Anonymous
|
@Anonymous
|
||||||
public class MarkController {
|
public class MarkController {
|
||||||
private static final String API_SECRET = "e10adc3949ba59abbe56e057f20f883e";
|
private static final String API_SECRET = "e10adc3949ba59abbe56e057f20f883e";
|
||||||
// erp_client_sb 服务地址
|
|
||||||
private static final String ERP_CLIENT_BASE_URL = "http://127.0.0.1:8081";
|
private static final String ERP_CLIENT_BASE_URL = "http://127.0.0.1:8081";
|
||||||
|
|
||||||
private final RestTemplate restTemplate = new RestTemplate();
|
private final RestTemplate restTemplate = new RestTemplate();
|
||||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||||
@Autowired
|
@Autowired
|
||||||
private RedisCache redisCache;
|
private RedisCache redisCache;
|
||||||
@Autowired
|
@Autowired
|
||||||
private IMarkService markService;
|
private IMarkService markService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取任务列表
|
* 获取任务列表
|
||||||
*/
|
*/
|
||||||
@@ -82,42 +79,49 @@ public class MarkController {
|
|||||||
dNode = reJson.get("D").get("items").get(0);
|
dNode = reJson.get("D").get("items").get(0);
|
||||||
downloadUrl = reJson.get("D").get("items").get(0).get("download_url").asText();
|
downloadUrl = reJson.get("D").get("items").get(0).get("download_url").asText();
|
||||||
}
|
}
|
||||||
|
|
||||||
String tempFilePath = System.getProperty("java.io.tmpdir") + "/trademark_" + System.currentTimeMillis() + ".xlsx";
|
String tempFilePath = System.getProperty("java.io.tmpdir") + "/trademark_" + System.currentTimeMillis() + ".xlsx";
|
||||||
HttpUtil.downloadFile(downloadUrl, FileUtil.file(tempFilePath));
|
HttpUtil.downloadFile(downloadUrl, FileUtil.file(tempFilePath));
|
||||||
|
|
||||||
List<Map<String, Object>> filteredData = new ArrayList<>();
|
List<Map<String, Object>> filteredData = new ArrayList<>();
|
||||||
|
List<String> excelHeaders = new ArrayList<>();
|
||||||
ExcelReader reader = null;
|
ExcelReader reader = null;
|
||||||
try {
|
try {
|
||||||
reader = ExcelUtil.getReader(FileUtil.file(tempFilePath));
|
reader = ExcelUtil.getReader(FileUtil.file(tempFilePath));
|
||||||
List<List<Object>> rows = reader.read();
|
List<List<Object>> rows = reader.read();
|
||||||
|
|
||||||
// 找到各列的索引
|
if (rows.isEmpty()) {
|
||||||
int asinIndex = -1, brandIndex = -1, trademarkTypeIndex = -1, registerDateIndex = -1, productImageIndex = -1;
|
throw new RuntimeException("Excel文件为空");
|
||||||
if (!rows.isEmpty()) {
|
}
|
||||||
List<Object> header = rows.get(0);
|
|
||||||
for (int i = 0; i < header.size(); i++) {
|
// 读取表头
|
||||||
String headerName = header.get(i).toString().trim();
|
List<Object> headerRow = rows.get(0);
|
||||||
if (headerName.equals("ASIN")) asinIndex = i;
|
for (Object cell : headerRow) {
|
||||||
else if (headerName.equals("品牌")) brandIndex = i;
|
excelHeaders.add(cell != null ? cell.toString().trim() : "");
|
||||||
else if (headerName.equals("商标类型")) trademarkTypeIndex = i;
|
}
|
||||||
else if (headerName.equals("注册时间")) registerDateIndex = i;
|
|
||||||
else if (headerName.equals("商品主图")) productImageIndex = i;
|
// 找到商标类型列的索引
|
||||||
|
int trademarkTypeIndex = -1;
|
||||||
|
for (int i = 0; i < excelHeaders.size(); i++) {
|
||||||
|
if ("商标类型".equals(excelHeaders.get(i))) {
|
||||||
|
trademarkTypeIndex = i;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 过滤TM和未注册数据
|
if (trademarkTypeIndex < 0) {
|
||||||
|
throw new RuntimeException("未找到'商标类型'列");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 过滤TM和未注册数据,保留所有列
|
||||||
for (int i = 1; i < rows.size(); i++) {
|
for (int i = 1; i < rows.size(); i++) {
|
||||||
List<Object> row = rows.get(i);
|
List<Object> row = rows.get(i);
|
||||||
if (trademarkTypeIndex >= 0 && row.size() > trademarkTypeIndex) {
|
if (row.size() > trademarkTypeIndex) {
|
||||||
String trademarkType = row.get(trademarkTypeIndex).toString().trim();
|
String trademarkType = row.get(trademarkTypeIndex).toString().trim();
|
||||||
if ("TM".equals(trademarkType) || "未注册".equals(trademarkType)) {
|
if ("TM".equals(trademarkType) || "未注册".equals(trademarkType)) {
|
||||||
Map<String, Object> item = new HashMap<>();
|
Map<String, Object> item = new HashMap<>();
|
||||||
if (asinIndex >= 0 && row.size() > asinIndex) item.put("ASIN", row.get(asinIndex));
|
// 保存所有列的数据
|
||||||
if (brandIndex >= 0 && row.size() > brandIndex) item.put("品牌", row.get(brandIndex));
|
for (int j = 0; j < excelHeaders.size() && j < row.size(); j++) {
|
||||||
if (trademarkTypeIndex >= 0 && row.size() > trademarkTypeIndex) item.put("商标类型", row.get(trademarkTypeIndex));
|
item.put(excelHeaders.get(j), row.get(j));
|
||||||
if (registerDateIndex >= 0 && row.size() > registerDateIndex) item.put("注册时间", row.get(registerDateIndex));
|
}
|
||||||
if (productImageIndex >= 0 && row.size() > productImageIndex) item.put("商品主图", row.get(productImageIndex));
|
|
||||||
filteredData.add(item);
|
filteredData.add(item);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -131,6 +135,7 @@ public class MarkController {
|
|||||||
Map<String, Object> combinedResult = new HashMap<>();
|
Map<String, Object> combinedResult = new HashMap<>();
|
||||||
combinedResult.put("original", dNode);
|
combinedResult.put("original", dNode);
|
||||||
combinedResult.put("filtered", filteredData);
|
combinedResult.put("filtered", filteredData);
|
||||||
|
combinedResult.put("headers", excelHeaders);
|
||||||
|
|
||||||
return AjaxResult.success(combinedResult);
|
return AjaxResult.success(combinedResult);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
@@ -138,7 +143,6 @@ public class MarkController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// 新建任务
|
// 新建任务
|
||||||
@PostMapping("newTask")
|
@PostMapping("newTask")
|
||||||
public AjaxResult newTask(@RequestParam("file") MultipartFile file) {
|
public AjaxResult newTask(@RequestParam("file") MultipartFile file) {
|
||||||
|
|||||||
@@ -108,11 +108,6 @@ spring:
|
|||||||
max-wait: 10s
|
max-wait: 10s
|
||||||
# 关闭超时时间
|
# 关闭超时时间
|
||||||
shutdown-timeout: 100ms
|
shutdown-timeout: 100ms
|
||||||
# 心跳检测配置
|
|
||||||
cluster:
|
|
||||||
refresh:
|
|
||||||
adaptive: true
|
|
||||||
period: 30s
|
|
||||||
# token配置
|
# token配置
|
||||||
token:
|
token:
|
||||||
# 令牌自定义标识
|
# 令牌自定义标识
|
||||||
|
|||||||
@@ -1,52 +0,0 @@
|
|||||||
package com.ruoyi.common.core.redis;
|
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.data.redis.connection.RedisConnectionFactory;
|
|
||||||
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
|
|
||||||
import org.springframework.scheduling.annotation.Scheduled;
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Redis连接健康检查组件
|
|
||||||
* 定期检测Redis连接状态,确保连接可用
|
|
||||||
*
|
|
||||||
* @author ruoyi
|
|
||||||
*/
|
|
||||||
@Slf4j
|
|
||||||
@Component
|
|
||||||
public class RedisHealthCheck {
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
private RedisConnectionFactory redisConnectionFactory;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 每5分钟检查一次Redis连接状态
|
|
||||||
*/
|
|
||||||
@Scheduled(fixedRate = 300000)
|
|
||||||
public void checkRedisConnection() {
|
|
||||||
try {
|
|
||||||
if (redisConnectionFactory instanceof LettuceConnectionFactory) {
|
|
||||||
LettuceConnectionFactory lettuceFactory = (LettuceConnectionFactory) redisConnectionFactory;
|
|
||||||
|
|
||||||
// 验证连接是否有效
|
|
||||||
if (!lettuceFactory.getConnection().ping().equals("PONG")) {
|
|
||||||
log.warn("Redis连接异常,尝试重新连接...");
|
|
||||||
lettuceFactory.resetConnection();
|
|
||||||
log.info("Redis连接已重置");
|
|
||||||
} else {
|
|
||||||
log.debug("Redis连接正常");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("Redis连接检查失败: {}", e.getMessage());
|
|
||||||
try {
|
|
||||||
// 尝试重置连接
|
|
||||||
((LettuceConnectionFactory) redisConnectionFactory).resetConnection();
|
|
||||||
log.info("Redis连接已重置");
|
|
||||||
} catch (Exception ex) {
|
|
||||||
log.error("Redis连接重置失败: {}", ex.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
package com.ruoyi.framework.config;
|
||||||
|
|
||||||
|
import io.lettuce.core.ClientOptions;
|
||||||
|
import io.lettuce.core.SocketOptions;
|
||||||
|
import org.springframework.boot.autoconfigure.data.redis.LettuceClientConfigurationBuilderCustomizer;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lettuce 连接配置
|
||||||
|
* 解决跨公网连接空闲后被中间设备关闭的问题
|
||||||
|
*
|
||||||
|
* @author ruoyi
|
||||||
|
*/
|
||||||
|
@Configuration
|
||||||
|
public class LettuceConfig {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 配置 Lettuce 客户端,双重保障防止连接失效
|
||||||
|
* 1. TCP Keepalive - 操作系统层维持连接活性
|
||||||
|
* 2. Ping Before Activate - 获取连接前验证有效性
|
||||||
|
*/
|
||||||
|
@Bean
|
||||||
|
public LettuceClientConfigurationBuilderCustomizer lettuceClientConfigurationBuilderCustomizer() {
|
||||||
|
return clientConfigurationBuilder -> {
|
||||||
|
clientConfigurationBuilder.clientOptions(ClientOptions.builder()
|
||||||
|
.autoReconnect(true) // 自动重连
|
||||||
|
.pingBeforeActivateConnection(true) // 获取连接前 ping 验证(关键配置)
|
||||||
|
.socketOptions(SocketOptions.builder()
|
||||||
|
.keepAlive(true) // TCP Keepalive 辅助保持连接
|
||||||
|
.connectTimeout(Duration.ofSeconds(10)) // 连接超时 10 秒
|
||||||
|
.build())
|
||||||
|
.build());
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -1,79 +0,0 @@
|
|||||||
package com.ruoyi.framework.config;
|
|
||||||
|
|
||||||
import org.springframework.context.annotation.Bean;
|
|
||||||
import org.springframework.context.annotation.Configuration;
|
|
||||||
import org.springframework.data.redis.connection.RedisConnectionFactory;
|
|
||||||
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
|
|
||||||
import org.springframework.data.redis.connection.lettuce.LettucePoolingClientConfiguration;
|
|
||||||
import org.springframework.data.redis.core.RedisTemplate;
|
|
||||||
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
|
|
||||||
import io.lettuce.core.ClientOptions;
|
|
||||||
import io.lettuce.core.resource.ClientResources;
|
|
||||||
import io.lettuce.core.resource.DefaultClientResources;
|
|
||||||
|
|
||||||
import java.time.Duration;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Redis连接池优化配置
|
|
||||||
* 解决Lettuce连接超时问题
|
|
||||||
*
|
|
||||||
* @author ruoyi
|
|
||||||
*/
|
|
||||||
@Configuration
|
|
||||||
public class RedisPoolConfig {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 配置Lettuce客户端,启用心跳检测和自动重连
|
|
||||||
*/
|
|
||||||
@Bean
|
|
||||||
public ClientResources clientResources() {
|
|
||||||
return DefaultClientResources.builder()
|
|
||||||
.ioThreadPoolSize(4) // IO线程数
|
|
||||||
.computationThreadPoolSize(4) // 计算线程数
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 优化Redis连接池配置
|
|
||||||
*/
|
|
||||||
@Bean
|
|
||||||
public LettucePoolingClientConfiguration lettucePoolConfig(ClientResources clientResources) {
|
|
||||||
GenericObjectPoolConfig<?> poolConfig = new GenericObjectPoolConfig<>();
|
|
||||||
poolConfig.setMaxTotal(50); // 最大连接数
|
|
||||||
poolConfig.setMaxIdle(20); // 最大空闲连接
|
|
||||||
poolConfig.setMinIdle(5); // 最小空闲连接
|
|
||||||
poolConfig.setMaxWaitMillis(10000); // 获取连接最大等待时间
|
|
||||||
|
|
||||||
return LettucePoolingClientConfiguration.builder()
|
|
||||||
.poolConfig(poolConfig)
|
|
||||||
.clientResources(clientResources)
|
|
||||||
.clientOptions(ClientOptions.builder()
|
|
||||||
.autoReconnect(true) // 自动重连
|
|
||||||
.pingBeforeActivateConnection(true) // 连接激活前ping检测
|
|
||||||
.build())
|
|
||||||
.commandTimeout(Duration.ofSeconds(10)) // 命令超时时间
|
|
||||||
.shutdownTimeout(Duration.ofMillis(100)) // 关闭超时时间
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 定期检查Redis连接状态
|
|
||||||
*/
|
|
||||||
@Bean
|
|
||||||
public RedisTemplate<Object, Object> optimizedRedisTemplate(RedisConnectionFactory connectionFactory) {
|
|
||||||
RedisTemplate<Object, Object> template = new RedisTemplate<>();
|
|
||||||
template.setConnectionFactory(connectionFactory);
|
|
||||||
|
|
||||||
// 使用FastJson序列化器
|
|
||||||
FastJson2JsonRedisSerializer serializer = new FastJson2JsonRedisSerializer(Object.class);
|
|
||||||
|
|
||||||
// 设置序列化器
|
|
||||||
template.setKeySerializer(new org.springframework.data.redis.serializer.StringRedisSerializer());
|
|
||||||
template.setValueSerializer(serializer);
|
|
||||||
template.setHashKeySerializer(new org.springframework.data.redis.serializer.StringRedisSerializer());
|
|
||||||
template.setHashValueSerializer(serializer);
|
|
||||||
|
|
||||||
template.afterPropertiesSet();
|
|
||||||
return template;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user