feat(trademark): 实现商标筛查功能并优化相关配置

- 新增商标筛查进度展示界面与交互逻辑
- 实现产品、品牌及平台跟卖许可的分项任务进度追踪
- 添加商标数据导出与任务重试、取消功能
- 调整Redis连接池配置以提升并发性能
- 禁用ChromeDriver预加载,改为按需启动以节省资源- 支持品牌商标远程筛查接口调用与结果解析
- 增加Hutool工具库依赖用于简化IO与Excel处理- 更新USPTO商标查询脚本实现自动化检测
- 修改Ruoyi后台Redis依赖版本并添加集群心跳配置- 切换本地开发环境API地址指向内网测试服务器
This commit is contained in:
2025-11-04 15:39:15 +08:00
parent c9874f1786
commit a62d7b6147
22 changed files with 2063 additions and 168 deletions

View File

@@ -211,7 +211,7 @@ function startSpringBoot() {
}
}
startSpringBoot();
// startSpringBoot();
function stopSpringBoot() {
if (!springProcess) return;
try {
@@ -348,9 +348,9 @@ app.whenReady().then(() => {
}
}
// setTimeout(() => {
// openAppIfNotOpened();
// }, 100);
setTimeout(() => {
openAppIfNotOpened();
}, 100);
app.on('activate', () => {
if (mainWindow && !mainWindow.isDestroyed()) {

View File

@@ -1,13 +1,13 @@
export type HttpMethod = 'GET' | 'POST' | 'DELETE';
export const CONFIG = {
CLIENT_BASE: 'http://localhost:8081',
RUOYI_BASE: 'http://8.138.23.49:8085',
//RUOYI_BASE: 'http://192.168.1.89:8085',
SSE_URL: 'http://8.138.23.49:8085/monitor/account/events'
// RUOYI_BASE: 'http://8.138.23.49:8085',
RUOYI_BASE: 'http://192.168.1.89:8085',
SSE_URL: 'http://192.168.1.89:8085/monitor/account/events'
} as const;
function resolveBase(path: string): string {
if (path.startsWith('/monitor/') || path.startsWith('/system/') || path.startsWith('/tool/banma') || path.startsWith('/tool/genmai')) {
if (path.startsWith('/monitor/') || path.startsWith('/system/') || path.startsWith('/tool/banma') || path.startsWith('/tool/genmai') || path.startsWith('/tool/mark')) {
return CONFIG.RUOYI_BASE;
}
return CONFIG.CLIENT_BASE;

View File

@@ -0,0 +1,21 @@
import { http } from './http'
export const markApi = {
// 新建任务
newTask(file: File) {
const formData = new FormData()
formData.append('file', file)
return http.upload<{ code: number, data: any, msg: string }>('/tool/mark/newTask', formData)
},
// 获取任务列表及筛选数据
getTask() {
return http.get<{ code: number, data: { original: any, filtered: any[] }, msg: string }>('/tool/mark/task')
},
// 品牌商标筛查
brandCheck(brands: string[]) {
return http.post<{ code: number, data: { total: number, filtered: number, passed: number, data: any[] }, msg: string }>('/tool/mark/brandCheck', brands)
}
}

View File

@@ -62,6 +62,26 @@ function handleSizeChange(size: number) {
function handleCurrentChange(page: number) {
currentPage.value = page
}
function handleNewTask() {
if (trademarkPanelRef.value && trademarkPanelRef.value.resetToIdle) {
trademarkPanelRef.value.resetToIdle()
}
trademarkData.value = []
currentPage.value = 1
}
function handleCancelTask() {
if (trademarkPanelRef.value && typeof trademarkPanelRef.value.stopTrademarkQuery === 'function') {
trademarkPanelRef.value.stopTrademarkQuery()
}
}
function handleRetryTask() {
if (trademarkPanelRef.value && typeof trademarkPanelRef.value.startTrademarkQuery === 'function') {
trademarkPanelRef.value.startTrademarkQuery()
}
}
</script>
<template>
@@ -84,8 +104,8 @@ function handleCurrentChange(page: number) {
</div>
<div class="body-layout">
<!-- 左侧步骤栏 -->
<aside :class="['steps-sidebar', currentTab === 'trademark' ? 'wide' : 'narrow']">
<!-- 左侧步骤栏 (商标筛查有数据时隐藏) -->
<aside v-show="!(currentTab === 'trademark' && (trademarkPanelRef?.queryStatus === 'inProgress' || trademarkPanelRef?.queryStatus === 'done' || trademarkPanelRef?.queryStatus === 'error' || trademarkPanelRef?.queryStatus === 'networkError'))" :class="['steps-sidebar', currentTab === 'trademark' ? 'wide' : 'narrow']">
<div class="steps-title">操作流程</div>
<!-- ASIN查询面板 -->
@@ -180,48 +200,168 @@ function handleCurrentChange(page: number) {
</tbody>
</table>
<!-- 商标筛查表格 -->
<table v-if="currentTab === 'trademark'" class="table table-trademark">
<thead>
<tr>
<th>商标名称</th>
<th>状态</th>
<th>类别</th>
<th>权利人</th>
<th>到期日期</th>
<th>相似度</th>
</tr>
</thead>
<tbody>
<tr v-for="(row, idx) in paginatedData" :key="idx">
<td><span class="trademark-name">{{ row.name }}</span></td>
<td>
<span :class="{
'status-registered': row.status === '已注册',
'status-risk': row.status === '近似风险',
'status-available': row.status === '可注册'
}">{{ row.status }}</span>
</td>
<td>{{ row.class }}</td>
<td class="truncate-cell">{{ row.owner }}</td>
<td>{{ row.expireDate }}</td>
<td>
<span v-if="row.similarity > 0" :class="{ 'similarity-high': row.similarity >= 80 }">
{{ row.similarity }}%
</span>
<span v-else>-</span>
</td>
</tr>
</tbody>
</table>
<!-- 商标筛查进度显示 -->
<div v-if="currentTab === 'trademark' && (trademarkPanelRef?.queryStatus === 'inProgress' || trademarkPanelRef?.queryStatus === 'done' || trademarkPanelRef?.queryStatus === 'error' || trademarkPanelRef?.queryStatus === 'networkError')" class="trademark-progress-container">
<!-- 进行中状态横幅 -->
<div v-if="trademarkPanelRef?.queryStatus === 'inProgress'" class="status-banner progress-banner">
<div class="banner-icon">
<img src="/icon/wait.png" alt="筛查中" class="icon-image" />
</div>
<div v-if="paginatedData.length === 0" class="empty-abs">
<div class="banner-content">
<div class="banner-title">数据筛查中...</div>
<div class="banner-desc">为确保结果准确请耐心等待保持后台运行...</div>
</div>
<div class="banner-actions">
<el-button size="default" @click="handleCancelTask">取消</el-button>
</div>
</div>
<!-- 完成/失败状态横幅样式统一 -->
<div v-if="trademarkPanelRef?.queryStatus === 'done' || trademarkPanelRef?.queryStatus === 'error' || trademarkPanelRef?.queryStatus === 'networkError'" class="status-banner done-banner">
<div class="banner-icon">
<img src="/icon/done1.png" alt="完成" class="icon-image" />
</div>
<div class="banner-content">
<div class="banner-title">筛查已完成</div>
<div class="banner-desc">点击"导出数据"按钮可导出为 Excel 表格文件如出现异常请检查账号地区设置</div>
</div>
<div class="banner-actions">
<el-button size="default" @click="handleNewTask">新建任务</el-button>
<el-button type="primary" size="default">导出数据</el-button>
</div>
</div>
<!-- 统一任务卡片容器 -->
<div class="unified-task-card">
<!-- 三个任务区块 -->
<div class="task-section">
<!-- 左侧状态列 -->
<div class="status-column">
<!-- 任务1产品商标筛查 -->
<div class="status-item">
<img v-if="trademarkPanelRef?.queryStatus === 'error' || trademarkPanelRef?.queryStatus === 'networkError'" src="/icon/error.png" alt="筛查失败" class="status-indicator-icon" />
<img v-else-if="(trademarkPanelRef?.taskProgress?.product?.current || 0) >= (trademarkPanelRef?.taskProgress?.product?.total || 1) && (trademarkPanelRef?.taskProgress?.product?.total || 0) > 0" src="/icon/done.png" alt="采集完成" class="status-indicator-icon" />
<img v-else-if="trademarkPanelRef?.queryStatus === 'inProgress'" src="/icon/inProgress.png" alt="采集中" class="status-indicator-icon" />
<img v-else src="/icon/acquisition.png" alt="待采集" class="status-indicator-icon" />
</div>
<div class="status-connector"></div>
<!-- 任务2品牌商标筛查 -->
<div class="status-item">
<img v-if="(trademarkPanelRef?.taskProgress?.brand?.current || 0) >= (trademarkPanelRef?.taskProgress?.brand?.total || 1)" src="/icon/done.png" alt="采集完成" class="status-indicator-icon" />
<img v-else-if="(trademarkPanelRef?.taskProgress?.brand?.current || 0) > 0" src="/icon/inProgress.png" alt="采集中" class="status-indicator-icon" />
<img v-else src="/icon/acquisition.png" alt="待采集" class="status-indicator-icon" />
</div>
<div class="status-connector"></div>
<!-- 任务3跟卖许可筛查 -->
<div class="status-item">
<img v-if="(trademarkPanelRef?.taskProgress?.platform?.current || 0) >= (trademarkPanelRef?.taskProgress?.platform?.total || 1)" src="/icon/done.png" alt="采集完成" class="status-indicator-icon" />
<img v-else-if="(trademarkPanelRef?.taskProgress?.platform?.current || 0) > 0" src="/icon/inProgress.png" alt="采集中" class="status-indicator-icon" />
<img v-else src="/icon/acquisition.png" alt="待采集" class="status-indicator-icon" />
</div>
</div>
<!-- 任务内容列 -->
<div class="tasks-content">
<div class="task-item">
<div class="task-title-row">
<div class="task-info">
<div class="task-name">{{ trademarkPanelRef?.taskProgress?.product?.label || '未注册/TM商标筛查' }}</div>
<div class="task-desc">{{ trademarkPanelRef?.taskProgress?.product?.desc || '筛查未注册商标或TM标的产品' }}<span v-if="trademarkPanelRef?.queryStatus !== 'inProgress'"> (已完成)</span></div>
</div>
</div>
<div class="task-progress-wrapper">
<div class="task-progress-bar">
<div class="task-progress-fill" :style="{ width: ((trademarkPanelRef?.taskProgress?.product?.total || 0) > 0 ? ((trademarkPanelRef?.taskProgress?.product?.current || 0) / trademarkPanelRef.taskProgress.product.total * 100) : 0) + '%' }"></div>
</div>
<div class="task-percentage">{{ (trademarkPanelRef?.taskProgress?.product?.total || 0) > 0 ? Math.round(((trademarkPanelRef?.taskProgress?.product?.current || 0) / trademarkPanelRef.taskProgress.product.total) * 100) : 0 }}%</div>
</div>
<div class="task-stats">
<div class="task-stat">
<span class="stat-label">查询数量</span>
<span class="stat-value">{{ (trademarkPanelRef?.taskProgress?.product?.total || 0) === 0 ? '-' : trademarkPanelRef.taskProgress.product.total }}</span>
</div>
<div class="task-stat highlight">
<span class="stat-label">未注册/TM标</span>
<span class="stat-value">{{ (trademarkPanelRef?.taskProgress?.product?.total || 0) > 0 ? (trademarkPanelRef?.taskProgress?.product?.completed || 0) : '-' }}</span>
</div>
<div class="task-stat">
<span class="stat-label">已过滤</span>
<span class="stat-value">{{ (trademarkPanelRef?.taskProgress?.product?.total || 0) > 0 ? ((trademarkPanelRef?.taskProgress?.product?.total || 0) - (trademarkPanelRef?.taskProgress?.product?.completed || 0)) : '-' }}</span>
</div>
</div>
</div>
<div class="task-item">
<div class="task-title-row">
<div class="task-info">
<div class="task-name">{{ trademarkPanelRef?.taskProgress?.brand?.label || '品牌商标筛查' }}</div>
<div class="task-desc">{{ trademarkPanelRef?.taskProgress?.brand?.desc || '筛查未注册商标的品牌' }}<span v-if="trademarkPanelRef?.queryStatus !== 'inProgress'"> (已完成)</span></div>
</div>
</div>
<div class="task-progress-wrapper">
<div class="task-progress-bar">
<div class="task-progress-fill" :style="{ width: ((trademarkPanelRef?.taskProgress?.brand?.total || 0) > 0 ? ((trademarkPanelRef?.taskProgress?.brand?.current || 0) / trademarkPanelRef.taskProgress.brand.total * 100) : 0) + '%' }"></div>
</div>
<div class="task-percentage">{{ (trademarkPanelRef?.taskProgress?.brand?.total || 0) > 0 ? Math.round(((trademarkPanelRef?.taskProgress?.brand?.current || 0) / trademarkPanelRef.taskProgress.brand.total) * 100) : 0 }}%</div>
</div>
<div class="task-stats">
<div class="task-stat">
<span class="stat-label">查询数量</span>
<span class="stat-value">{{ (trademarkPanelRef?.taskProgress?.brand?.total || 0) === 0 ? '-' : trademarkPanelRef.taskProgress.brand.total }}</span>
</div>
<div class="task-stat highlight">
<span class="stat-label">未注册</span>
<span class="stat-value">{{ (trademarkPanelRef?.taskProgress?.brand?.total || 0) > 0 ? (trademarkPanelRef?.taskProgress?.brand?.completed || 0) : '-' }}</span>
</div>
<div class="task-stat">
<span class="stat-label">已注册</span>
<span class="stat-value">{{ (trademarkPanelRef?.taskProgress?.brand?.total || 0) > 0 ? ((trademarkPanelRef?.taskProgress?.brand?.total || 0) - (trademarkPanelRef?.taskProgress?.brand?.completed || 0)) : '-' }}</span>
</div>
</div>
</div>
<div class="task-item">
<div class="task-title-row">
<div class="task-info">
<div class="task-name">{{ trademarkPanelRef?.taskProgress?.platform?.label || '亚马逊跟卖许可筛查' }}</div>
<div class="task-desc">{{ trademarkPanelRef?.taskProgress?.platform?.desc || '筛查亚马逊许可跟卖的商品' }}<span v-if="trademarkPanelRef?.queryStatus !== 'inProgress'"> (未开始)</span></div>
</div>
</div>
<div class="task-progress-wrapper">
<div class="task-progress-bar">
<div class="task-progress-fill" :style="{ width: ((trademarkPanelRef?.taskProgress?.platform?.total || 0) > 0 ? ((trademarkPanelRef?.taskProgress?.platform?.current || 0) / trademarkPanelRef.taskProgress.platform.total * 100) : 0) + '%' }"></div>
</div>
<div class="task-percentage">{{ (trademarkPanelRef?.taskProgress?.platform?.total || 0) > 0 ? Math.round(((trademarkPanelRef?.taskProgress?.platform?.current || 0) / trademarkPanelRef.taskProgress.platform.total) * 100) : 0 }}%</div>
</div>
<div class="task-stats">
<div class="task-stat">
<span class="stat-label">查询数量</span>
<span class="stat-value">{{ (trademarkPanelRef?.taskProgress?.platform?.total || 0) === 0 ? '-' : trademarkPanelRef.taskProgress.platform.total }}</span>
</div>
<div class="task-stat highlight">
<span class="stat-label">可跟卖</span>
<span class="stat-value">{{ (trademarkPanelRef?.taskProgress?.platform?.total || 0) > 0 ? (trademarkPanelRef?.taskProgress?.platform?.completed || 0) : '-' }}</span>
</div>
<div class="task-stat">
<span class="stat-label">已过滤</span>
<span class="stat-value">{{ (trademarkPanelRef?.taskProgress?.platform?.total || 0) > 0 ? ((trademarkPanelRef?.taskProgress?.platform?.total || 0) - (trademarkPanelRef?.taskProgress?.platform?.completed || 0)) : '-' }}</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div v-if="paginatedData.length === 0 && !(currentTab === 'trademark' && (trademarkPanelRef?.queryStatus === 'inProgress' || trademarkPanelRef?.queryStatus === 'done' || trademarkPanelRef?.queryStatus === 'error' || trademarkPanelRef?.queryStatus === 'networkError'))" class="empty-abs">
<!-- 商标筛查状态显示 -->
<div v-if="currentTab === 'trademark' && trademarkPanelRef?.queryStatus" class="empty-container">
<div v-if="currentTab === 'trademark' && trademarkPanelRef?.queryStatus && trademarkPanelRef.queryStatus !== 'inProgress' && trademarkPanelRef.queryStatus !== 'done' && trademarkPanelRef.queryStatus !== 'error' && trademarkPanelRef.queryStatus !== 'networkError'" class="empty-container">
<img
:src="trademarkPanelRef.statusConfig[trademarkPanelRef.queryStatus].icon"
:alt="trademarkPanelRef.statusConfig[trademarkPanelRef.queryStatus].title"
:class="['status-image', { 'status-loading': trademarkPanelRef.queryStatus === 'inProgress' }]"
class="status-image"
/>
<div class="empty-title">{{ trademarkPanelRef.statusConfig[trademarkPanelRef.queryStatus].title }}</div>
<div v-if="trademarkPanelRef.statusConfig[trademarkPanelRef.queryStatus].desc" class="empty-desc">
@@ -240,7 +380,7 @@ function handleCurrentChange(page: number) {
</div>
</div>
</div>
<div v-if="currentTab !== 'genmai'" class="pagination-fixed">
<div v-if="currentTab !== 'genmai' && currentTab !== 'trademark'" class="pagination-fixed">
<el-pagination
:current-page="currentPage"
:page-sizes="[15,30,50,100]"
@@ -618,6 +758,253 @@ function handleCurrentChange(page: number) {
text-decoration: underline;
}
/* 商标筛查进度样式 */
.trademark-progress-container {
padding: 24px;
width: 100%;
height: 100%;
box-sizing: border-box;
overflow-y: auto;
display: flex;
flex-direction: column;
align-items: center;
}
/* 状态横幅 */
.status-banner {
box-sizing: border-box;
display: flex;
flex-direction: row;
align-items: center;
padding: 16px;
gap: 16px;
width: 70%;
max-width: 900px;
min-height: 90px;
background: #FFFFFF;
border: 1px solid rgba(0, 0, 0, 0.1);
border-radius: 16px;
flex: none;
margin-bottom: 16px;
}
.status-banner.progress-banner {
box-shadow: 0px 8px 20px rgba(22, 119, 255, 0.2);
}
.status-banner.done-banner {
box-shadow: 0px 8px 20px rgba(103, 194, 58, 0.2);
}
.status-banner.error-banner {
box-shadow: 0px 8px 20px rgba(245, 108, 108, 0.2);
}
.banner-icon {
flex-shrink: 0;
width: 64px;
height: 64px;
display: flex;
align-items: center;
justify-content: center;
background: transparent;
border-radius: 50%;
}
.done-banner .banner-icon {
background: transparent;
}
.icon-image {
width: 48px;
height: 48px;
filter: none;
}
.banner-content {
flex: 1;
min-width: 0;
text-align: left;
}
.banner-title {
font-size: 16px;
font-weight: 600;
color: #303133;
margin-bottom: 4px;
}
.banner-desc {
font-size: 13px;
color: #606266;
line-height: 1.5;
}
.banner-actions {
flex-shrink: 0;
display: flex;
gap: 12px;
}
.banner-actions .el-button {
min-width: 100px;
}
/* 统一任务卡片容器 */
.unified-task-card {
width: 70%;
max-width: 900px;
background: #FFFFFF;
border: 1px solid rgba(0, 0, 0, 0.06);
border-radius: 12px;
box-shadow: 0px 4px 12px rgba(0, 0, 0, 0.08);
overflow: hidden;
}
/* 任务区块 */
.task-section {
display: flex;
flex-direction: row;
padding: 20px;
gap: 20px;
}
/* 左侧状态列 */
.status-column {
display: flex;
flex-direction: column;
align-items: center;
gap: 0;
flex-shrink: 0;
}
.status-item {
display: flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
flex-shrink: 0;
}
.status-indicator-icon {
width: 24px;
height: 24px;
object-fit: contain;
}
.status-indicator-icon[src*="inProgress"] {
animation: spin 1.5s linear infinite;
}
.status-connector {
width: 2px;
flex: 1;
background: #e5e7eb;
margin: 6px 0;
min-height: 50px;
}
/* 任务内容列 */
.tasks-content {
flex: 1;
display: flex;
flex-direction: column;
gap: 16px;
min-width: 0;
}
.task-item {
width: 100%;
padding: 0;
padding-bottom: 16px;
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
}
.task-item:first-child {
padding-top: 0;
}
.task-item:last-child {
padding-bottom: 0;
border-bottom: none;
}
/* 竖线分隔(换行后不需要) */
.vertical-divider {
display: none;
}
/* 任务标题行 */
.task-title-row {
display: flex;
align-items: flex-start;
margin-bottom: 12px;
}
.task-info {
flex: 1;
min-width: 0;
text-align: left;
}
.task-name {
font-size: 13px;
font-weight: 600;
color: #303133;
margin-bottom: 2px;
line-height: 1.4;
}
.task-desc {
font-size: 11px;
color: #909399;
line-height: 1.4;
}
/* 进度条 */
.task-progress-wrapper {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 10px;
}
.task-progress-bar {
flex: 1;
height: 4px;
background: rgba(22, 119, 255, 0.1);
border-radius: 999px;
overflow: hidden;
}
.task-progress-fill {
height: 100%;
background: linear-gradient(90deg, #1677FF 0%, #4096FF 100%);
border-radius: 999px;
transition: width 0.3s ease;
}
.task-percentage {
flex-shrink: 0;
font-size: 13px;
font-weight: 600;
color: #1677FF;
min-width: 40px;
text-align: right;
}
/* 统计数据 */
.task-stats {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
text-align: left;
}
.task-stat {
flex: 1;
display: flex;
flex-direction: column;
gap: 2px;
}
.task-stat.highlight {
border-left: 1px solid rgba(0, 0, 0, 0.06);
border-right: 1px solid rgba(0, 0, 0, 0.06);
padding: 0 8px;
}
.stat-label {
font-size: 10px;
color: #909399;
}
.stat-value {
font-size: 16px;
font-weight: 600;
color: #303133;
}
.task-stat.highlight .stat-label {
color: #67c23a;
}
.task-stat.highlight .stat-value {
color: #67c23a;
}
/* 响应式设计 */
@media (max-width: 1200px) {
.steps-sidebar.narrow {
@@ -640,6 +1027,10 @@ function handleCurrentChange(page: number) {
.genmai-info-boxes {
width: 95%;
}
.status-banner,
.unified-task-card {
width: 85%;
}
}
@media (max-width: 768px) {
@@ -655,6 +1046,35 @@ function handleCurrentChange(page: number) {
width: 100px;
font-size: 13px;
}
.status-banner,
.unified-task-card {
width: 95%;
}
.status-banner {
min-height: auto;
padding: 14px;
}
.banner-icon {
width: 40px;
height: 40px;
}
.icon-image {
width: 24px;
height: 24px;
}
.banner-title {
font-size: 14px;
}
.banner-desc {
font-size: 12px;
}
.banner-actions {
flex-direction: column;
width: 100%;
}
.banner-actions .el-button {
width: 100%;
}
}
</style>

View File

@@ -3,6 +3,7 @@ import { ref, inject, defineAsyncComponent } from 'vue'
import { ElMessage } from 'element-plus'
import { handlePlatformFileExport } from '../../utils/settings'
import { getUsernameFromToken } from '../../utils/token'
import { markApi } from '../../api/mark'
const TrialExpiredDialog = defineAsyncComponent(() => import('../common/TrialExpiredDialog.vue'))
const refreshVipStatus = inject<() => Promise<boolean>>('refreshVipStatus')
@@ -17,12 +18,21 @@ const emit = defineEmits<{
const trademarkData = ref<any[]>([])
const trademarkFileName = ref('')
const trademarkFile = ref<File | null>(null)
const trademarkUpload = ref<HTMLInputElement | null>(null)
const trademarkLoading = ref(false)
const trademarkProgress = ref(0)
const exportLoading = ref(false)
const currentStep = ref(0)
const totalSteps = ref(0)
let brandProgressTimer: any = null
// 三个任务的进度数据
const taskProgress = ref({
product: { total: 0, current: 0, completed: 0, label: '产品商标筛查', desc: '筛查未注册商标或TM标的商品' },
brand: { total: 0, current: 0, completed: 0, label: '品牌商标筛查', desc: '筛查未注册商标的品牌' },
platform: { total: 0, current: 0, completed: 0, label: '跟卖许可筛查', desc: '筛查亚马逊许可跟卖的商品' }
})
// 查询状态: idle-待开始, inProgress-进行中, done-已完成, error-错误, networkError-网络错误, cancel-已取消
const queryStatus = ref<'idle' | 'inProgress' | 'done' | 'error' | 'networkError' | 'cancel'>('idle')
@@ -39,14 +49,14 @@ const statusConfig = {
desc: '请耐心等待,正在为您筛查商标信息'
},
done: {
icon: '/icon/done.png',
icon: '/icon/done1.png',
title: '筛查完成',
desc: '已成功完成所有商标筛查任务'
},
error: {
icon: '/icon/error.png',
title: '查失败',
desc: '查过程中出现错误,请重试'
title: '查失败',
desc: '查过程中出现错误,请重新尝试'
},
networkError: {
icon: '/icon/networkErrors.png',
@@ -61,19 +71,15 @@ const statusConfig = {
}
// 选择的账号(模拟数据)
const selectedAccount = ref('chensri3.com')
const selectedAccount = ref(1)
const accountOptions = [
{ label: 'chensri3.com', value: 'chensri3.com', status: '美国' },
{ label: 'zhangikcpi.com', value: 'zhangikcpi.com', status: '加拿大' },
{ label: 'hhhhsoutlook...', value: 'hhhhsoutlook', status: '日本' }
{ id: 1, name: 'chensri3.com', username: 'chensri3.com', status: 1 }
]
// 地区选择
const region = ref('美国')
const regionOptions = [
{ label: '美国', value: '美国', flag: '🇺🇸' },
{ label: '日本', value: '日本', flag: '🇯🇵' },
{ label: '加拿大', value: '加拿大', flag: '🇨🇦' }
{ label: '美国', value: '美国', flag: '🇺🇸' }
]
// 查询类型多选
@@ -103,6 +109,7 @@ async function handleTrademarkUpload(e: Event) {
}
trademarkFileName.value = file.name
trademarkFile.value = file
queryStatus.value = 'idle' // 重置状态
trademarkData.value = []
emit('updateData', [])
@@ -111,7 +118,7 @@ async function handleTrademarkUpload(e: Event) {
}
async function startTrademarkQuery() {
if (!trademarkFileName.value) {
if (!trademarkFileName.value || !trademarkFile.value) {
showMessage('请先导入商标列表', 'warning')
return
}
@@ -129,30 +136,231 @@ async function startTrademarkQuery() {
trademarkData.value = []
queryStatus.value = 'inProgress'
const mockData = [
{ name: 'SONY', status: '已注册', class: '9类-电子产品', owner: 'Sony Corporation', expireDate: '2028-12-31', similarity: 0 },
{ name: 'SUMSUNG', status: '近似风险', class: '9类-电子产品', owner: '待查询', expireDate: '-', similarity: 85 },
{ name: 'NIKE', status: '已注册', class: '25类-服装鞋帽', owner: 'Nike, Inc.', expireDate: '2029-06-15', similarity: 0 },
{ name: 'ADIBAS', status: '近似风险', class: '25类-服装鞋帽', owner: '待查询', expireDate: '-', similarity: 92 },
{ name: 'MyBrand', status: '可注册', class: '未查询', owner: '-', expireDate: '-', similarity: 0 }
]
// 完全重置任务进度包括total
taskProgress.value.product.total = 0
taskProgress.value.product.current = 0
taskProgress.value.product.completed = 0
taskProgress.value.brand.total = 0
taskProgress.value.brand.current = 0
taskProgress.value.brand.completed = 0
taskProgress.value.platform.total = 0
taskProgress.value.platform.current = 0
taskProgress.value.platform.completed = 0
totalSteps.value = mockData.length
// 通知父组件更新数据
emit('updateData', [])
try {
for (let i = 0; i < mockData.length; i++) {
if (!trademarkLoading.value) break // 被取消
let productResult: any = null
let brandList: string[] = []
currentStep.value = i + 1
await new Promise(resolve => setTimeout(resolve, 800))
trademarkData.value.push(mockData[i])
trademarkProgress.value = Math.round(((i + 1) / mockData.length) * 100)
// 判断是否需要执行产品商标筛查
const needProductCheck = queryTypes.value.includes('product')
const needBrandCheck = queryTypes.value.includes('brand')
if (needProductCheck) {
// 步骤1: 产品商标筛查 - 调用新建任务接口
showMessage('正在上传文件...', 'info')
const createResult = await markApi.newTask(trademarkFile.value)
if (createResult.code !== 200 && createResult.code !== 0) {
throw new Error(createResult.msg || '创建任务失败')
}
showMessage('文件上传成功,正在处理...', 'success')
// 步骤2: 轮询检查任务状态最多等待60秒
const taskData = taskProgress.value.product
const maxWaitTime = 60000 // 最多等60秒
const pollInterval = 3000 // 每3秒检查一次
const startTime = Date.now()
let taskResult: any = null
while (Date.now() - startTime < maxWaitTime) {
if (!trademarkLoading.value) return
// 等待一段时间
await new Promise(resolve => setTimeout(resolve, pollInterval))
if (!trademarkLoading.value) return
// 尝试获取任务结果
try {
taskResult = await markApi.getTask()
if (taskResult.code === 200 || taskResult.code === 0) {
// 检查是否有 download_url有则说明任务完成
if (taskResult.data.original?.download_url) {
break
}
}
} catch (err) {
// 继续等待
console.log('等待任务处理中...', err)
}
}
if (!trademarkLoading.value) return
// 步骤3: 检查是否成功获取到结果
if (!taskResult || (taskResult.code !== 200 && taskResult.code !== 0)) {
throw new Error('获取任务超时或失败,请重试')
}
if (!taskResult.data.original?.download_url) {
throw new Error('任务处理超时,请稍后重试')
}
productResult = taskResult
// 从后端获取真实的统计数据
taskData.total = taskResult.data.original?.total || 0
taskData.completed = taskResult.data.filtered.length
taskData.current = taskData.total
// 映射后端数据到前端格式
trademarkData.value = taskResult.data.filtered.map((item: any) => ({
name: item['品牌'] || '',
status: item['商标类型'] || '',
class: '',
owner: '',
expireDate: item['注册时间'] || '',
similarity: 0,
// 保留原始数据供后续使用
asin: item['ASIN'],
productImage: item['商品主图']
}))
// 如果需要品牌筛查,从产品结果中提取品牌列表
if (needBrandCheck) {
brandList = taskResult.data.filtered
.map((item: any) => item['品牌'])
.filter((brand: string) => brand && brand.trim())
}
showMessage(`产品筛查完成,共查询 ${taskData.total} 条,筛查出 ${taskData.completed} 条未注册/TM标`, 'success')
}
// 品牌商标筛查
if (needBrandCheck) {
if (!trademarkLoading.value) return
// 如果没有执行产品筛查,需要先上传文件获取品牌列表
if (!needProductCheck) {
showMessage('正在上传文件提取品牌列表...', 'info')
// 调用新建任务接口获取处理后的数据
const createResult = await markApi.newTask(trademarkFile.value)
if (createResult.code !== 200 && createResult.code !== 0) {
throw new Error(createResult.msg || '创建任务失败')
}
// 轮询获取任务结果
const maxWaitTime = 60000
const pollInterval = 3000
const startTime = Date.now()
let taskResult: any = null
while (Date.now() - startTime < maxWaitTime) {
if (!trademarkLoading.value) return
await new Promise(resolve => setTimeout(resolve, pollInterval))
if (!trademarkLoading.value) return
try {
taskResult = await markApi.getTask()
if (taskResult.code === 200 || taskResult.code === 0) {
if (taskResult.data.original?.download_url) {
break
}
}
} catch (err) {
console.log('等待任务处理中...', err)
}
}
if (!trademarkLoading.value) return
if (!taskResult || (taskResult.code !== 200 && taskResult.code !== 0)) {
throw new Error('获取任务超时或失败,请重试')
}
if (!taskResult.data.original?.download_url) {
throw new Error('任务处理超时,请稍后重试')
}
// 从结果中提取品牌列表包括TM和未注册的品牌
brandList = taskResult.data.filtered
.map((item: any) => item['品牌'])
.filter((brand: string) => brand && brand.trim())
showMessage(`品牌列表提取成功,共 ${brandList.length} 个品牌`, 'success')
}
if (brandList.length === 0) {
showMessage('没有品牌需要筛查', 'warning')
} else {
const brandData = taskProgress.value.brand
brandData.total = brandList.length
brandData.current = 0
brandData.completed = 0
showMessage(`开始品牌商标筛查,共 ${brandList.length} 个品牌...`, 'info')
// 模拟进度动画每秒增加20个
const batchSize = 20
brandProgressTimer = setInterval(() => {
if (brandData.current < brandList.length * 0.95) {
brandData.current = Math.min(brandData.current + batchSize, brandList.length * 0.95)
}
}, 1000)
// 调用品牌筛查接口(浏览器内并发,速度快)
const brandResult = await markApi.brandCheck(brandList)
if (brandProgressTimer) clearInterval(brandProgressTimer)
if (!trademarkLoading.value) return
if (brandResult.code === 200 || brandResult.code === 0) {
// 完成显示100%
brandData.current = brandData.total
brandData.completed = brandResult.data.filtered
// 将品牌筛查结果追加到展示数据中
const brandItems = brandResult.data.data.map((item: any) => ({
name: item.brand || '',
status: item.status || '未注册',
class: '',
owner: '',
expireDate: '',
similarity: 0,
isBrand: true // 标记为品牌数据
}))
trademarkData.value = [...trademarkData.value, ...brandItems]
showMessage(`品牌筛查完成,共查询 ${brandData.total} 个品牌,筛查出 ${brandData.completed} 个未注册品牌`, 'success')
} else {
throw new Error(brandResult.msg || '品牌筛查失败')
}
}
}
if (trademarkLoading.value) {
queryStatus.value = 'done'
emit('updateData', trademarkData.value)
showMessage('商标筛查完成', 'success')
let summaryMsg = '筛查完成'
if (needProductCheck) {
const taskData = taskProgress.value.product
summaryMsg += `,产品:${taskData.completed}/${taskData.total}`
}
if (needBrandCheck && brandList.length > 0) {
const brandData = taskProgress.value.brand
summaryMsg += `,品牌:${brandData.completed}/${brandData.total}`
}
showMessage(summaryMsg, 'success')
}
} catch (error: any) {
if (error.message && error.message.includes('网络')) {
@@ -163,7 +371,16 @@ async function startTrademarkQuery() {
errorMessage.value = error.message || '查询失败'
}
showMessage(errorMessage.value, 'error')
// 失败时清空数据,不显示侧边栏
trademarkData.value = []
emit('updateData', [])
} finally {
// 清除定时器
if (brandProgressTimer) {
clearInterval(brandProgressTimer)
brandProgressTimer = null
}
trademarkLoading.value = false
currentStep.value = 0
totalSteps.value = 0
@@ -171,6 +388,11 @@ async function startTrademarkQuery() {
}
function stopTrademarkQuery() {
// 清除进度动画定时器
if (brandProgressTimer) {
clearInterval(brandProgressTimer)
brandProgressTimer = null
}
trademarkLoading.value = false
queryStatus.value = 'cancel'
currentStep.value = 0
@@ -239,12 +461,28 @@ function downloadTrademarkTemplate() {
URL.revokeObjectURL(url)
}
function resetToIdle() {
queryStatus.value = 'idle'
trademarkData.value = []
trademarkFileName.value = ''
trademarkFile.value = null
taskProgress.value.product.current = 0
taskProgress.value.product.completed = 0
taskProgress.value.brand.current = 0
taskProgress.value.brand.completed = 0
taskProgress.value.platform.current = 0
taskProgress.value.platform.completed = 0
}
defineExpose({
trademarkLoading,
trademarkProgress,
trademarkData,
queryStatus,
statusConfig
statusConfig,
taskProgress,
resetToIdle,
stopTrademarkQuery
})
</script>
@@ -278,19 +516,26 @@ defineExpose({
<div class="step-header"><div class="title">选择亚马逊商家账号</div></div>
<div class="desc">请确保账号地区与专利地区一致不一致可能导致结果不准确或失败</div>
<el-select v-model="selectedAccount" placeholder="选择账号" size="small" style="width: 100%">
<el-option v-for="opt in accountOptions" :key="opt.value" :label="opt.label" :value="opt.value">
<div class="account-option">
<span>{{ opt.label }}</span>
<span class="account-status">{{ opt.status }}</span>
<span class="account-check"></span>
<el-scrollbar :class="['account-list', { 'scroll-limit': accountOptions.length > 3 }]">
<div>
<div
v-for="acc in accountOptions"
:key="acc.id"
:class="['acct-item', 'disabled', { selected: selectedAccount === acc.id }]"
>
<span class="acct-row">
<span :class="['status-dot', acc.status === 1 ? 'on' : 'off']"></span>
<img class="avatar" src="/image/user.png" alt="avatar" />
<span class="acct-text">{{ acc.name || acc.username }}</span>
<span v-if="selectedAccount === acc.id" class="acct-check"></span>
</span>
</div>
</el-option>
</el-select>
</div>
</el-scrollbar>
<div class="step-actions btn-row">
<el-button size="small" class="w50">添加账号</el-button>
<el-button size="small" class="w50 btn-blue">账号管理</el-button>
<el-button size="small" class="w50" disabled>添加账号</el-button>
<el-button size="small" class="w50 btn-blue" disabled>账号管理</el-button>
</div>
</div>
</div>
@@ -301,7 +546,7 @@ defineExpose({
<div class="step-card">
<div class="step-header"><div class="title">选择产品品牌地区</div></div>
<div class="desc">暂时仅支持美国品牌商标筛查后续将开放更多地区敬请期待</div>
<el-select v-model="region" placeholder="选择地区" size="small" style="width: 100%">
<el-select v-model="region" placeholder="选择地区" size="small" style="width: 100%" disabled>
<el-option v-for="opt in regionOptions" :key="opt.value" :label="opt.label" :value="opt.value">
<span style="margin-right:6px">{{ opt.flag }}</span>{{ opt.label }}
</el-option>
@@ -337,7 +582,7 @@ defineExpose({
</div>
</div>
<div :class="['query-card', { active: queryTypes.includes('platform') }]" @click="toggleQueryType('platform')">
<div :class="['query-card', 'query-disabled', { active: queryTypes.includes('platform') }]">
<div class="query-check">
<div class="check-icon" v-if="queryTypes.includes('platform')"></div>
</div>
@@ -355,9 +600,12 @@ defineExpose({
<div class="bottom-action">
<div class="action-header">
<span class="step-indicator">{{ trademarkLoading ? currentStep : 1 }}/4</span>
<el-button class="start-btn" type="primary" :disabled="!trademarkFileName || trademarkLoading" :loading="trademarkLoading" @click="startTrademarkQuery">
<el-button v-if="!trademarkLoading" class="start-btn" type="primary" :disabled="!trademarkFileName || queryTypes.length === 0" @click="startTrademarkQuery">
开始筛查
</el-button>
<el-button v-else class="start-btn stop-btn" @click="stopTrademarkQuery">
停止筛查
</el-button>
</div>
</div>
@@ -391,7 +639,7 @@ defineExpose({
.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; }
.links { display: flex; align-items: center; gap: 6px; margin-bottom: 8px; }
.link { color: #409EFF; cursor: pointer; font-size: 12px; }
.link { color: #909399; cursor: pointer; font-size: 12px; }
.file-chip {
display: flex;
@@ -403,18 +651,21 @@ defineExpose({
font-size: 12px;
color: #606266;
margin-top: 6px;
min-width: 0;
}
.file-chip .dot {
width: 6px;
height: 6px;
background: #409EFF;
border-radius: 50%;
display: inline-block;
flex-shrink: 0;
}
.file-chip .name {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
flex: 1;
min-width: 0;
}
.dropzone {
@@ -459,6 +710,11 @@ defineExpose({
border-color: #1677FF;
background: #f0f9ff;
}
.query-card.query-disabled {
cursor: not-allowed;
opacity: 0.5;
pointer-events: none;
}
.query-check {
width: 16px;
height: 16px;
@@ -468,15 +724,15 @@ defineExpose({
align-items: center;
justify-content: center;
flex-shrink: 0;
background: #ffffff;
background: transparent;
transition: all 0.2s ease;
}
.query-card.active .query-check {
border-color: #1677FF;
background: #1677FF;
background: transparent;
}
.check-icon {
color: #ffffff;
color: #1677FF;
font-size: 10px;
font-weight: bold;
line-height: 1;
@@ -534,32 +790,90 @@ defineExpose({
}
.start-btn {
flex: 1;
height: 38px;
height: 32px;
padding: 0px 16px;
background: #1677FF;
border-color: #1677FF;
font-size: 14px;
font-weight: 500;
border-radius: 6px;
}
.stop-btn {
background: #f56c6c;
border-color: #f56c6c;
}
.stop-btn:hover {
background: #f78989;
border-color: #f78989;
}
.btn-blue { background: #1677FF; border-color: #1677FF; color: #fff; }
.btn-blue:disabled { background: #a6c8ff; border-color: #a6c8ff; color: #fff; }
.account-option {
display: flex;
.account-list {
height: auto;
background: #fff;
border: 1px solid #ebeef5;
border-radius: 4px;
margin-bottom: 8px;
padding: 4px;
}
.scroll-limit { max-height: 140px; }
.avatar { width: 18px; height: 18px; border-radius: 50%; }
.acct-row {
display: grid;
grid-template-columns: 6px 18px 1fr auto;
align-items: center;
gap: 8px;
gap: 6px;
width: 100%;
}
.account-status {
margin-left: auto;
.acct-text {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
min-width: 0;
font-size: 12px;
color: #909399;
color: #303133;
}
.account-check {
color: #1677FF;
font-size: 14px;
.status-dot {
width: 6px;
height: 6px;
border-radius: 50%;
display: inline-block;
}
.status-dot.on { background: #22c55e; }
.status-dot.off { background: #f87171; }
.acct-item {
padding: 6px 8px;
border-radius: 6px;
margin-bottom: 4px;
}
.acct-item:last-child {
margin-bottom: 0;
}
.acct-item.selected {
background: #eef5ff;
box-shadow: inset 0 0 0 1px #d6e4ff;
}
.acct-item.disabled {
cursor: not-allowed;
opacity: 0.6;
}
.acct-check {
display: inline-flex;
align-items: center;
justify-content: center;
width: 16px;
height: 16px;
border-radius: 50%;
background: transparent;
color: #111;
font-size: 12px;
}
.account-list :deep(.el-scrollbar__view) {
padding: 0;
}
.account-list::-webkit-scrollbar { width: 0; height: 0; }
.trademark-panel :deep(.el-select),
.trademark-panel :deep(.el-input__wrapper) {

Binary file not shown.

After

Width:  |  Height:  |  Size: 533 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 894 B

View File

@@ -122,6 +122,12 @@
<artifactId>oshi-core</artifactId>
<version>6.4.6</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.36</version>
<scope>compile</scope>
</dependency>
</dependencies>

View File

@@ -11,7 +11,7 @@ import org.springframework.core.annotation.Order;
/**
* ChromeDriver 配置类
* 启动时后台预加载驱动,提供全局单例 Bean
* 已禁用预加载,由 TrademarkCheckUtil 按需创建
*/
@Slf4j
@Configuration
@@ -22,21 +22,26 @@ public class ChromeDriverPreloader implements ApplicationRunner {
@Override
public void run(ApplicationArguments args) {
new Thread(() -> {
globalDriver = SeleniumUtil.createDriver(true);
log.info("ChromeDriver 预加载完成");
}, "ChromeDriver-Preloader").start();
// 不再预加载,节省资源
log.info("ChromeDriver 配置已加载(按需启动)");
}
@Bean
public ChromeDriver chromeDriver() {
if (globalDriver == null) globalDriver = SeleniumUtil.createDriver(true);
// 为兼容性保留 Bean但不自动创建
if (globalDriver == null) globalDriver = SeleniumUtil.createDriver(false);
return globalDriver;
}
@PreDestroy
public void cleanup() {
if (globalDriver != null) {
try {
globalDriver.quit();
} catch (Exception e) {
log.error("关闭ChromeDriver失败", e);
}
}
}
}

View File

@@ -0,0 +1,90 @@
package com.tashow.erp.controller;
import com.tashow.erp.utils.JsonData;
import com.tashow.erp.utils.LoggerUtil;
import com.tashow.erp.utils.TrademarkCheckUtil;
import org.slf4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.*;
import java.util.concurrent.ThreadLocalRandom;
import java.util.stream.Collectors;
/**
* 商标检查控制器 - 极速版(浏览器内并发)
*/
@RestController
@RequestMapping("/api/trademark")
public class TrademarkController {
private static final Logger logger = LoggerUtil.getLogger(TrademarkController.class);
@Autowired
private TrademarkCheckUtil util;
/**
* 批量品牌商标筛查(浏览器内并发,极速版)
*/
@PostMapping("/brandCheck")
public JsonData brandCheck(@RequestBody List<String> brands) {
try {
List<String> list = brands.stream()
.filter(b -> b != null && !b.trim().isEmpty())
.map(String::trim)
.distinct()
.collect(Collectors.toList());
logger.info("开始检查 {}个品牌", list.size());
long start = System.currentTimeMillis();
// 串行查询(不加延迟)
List<Map<String, Object>> unregistered = new ArrayList<>();
int checkedCount = 0;
int registeredCount = 0;
for (int i = 0; i < list.size(); i++) {
String brand = list.get(i);
logger.info("处理第 {} 个: {}", i + 1, brand);
Map<String, Boolean> results = util.batchCheck(Collections.singletonList(brand));
results.forEach((b, isReg) -> {
if (!isReg) {
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;
int failedCount = list.size() - checkedCount;
Map<String, Object> res = new HashMap<>();
res.put("total", list.size());
res.put("checked", checkedCount);
res.put("registered", registeredCount);
res.put("unregistered", unregistered.size());
res.put("failed", failedCount);
res.put("data", unregistered);
res.put("duration", t + "");
logger.info("完成: 共{}个,成功查询{}个(已注册{}个,未注册{}个),查询失败{}个,耗时{}秒",
list.size(), checkedCount, registeredCount, unregistered.size(), failedCount, t);
return JsonData.buildSuccess(res);
} catch (Exception e) {
logger.error("筛查失败", e);
return JsonData.buildError("筛查失败: " + e.getMessage());
} finally {
// 采集完成或失败后关闭浏览器
util.closeDriver();
}
}
}

View File

@@ -0,0 +1,160 @@
package com.tashow.erp.test;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.http.*;
import org.springframework.web.client.RestTemplate;
/**
* USPTO API 完整测试
* 测试目的:验证是否可以通过品牌名称搜索商标
*/
public class UsptoApiTest {
private static final String API_KEY = "TGP31sjvuxOb5bV21iYIihDpnXI4mFlM";
private static final RestTemplate rest = new RestTemplate();
private static final ObjectMapper mapper = new ObjectMapper();
public static void main(String[] args) {
System.out.println("=== USPTO API 功能测试 ===\n");
System.out.println("API Key: " + API_KEY + "\n");
// 测试1通过序列号查询已知可用
testBySerialNumber();
System.out.println("\n" + "=".repeat(60) + "\n");
// 测试2尝试通过品牌名称搜索
testByBrandName();
System.out.println("\n" + "=".repeat(60) + "\n");
// 测试3对比当前实现的tmsearch
testCurrentImplementation();
}
/**
* 测试1通过序列号查询官方TSDR API
*/
private static void testBySerialNumber() {
System.out.println("【测试1】通过序列号查询");
System.out.println("端点: https://tsdrapi.uspto.gov/ts/cd/casestatus/sn88123456/info.json");
try {
HttpHeaders headers = new HttpHeaders();
headers.set("USPTO-API-KEY", API_KEY);
HttpEntity<String> entity = new HttpEntity<>(headers);
ResponseEntity<String> response = rest.exchange(
"https://tsdrapi.uspto.gov/ts/cd/casestatus/sn88123456/info.json",
HttpMethod.GET, entity, String.class
);
JsonNode json = mapper.readTree(response.getBody());
String markElement = json.get("trademarks").get(0).get("status").get("markElement").asText();
int status = json.get("trademarks").get(0).get("status").get("status").asInt();
String regNumber = json.get("trademarks").get(0).get("status").get("usRegistrationNumber").asText();
System.out.println("✓ 成功!");
System.out.println(" 商标名称: " + markElement);
System.out.println(" 状态码: " + status);
System.out.println(" 注册号: " + (regNumber.isEmpty() ? "未注册" : regNumber));
System.out.println("\n结论: ✅ 可以查询,但必须知道序列号");
} catch (Exception e) {
System.out.println("✗ 失败: " + e.getMessage());
}
}
/**
* 测试2尝试通过品牌名称搜索
*/
private static void testByBrandName() {
System.out.println("【测试2】尝试通过品牌名称搜索");
String[] brands = {"Nike", "MYLIFE", "TestBrand123"};
String[] searchUrls = {
"https://tsdrapi.uspto.gov/ts/cd/search?q=%s",
"https://tsdrapi.uspto.gov/search?keyword=%s",
"https://api.uspto.gov/trademark/search?q=%s",
"https://api.uspto.gov/tmsearch/search?q=%s",
};
boolean foundSearchApi = false;
for (String brand : brands) {
System.out.println("\n测试品牌: " + brand);
for (String urlTemplate : searchUrls) {
String url = String.format(urlTemplate, brand);
System.out.println(" 尝试: " + url);
try {
HttpHeaders headers = new HttpHeaders();
headers.set("USPTO-API-KEY", API_KEY);
HttpEntity<String> entity = new HttpEntity<>(headers);
ResponseEntity<String> response = rest.exchange(url, HttpMethod.GET, entity, String.class);
System.out.println(" ✓✓✓ 成功! 找到品牌搜索API!");
System.out.println(" 响应: " + response.getBody().substring(0, Math.min(200, response.getBody().length())));
foundSearchApi = true;
break;
} catch (Exception e) {
System.out.println(" ✗ 404/失败");
}
}
if (foundSearchApi) break;
}
if (!foundSearchApi) {
System.out.println("\n结论: ❌ USPTO官方API不支持品牌名称搜索");
}
}
/**
* 测试3当前实现的tmsearch方式
*/
private static void testCurrentImplementation() {
System.out.println("【测试3】当前实现方式tmsearch内部API");
System.out.println("端点: https://tmsearch.uspto.gov/prod-stage-v1-0-0/tmsearch");
String brand = "Nike";
String requestBody = String.format(
"{\"query\":{\"bool\":{\"must\":[{\"bool\":{\"should\":[" +
"{\"match_phrase\":{\"WM\":{\"query\":\"%s\",\"boost\":5}}}," +
"{\"match\":{\"WM\":{\"query\":\"%s\",\"boost\":2}}}," +
"{\"match_phrase\":{\"PM\":{\"query\":\"%s\",\"boost\":2}}}" +
"]}}]}},\"size\":1,\"_source\":[\"alive\"]}",
brand, brand, brand
);
try {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<String> entity = new HttpEntity<>(requestBody, headers);
ResponseEntity<String> response = rest.postForEntity(
"https://tmsearch.uspto.gov/prod-stage-v1-0-0/tmsearch",
entity, String.class
);
JsonNode json = mapper.readTree(response.getBody());
boolean hasResults = json.get("hits").get("hits").size() > 0;
System.out.println("✓ 成功!");
System.out.println(" 品牌: " + brand);
System.out.println(" 找到结果: " + (hasResults ? "" : ""));
if (hasResults) {
boolean alive = json.get("hits").get("hits").get(0).get("_source").get("alive").asBoolean();
System.out.println(" 是否注册: " + (alive ? "已注册" : "未注册"));
}
System.out.println("\n结论: ✅ 支持品牌名称直接搜索(无需序列号)");
System.out.println(" ⚠️ 但这是内部API非官方公开接口");
} catch (Exception e) {
System.out.println("✗ 失败: " + e.getMessage());
}
}
}

View File

@@ -1,15 +1,53 @@
package com.tashow.erp.test;
import com.tashow.erp.utils.DeviceUtils;
import com.tashow.erp.utils.TrademarkCheckUtil;
import org.openqa.selenium.JavascriptExecutor;
import org.openqa.selenium.chrome.ChromeDriver;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
@RestController
public class aa {
@Autowired
private ChromeDriver driver;
@Autowired
RestTemplate restTemplate;
@Autowired
TrademarkCheckUtil trademarkCheckUtil;
@GetMapping("/aa")
public String aa() {
DeviceUtils deviceUtils = new DeviceUtils();
return deviceUtils.generateDeviceId();
public boolean aa() {
return checkTrademark("HEIBAGO");
}
public boolean checkTrademark(String brandName) {
try {
driver.get("https://tmsearch.uspto.gov/search/search-results");
Thread.sleep(2000);
String script = String.format("""
return fetch('https://tmsearch.uspto.gov/prod-stage-v1-0-0/tmsearch', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
query: {bool: {must: [{bool: {should: [
{match_phrase: {WM: {query: '%s', boost: 5}}},
{match: {WM: {query: '%s', boost: 2}}},
{match_phrase: {PM: {query: '%s', boost: 2}}}
]}}]}},
size: 1, _source: ['alive']
})
}).then(r => r.json()).then(d => d?.hits?.hits?.[0]?.source?.alive || false);
""", brandName, brandName, brandName);
Object result = ((JavascriptExecutor) driver).executeAsyncScript("var callback = arguments[arguments.length - 1];" + script.replace("return", "").replace(";", ".then(callback);"));
return Boolean.TRUE.equals(result);
} catch (Exception e) {
System.err.println("检测失败: " + e.getMessage());
return false;
}
}
}

View File

@@ -0,0 +1,32 @@
package com.tashow.erp.utils;
import cn.hutool.http.HttpUtil;
import org.springframework.stereotype.Component;
/**
* 代理IP池
*/
@Component
public class ProxyPool {
private static final String API_URL = "http://api.tianqiip.com/getip?secret=h6x09x0eenxuf4s7&num=1&type=txt&port=2&time=3&mr=1&sign=620719f6b7d66744b0216a4f61a6bcee";
/**
* 获取一个代理IP
* @return 代理地址格式host:port如 123.96.236.32:40016
*/
public String getProxy() {
try {
String response = HttpUtil.get(API_URL);
if (response != null && !response.trim().isEmpty()) {
String proxy = response.trim();
System.out.println("获取到代理: " + proxy);
return proxy;
}
} catch (Exception e) {
System.err.println("获取代理失败: " + e.getMessage());
}
return null;
}
}

View File

@@ -21,6 +21,16 @@ public class SeleniumUtil {
* @return 配置好的 ChromeDriver
*/
public static ChromeDriver createDriver(boolean headless) {
return createDriver(headless, null);
}
/**
* 创建防检测的 ChromeDriver支持代理
* @param headless 是否启用无头模式
* @param proxy 代理地址格式host:port如 123.96.236.32:40016
* @return 配置好的 ChromeDriver
*/
public static ChromeDriver createDriver(boolean headless, String proxy) {
try {
WebDriverManager.chromedriver()
.driverRepositoryUrl(new URL("https://registry.npmmirror.com/-/binary/chromedriver/"))
@@ -37,6 +47,12 @@ public class SeleniumUtil {
options.addArguments("--no-sandbox", "--disable-gpu", "--disable-dev-shm-usage");
options.addArguments("user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36");
// 配置代理
if (proxy != null && !proxy.trim().isEmpty()) {
options.addArguments("--proxy-server=http://" + proxy);
options.addArguments("--proxy-bypass-list=<-loopback>");
System.out.println("ChromeDriver使用代理: http://" + proxy);
}
// 无头模式
if (headless) {
options.addArguments("--headless=new");

View File

@@ -1,42 +1,128 @@
package com.tashow.erp.utils;
import jakarta.annotation.PreDestroy;
import org.openqa.selenium.JavascriptExecutor;
import org.openqa.selenium.chrome.ChromeDriver;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;
import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;
/**
* 商标检查工具
* 支持批量并发查询每100次切换代理
*/
@Component
public class TrademarkCheckUtil {
@Autowired
private ProxyPool proxyPool;
private ChromeDriver driver;
@Autowired
RestTemplate restTemplate;
public boolean checkTrademark(String brandName) {
private final AtomicInteger checkCount = new AtomicInteger(0);
private static final int PROXY_SWITCH_THRESHOLD = 100;
private synchronized void ensureInit() {
if (driver == null) {
for (int i = 0; i < 3; i++) {
try {
driver = SeleniumUtil.createDriver(false, proxyPool.getProxy());
driver.get("https://tmsearch.uspto.gov/search/search-results");
Thread.sleep(2000);
String script = String.format("""
return fetch('https://tmsearch.uspto.gov/prod-stage-v1-0-0/tmsearch', {
Thread.sleep(5000);
return; // 成功则返回
} catch (Exception e) {
System.err.println("初始化失败(尝试" + (i+1) + "/3: " + e.getMessage());
if (driver != null) {
try { driver.quit(); } catch (Exception ex) {}
driver = null;
}
if (i == 2) throw new RuntimeException("初始化失败已重试3次", e);
}
}
}
}
public synchronized Map<String, Boolean> batchCheck(List<String> brands) {
ensureInit();
// 每100个切换代理
if (checkCount.addAndGet(brands.size()) >= PROXY_SWITCH_THRESHOLD) {
try { driver.quit(); } catch (Exception e) {}
driver = null;
checkCount.set(0);
ensureInit();
}
// 构建批量查询脚本(带错误诊断)
String script = """
const brands = arguments[0];
const callback = arguments[arguments.length - 1];
Promise.all(brands.map(brand =>
fetch('https://tmsearch.uspto.gov/prod-stage-v1-0-0/tmsearch', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
query: {bool: {must: [{bool: {should: [
{match_phrase: {WM: {query: '%s', boost: 5}}},
{match: {WM: {query: '%s', boost: 2}}},
{match_phrase: {PM: {query: '%s', boost: 2}}}
{match_phrase: {WM: {query: brand, boost: 5}}},
{match: {WM: {query: brand, boost: 2}}},
{match_phrase: {PM: {query: brand, boost: 2}}}
]}}]}},
size: 1, _source: ['alive']
})
}).then(r => r.json()).then(d => d?.hits?.hits?.[0]?.source?.alive || false);
""", brandName, brandName, brandName);
Object result = ((JavascriptExecutor) driver).executeAsyncScript("var callback = arguments[arguments.length - 1];" + script.replace("return", "").replace(";", ".then(callback);"));
return Boolean.TRUE.equals(result);
})
.then(r => {
if (!r.ok) {
return {brand, alive: false, error: `HTTP ${r.status}: ${r.statusText}`};
}
return r.json().then(d => ({
brand,
alive: d?.hits?.hits?.[0]?.source?.alive || false,
error: null
}));
})
.catch(e => ({
brand,
alive: false,
error: e.name + ': ' + e.message
}))
)).then(callback);
""";
try {
@SuppressWarnings("unchecked")
List<Map<String, Object>> results = (List<Map<String, Object>>)
((JavascriptExecutor) driver).executeAsyncScript(script, brands);
Map<String, Boolean> resultMap = new HashMap<>();
for (Map<String, Object> item : results) {
String brand = (String) item.get("brand");
Boolean alive = (Boolean) item.get("alive");
String error = (String) item.get("error");
if (error != null) {
// 查询失败,不放入结果,只打印错误
System.err.println(brand + " -> [查询失败: " + error + "]");
} else {
// 查询成功,放入结果
resultMap.put(brand, alive);
System.out.println(brand + " -> " + (alive ? "✓ 已注册" : "✗ 未注册"));
}
}
return resultMap;
} catch (Exception e) {
System.err.println("检测失败: " + e.getMessage());
return false;
System.err.println("批量查询失败: " + e.getMessage());
return new HashMap<>();
}
}
public synchronized void closeDriver() {
if (driver != null) {
try { driver.quit(); } catch (Exception e) {}
driver = null;
checkCount.set(0);
}
}
@PreDestroy
public void cleanup() {
closeDriver();
}
}

View File

@@ -1,5 +1,4 @@
package com.ruoyi.web.controller.tool;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.ruoyi.common.annotation.Anonymous;
@@ -7,8 +6,11 @@ import com.ruoyi.common.constant.CacheConstants;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.redis.RedisCache;
import com.ruoyi.system.service.IMarkService;
import cn.hutool.core.io.FileUtil;
import cn.hutool.http.HttpUtil;
import cn.hutool.poi.excel.ExcelReader;
import cn.hutool.poi.excel.ExcelUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.FileSystemResource;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
@@ -17,21 +19,19 @@ import org.springframework.util.MultiValueMap;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.util.logging.Handler;
import java.util.*;
@RequestMapping("/tool/mark")
@RestController
@Anonymous
public class MarkController {
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 final RestTemplate restTemplate = new RestTemplate();
private final ObjectMapper objectMapper = new ObjectMapper();
@Autowired
private RedisCache redisCache;
@Autowired
private IMarkService markService;
@@ -56,7 +56,83 @@ public class MarkController {
HttpEntity<MultiValueMap<String, String>> requestEntity = new HttpEntity<>(formData, headers);
String result = restTemplate.postForObject("https://api.fangzhoujingxuan.com/Task", requestEntity, String.class);
JsonNode json = objectMapper.readTree(result);
return AjaxResult.success(json.get("D").asText());
if(json.get("S").asInt()==-1006){
token= markService.login();
formData.add("t", token);
requestEntity = new HttpEntity<>(formData, headers);
result = restTemplate.postForObject("https://api.fangzhoujingxuan.com/Task", requestEntity, String.class);
json= objectMapper.readTree(result);
}
JsonNode dNode = json.get("D").get("items").get(0);
// 获取下载链接并处理Excel数据
String downloadUrl = dNode.get("download_url").asText();
for (int i = 0; i < 6 && downloadUrl.isEmpty(); i++) {
Thread.sleep(5000);
long reTs = System.currentTimeMillis();
MultiValueMap<String, String> reFormData = new LinkedMultiValueMap<>();
reFormData.add("c", "TaskPageList");
reFormData.add("d", d);
reFormData.add("t", token);
reFormData.add("s", markService.md5(reTs + d + API_SECRET));
reFormData.add("ts", String.valueOf(reTs));
reFormData.add("website", "1");
HttpEntity<MultiValueMap<String, String>> reRequestEntity = new HttpEntity<>(reFormData, headers);
String reResult = restTemplate.postForObject("https://api.fangzhoujingxuan.com/Task", reRequestEntity, String.class);
JsonNode reJson = objectMapper.readTree(reResult);
dNode = reJson.get("D").get("items").get(0);
downloadUrl = reJson.get("D").get("items").get(0).get("download_url").asText();
}
String tempFilePath = System.getProperty("java.io.tmpdir") + "/trademark_" + System.currentTimeMillis() + ".xlsx";
HttpUtil.downloadFile(downloadUrl, FileUtil.file(tempFilePath));
List<Map<String, Object>> filteredData = new ArrayList<>();
ExcelReader reader = null;
try {
reader = ExcelUtil.getReader(FileUtil.file(tempFilePath));
List<List<Object>> rows = reader.read();
// 找到各列的索引
int asinIndex = -1, brandIndex = -1, trademarkTypeIndex = -1, registerDateIndex = -1, productImageIndex = -1;
if (!rows.isEmpty()) {
List<Object> header = rows.get(0);
for (int i = 0; i < header.size(); i++) {
String headerName = header.get(i).toString().trim();
if (headerName.equals("ASIN")) asinIndex = i;
else if (headerName.equals("品牌")) brandIndex = i;
else if (headerName.equals("商标类型")) trademarkTypeIndex = i;
else if (headerName.equals("注册时间")) registerDateIndex = i;
else if (headerName.equals("商品主图")) productImageIndex = i;
}
}
// 过滤TM和未注册数据
for (int i = 1; i < rows.size(); i++) {
List<Object> row = rows.get(i);
if (trademarkTypeIndex >= 0 && row.size() > trademarkTypeIndex) {
String trademarkType = row.get(trademarkTypeIndex).toString().trim();
if ("TM".equals(trademarkType) || "未注册".equals(trademarkType)) {
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));
if (trademarkTypeIndex >= 0 && row.size() > trademarkTypeIndex) item.put("商标类型", row.get(trademarkTypeIndex));
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);
}
}
}
} finally {
if (reader != null) {
reader.close();
}
FileUtil.del(tempFilePath);
}
Map<String, Object> combinedResult = new HashMap<>();
combinedResult.put("original", dNode);
combinedResult.put("filtered", filteredData);
return AjaxResult.success(combinedResult);
} catch (Exception e) {
throw new RuntimeException(e);
}
@@ -89,11 +165,49 @@ public class MarkController {
requestEntity = new HttpEntity<>(formData, headers);
result = restTemplate.postForObject("https://api.fangzhoujingxuan.com/Task", requestEntity, String.class);
jsonNode= objectMapper.readTree(result);
}
return jsonNode.get("S").asInt()==1?AjaxResult.success(result): AjaxResult.error( jsonNode.get("S").asText());
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* 品牌商标筛查(调用 erp_client_sb 服务)
* @param brands 品牌列表JSON数组
* @return 筛查结果
*/
@PostMapping("brandCheck")
public AjaxResult brandCheck(@RequestBody List<String> brands) {
try {
if (brands == null || brands.isEmpty()) {
return AjaxResult.error("品牌列表不能为空");
}
// 调用 erp_client_sb 的商标检查接口
String url = ERP_CLIENT_BASE_URL + "/api/trademark/brandCheck";
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<List<String>> requestEntity = new HttpEntity<>(brands, headers);
// 调用远程服务
String result = restTemplate.postForObject(url, requestEntity, String.class);
JsonNode jsonNode = objectMapper.readTree(result);
// 判断返回状态 (erp_client_sb 的 JsonData: code=0 成功, code=-1 失败)
if (jsonNode.get("code").asInt() == 0) {
// 转换数据格式以适配前端
JsonNode data = jsonNode.get("data");
return AjaxResult.success(objectMapper.convertValue(data, Map.class));
} else {
String msg = jsonNode.has("msg") ? jsonNode.get("msg").asText() : "品牌筛查失败";
return AjaxResult.error(msg);
}
} catch (Exception e) {
e.printStackTrace();
return AjaxResult.error("品牌筛查失败: " + e.getMessage());
}
}
}

View File

@@ -94,21 +94,25 @@ spring:
# 密码
# password:
password: 123123
# 连接超时时间(降低超时,快速失败)
timeout: 5s
# 连接超时时间
timeout: 10s
lettuce:
pool:
# 连接池中的最小空闲连接(保持预热连接,避免临时建连)
min-idle: 2
min-idle: 5
# 连接池中的最大空闲连接
max-idle: 10
# 连接池的最大数据库连接数(增加以应对并发)
max-active: 20
# 连接池最大阻塞等待时间(设置合理超时,避免无限等待)
max-wait: 5000ms
max-idle: 20
# 连接池的最大数据库连接数
max-active: 50
# 连接池最大阻塞等待时间
max-wait: 10s
# 关闭超时时间
shutdown-timeout: 100ms
# 心跳检测配置
cluster:
refresh:
adaptive: true
period: 30s
# token配置
token:
# 令牌自定义标识

View File

@@ -100,9 +100,11 @@
</dependency>
<!-- redis 缓存操作 -->
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-data-redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<version>4.0.0-M3</version>
</dependency>
<!-- pool 对象池 -->

View File

@@ -0,0 +1,52 @@
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());
}
}
}
}

View File

@@ -0,0 +1,79 @@
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;
}
}

44
test_brand_data.json Normal file
View File

@@ -0,0 +1,44 @@
{
"items": [
{
"brand_info": "{\"brand_name\":\"Bandelt\",\"brand_type\":\"R\",\"status_code\":\"Live\",\"filing_date\":\"2021-06-27\",\"registration_date\":\"2022-07-12\"}"
},
{
"brand_info": "{\"brand_name\":\"Magic Ants\",\"brand_type\":\"R\",\"status_code\":\"Live\",\"filing_date\":\"2018-04-09\",\"registration_date\":\"2019-02-12\"}"
},
{
"brand_info": "{\"brand_name\":\"VWALK\",\"brand_type\":\"TM\",\"status_code\":\"Dead\",\"filing_date\":\"2020-08-18\",\"registration_date\":\"\"}"
},
{
"brand_info": "{\"brand_name\":\"TUPWEL\",\"brand_type\":\"TM\",\"status_code\":\"Dead\",\"filing_date\":\"\",\"registration_date\":\"\"}"
},
{
"brand_info": "{\"brand_name\":\"Muruseni\",\"brand_type\":\"多品类\",\"status_code\":\"\",\"filing_date\":\"\",\"registration_date\":\"\"}"
},
{
"brand_info": "{\"brand_name\":\"FLYPROFiber\",\"brand_type\":\"R\",\"status_code\":\"Live\",\"filing_date\":\"2020-07-29\",\"registration_date\":\"2021-03-09\"}"
},
{
"brand_info": "{\"brand_name\":\"LECPECON\",\"brand_type\":\"R\",\"status_code\":\"Live\",\"filing_date\":\"2020-12-28\",\"registration_date\":\"2021-10-19\"}"
},
{
"brand_info": "{\"brand_name\":\"SquEqu\",\"brand_type\":\"R\",\"status_code\":\"Live\",\"filing_date\":\"2023-01-18\",\"registration_date\":\"2024-02-06\"}"
},
{
"brand_info": "{\"brand_name\":\"pechpell\",\"brand_type\":\"TM\",\"status_code\":\"Live\",\"filing_date\":\"2024-09-27\",\"registration_date\":\"\"}"
},
{
"brand_info": "{\"brand_name\":\"Arefic\",\"brand_type\":\"R\",\"status_code\":\"Live\",\"filing_date\":\"2022-09-08\",\"registration_date\":\"2023-10-17\"}"
},
{
"brand_info": "{\"brand_name\":\"Tiga\",\"brand_type\":\"多品类\",\"status_code\":\"\",\"filing_date\":\"\",\"registration_date\":\"\"}"
},
{
"brand_info": "{\"brand_name\":\"uxcell\",\"brand_type\":\"多品类\",\"status_code\":\"\",\"filing_date\":\"\",\"registration_date\":\"\"}"
},
{
"brand_info": "{\"brand_name\":\"CJRSLRB\",\"brand_type\":\"R\",\"status_code\":\"Live\",\"filing_date\":\"2014-11-28\",\"registration_date\":\"2015-07-21\"}"
}
]
}

412
test_response.json Normal file
View File

@@ -0,0 +1,412 @@
{
"trademarks" : [ {
"status" : {
"staff" : {
"examiner" : {
"number" : null
},
"paralegal" : null,
"ituParalegal" : null,
"lie" : null,
"chargeTo" : null
},
"correspondence" : {
"freeFormAddress" : [ ],
"address" : {
"line1" : "625 SLATERS LANE, FOURTH FLOOR",
"city" : "ALEXANDRIA",
"region" : {
"stateCountry" : {
"code" : "VA",
"name" : "VIRGINIA"
},
"isoRegion" : {
"code" : "VA",
"name" : "VIRGINIA"
},
"iso" : {
"code" : "US",
"name" : "UNITED STATES"
},
"wipo" : null
},
"postalCode" : "22314-1176",
"countryCode" : "US",
"countryName" : "UNITED STATES"
},
"attorneyName" : "Thomas J. Moore",
"attorneyEmail" : {
"authIndicator" : "Y",
"addresses" : [ "mail@baconthomas.com" ]
},
"individualFullName" : "THOMAS J. MOORE",
"firmName" : "BACON & THOMAS, PLLC",
"correspondantPhone" : "703-683-0500",
"correspondantFax" : "703-683-1080",
"correspondantEmail" : {
"authIndicator" : "Y",
"addresses" : [ "mail@baconthomas.com" ]
}
},
"serialNumber" : 88123456,
"designSearchList" : [ ],
"filingDate" : "2018-09-19",
"usRegistrationNumber" : "",
"filedAsTeasPlusApp" : true,
"currentlyAsTeasPlusApp" : true,
"filedAsBaseApp" : false,
"currentlyAsBaseApp" : false,
"filedAsTeasRfApp" : false,
"currentlyAsTeasRfApp" : false,
"supplementalRegister" : false,
"amendPrincipal" : false,
"amendSupplemental" : false,
"trademark" : false,
"certificationMark" : false,
"serviceMark" : true,
"collectiveMembershipMark" : false,
"collectiveServiceMark" : false,
"collectiveTradeMark" : false,
"status" : 606,
"statusDate" : "2019-10-14",
"dateAbandoned" : "2019-10-14",
"standardChar" : true,
"markDrawingCd" : "4",
"colorDrawingCurr" : false,
"section2f" : false,
"section2fPartial" : false,
"others" : false,
"publishedPrevRegMark" : false,
"clsTotal" : 3,
"filedUse" : false,
"filedItu" : true,
"filed44d" : false,
"filed44e" : false,
"filed66a" : false,
"filedNoBasis" : false,
"useCurr" : false,
"ituCurr" : true,
"sect44dCurr" : false,
"sect44eCurr" : false,
"sect66aCurr" : false,
"noBasisCurr" : false,
"useAmended" : false,
"ituAmended" : false,
"sect44dAmended" : false,
"sect44eAmended" : false,
"attrnyDktNumber" : "MYLI6005/TJM",
"sect8Filed" : false,
"sect8Acpt" : false,
"sect8PartialAcpt" : false,
"sect15Filed" : false,
"sect15Ack" : false,
"sect71Filed" : false,
"sect71Acpt" : false,
"sect71PartialAcpt" : false,
"renewalFiled" : false,
"changeInReg" : false,
"lawOffAsgnCd" : "M40",
"currLocationCd" : "700",
"currLocationDt" : "2019-03-12",
"chargeToLocation" : null,
"phyLocation" : null,
"phyLocationDt" : null,
"extStatusDesc" : "Abandoned because no Statement of Use or Extension Request timely filed after Notice of Allowance was issued. To view all documents in this file, click on the Trademark Document Retrieval link at the top of this page. ",
"intStatusDesc" : null,
"markDrawDesc" : "STANDARD CHARACTER MARK",
"currentLoc" : "INTENT TO USE SECTION",
"correction" : "",
"disclaimer" : "\"UL\"",
"markElement" : "MYLIFE UL PROTECT",
"parentOf" : [ ],
"prevRegNumList" : [ ],
"newLawOffAsgnCd" : "113",
"lawOffAssigned" : "LAW OFFICE 113",
"tm5Status" : 10,
"tm5StatusDesc" : "DEAD/APPLICATION/Refused/Dismissed or Invalidated",
"tm5StatusDef" : "This trademark application was refused, dismissed, or invalidated by the Office and this application is no longer active.",
"physicalLocationHistory" : [ {
"eventDate" : "2018-09-25",
"physicalLocation" : "MADCD",
"physicalLocationDescription" : "NO PHYSICAL FILE"
}, {
"eventDate" : "2018-09-22",
"physicalLocation" : "OUT",
"physicalLocationDescription" : "NO PHYSICAL FILE"
} ],
"pseudoMark" : null
},
"parties" : {
"ownerGroups" : {
"20" : [ {
"serialNumber" : 88123456,
"partyType" : 20,
"partyTypeDescription" : "OWNER AT PUBLICATION",
"reelFrame" : null,
"entityNum" : 1,
"entityType" : {
"code" : 3,
"description" : "CORPORATION"
},
"name" : "Modern Woodmen of America",
"composedOf" : null,
"dbaAkaFormerly" : null,
"assignment" : null,
"address1" : "1701 - 1st Avenue",
"address2" : "",
"city" : "Rock Island",
"addressStateCountry" : {
"stateCountry" : {
"code" : "IL",
"name" : "ILLINOIS"
},
"isoRegion" : {
"code" : "IL",
"name" : "ILLINOIS"
},
"iso" : {
"code" : "US",
"name" : "UNITED STATES"
},
"wipo" : null
},
"zip" : "61201",
"citizenship" : {
"stateCountry" : {
"code" : "IL",
"name" : "ILLINOIS"
},
"isoRegion" : {
"code" : "IL",
"name" : "ILLINOIS"
},
"iso" : {
"code" : "US",
"name" : "UNITED STATES"
},
"wipo" : null
}
} ],
"10" : [ {
"serialNumber" : 88123456,
"partyType" : 10,
"partyTypeDescription" : "ORIGINAL APPLICANT",
"reelFrame" : null,
"entityNum" : 1,
"entityType" : {
"code" : 3,
"description" : "CORPORATION"
},
"name" : "Modern Woodmen of America",
"composedOf" : null,
"dbaAkaFormerly" : null,
"assignment" : null,
"address1" : "1701 - 1st Avenue",
"address2" : "",
"city" : "Rock Island",
"addressStateCountry" : {
"stateCountry" : {
"code" : "IL",
"name" : "ILLINOIS"
},
"isoRegion" : {
"code" : "IL",
"name" : "ILLINOIS"
},
"iso" : {
"code" : "US",
"name" : "UNITED STATES"
},
"wipo" : null
},
"zip" : "61201",
"citizenship" : {
"stateCountry" : {
"code" : "IL",
"name" : "ILLINOIS"
},
"isoRegion" : {
"code" : "IL",
"name" : "ILLINOIS"
},
"iso" : {
"code" : "US",
"name" : "UNITED STATES"
},
"wipo" : null
}
} ]
}
},
"gsList" : [ {
"serialNumber" : 88123456,
"internationalClassPrime" : true,
"usClasses" : [ {
"code" : "100",
"description" : "Miscellaneous"
}, {
"code" : "101",
"description" : "Advertising and Business"
}, {
"code" : "102",
"description" : "Insurance and Financial"
} ],
"internationalClasses" : [ {
"code" : "036",
"description" : "Insurance and financial"
} ],
"pseudoClasses" : [ ],
"statusCode" : "6",
"statusDescription" : "ACTIVE",
"statusDate" : "2018-09-25",
"firstUseDate" : null,
"firstUseInCommerceDate" : null,
"firstUseDateDescription" : null,
"firstUseInCommerceDateDescription" : null,
"description" : "Insurance administration in the field of life insurance; Insurance agencies in the field of life insurance; Insurance agency and brokerage; Insurance brokerage in the field of life insurance; Insurance services, namely, underwriting life insurance; Insurance services, namely, underwriting, issuance and administration of life insurance; Insurance underwriting in the field of life insurance; Issuance of life insurance",
"classBasis" : null,
"primeClassCode" : "036"
} ],
"foreignInfoList" : [ ],
"prosecutionHistory" : [ {
"entryNumber" : 18,
"entryCode" : "MAB6",
"entryType" : "E",
"proceedingNum" : 0,
"entryDate" : "2019-10-15T04:00:00.000+0000",
"entryDesc" : "ABANDONMENT NOTICE E-MAILED - NO USE STATEMENT FILED"
}, {
"entryNumber" : 17,
"entryCode" : "ABN6",
"entryType" : "S",
"proceedingNum" : 0,
"entryDate" : "2019-10-14T04:00:00.000+0000",
"entryDesc" : "ABANDONMENT - NO USE STATEMENT FILED"
}, {
"entryNumber" : 16,
"entryCode" : "NOAM",
"entryType" : "E",
"proceedingNum" : 0,
"entryDate" : "2019-03-12T04:00:00.000+0000",
"entryDesc" : "NOA E-MAILED - SOU REQUIRED FROM APPLICANT"
}, {
"entryNumber" : 15,
"entryCode" : "NPUB",
"entryType" : "E",
"proceedingNum" : 0,
"entryDate" : "2019-01-15T05:00:00.000+0000",
"entryDesc" : "OFFICIAL GAZETTE PUBLICATION CONFIRMATION E-MAILED"
}, {
"entryNumber" : 14,
"entryCode" : "PUBO",
"entryType" : "A",
"proceedingNum" : 0,
"entryDate" : "2019-01-15T05:00:00.000+0000",
"entryDesc" : "PUBLISHED FOR OPPOSITION"
}, {
"entryNumber" : 13,
"entryCode" : "NONP",
"entryType" : "E",
"proceedingNum" : 0,
"entryDate" : "2018-12-26T05:00:00.000+0000",
"entryDesc" : "NOTIFICATION OF NOTICE OF PUBLICATION E-MAILED"
}, {
"entryNumber" : 12,
"entryCode" : "ALIE",
"entryType" : "A",
"proceedingNum" : 0,
"entryDate" : "2018-12-06T05:00:00.000+0000",
"entryDesc" : "ASSIGNED TO LIE"
}, {
"entryNumber" : 11,
"entryCode" : "CNSA",
"entryType" : "P",
"proceedingNum" : 0,
"entryDate" : "2018-11-15T05:00:00.000+0000",
"entryDesc" : "APPROVED FOR PUB - PRINCIPAL REGISTER"
}, {
"entryNumber" : 10,
"entryCode" : "XAEC",
"entryType" : "I",
"proceedingNum" : 0,
"entryDate" : "2018-11-09T05:00:00.000+0000",
"entryDesc" : "EXAMINER'S AMENDMENT ENTERED"
}, {
"entryNumber" : 9,
"entryCode" : "GNEN",
"entryType" : "O",
"proceedingNum" : 0,
"entryDate" : "2018-11-09T05:00:00.000+0000",
"entryDesc" : "NOTIFICATION OF EXAMINERS AMENDMENT E-MAILED"
}, {
"entryNumber" : 8,
"entryCode" : "GNEA",
"entryType" : "O",
"proceedingNum" : 0,
"entryDate" : "2018-11-09T05:00:00.000+0000",
"entryDesc" : "EXAMINERS AMENDMENT E-MAILED"
}, {
"entryNumber" : 7,
"entryCode" : "CNEA",
"entryType" : "R",
"proceedingNum" : 0,
"entryDate" : "2018-11-09T05:00:00.000+0000",
"entryDesc" : "EXAMINERS AMENDMENT -WRITTEN"
}, {
"entryNumber" : 6,
"entryCode" : "GNRN",
"entryType" : "O",
"proceedingNum" : 0,
"entryDate" : "2018-11-08T05:00:00.000+0000",
"entryDesc" : "NOTIFICATION OF NON-FINAL ACTION E-MAILED"
}, {
"entryNumber" : 5,
"entryCode" : "GNRT",
"entryType" : "F",
"proceedingNum" : 0,
"entryDate" : "2018-11-08T05:00:00.000+0000",
"entryDesc" : "NON-FINAL ACTION E-MAILED"
}, {
"entryNumber" : 4,
"entryCode" : "CNRT",
"entryType" : "R",
"proceedingNum" : 0,
"entryDate" : "2018-11-08T05:00:00.000+0000",
"entryDesc" : "NON-FINAL ACTION WRITTEN"
}, {
"entryNumber" : 3,
"entryCode" : "DOCK",
"entryType" : "D",
"proceedingNum" : 0,
"entryDate" : "2018-10-31T04:00:00.000+0000",
"entryDesc" : "ASSIGNED TO EXAMINER"
}, {
"entryNumber" : 2,
"entryCode" : "NWOS",
"entryType" : "I",
"proceedingNum" : 0,
"entryDate" : "2018-09-25T04:00:00.000+0000",
"entryDesc" : "NEW APPLICATION OFFICE SUPPLIED DATA ENTERED"
}, {
"entryNumber" : 1,
"entryCode" : "NWAP",
"entryType" : "I",
"proceedingNum" : 0,
"entryDate" : "2018-09-22T04:00:00.000+0000",
"entryDesc" : "NEW APPLICATION ENTERED"
} ],
"relationshipBundleList" : [ ],
"internationalData" : false,
"publication" : {
"serialNumber" : 88123456,
"datePublished" : "2019-01-15",
"noticeOfAllowanceDate" : "2019-03-12",
"officialGazettes" : [ ]
},
"divisional" : {
"serialNumber" : 88123456,
"childOf" : null,
"parentOfList" : [ ]
}
} ]
}