This commit is contained in:
2025-09-25 16:05:29 +08:00
parent bb997857fd
commit 5e876b0f1d
17 changed files with 1001 additions and 632 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -19,7 +19,7 @@
"@vitejs/plugin-vue": "^4.4.1", "@vitejs/plugin-vue": "^4.4.1",
"chalk": "^4.1.2", "chalk": "^4.1.2",
"chokidar": "^3.5.3", "chokidar": "^3.5.3",
"electron": "^32.1.2", "electron": "^38.1.2",
"electron-builder": "^25.1.6", "electron-builder": "^25.1.6",
"typescript": "^5.2.2", "typescript": "^5.2.2",
"vite": "^4.5.0" "vite": "^4.5.0"

View File

@@ -43,6 +43,10 @@ export const deviceApi = {
heartbeat(payload: { username: string; deviceId: string; version?: string }) { heartbeat(payload: { username: string; deviceId: string; version?: string }) {
return http.postVoid(`${base}/heartbeat`, payload) return http.postVoid(`${base}/heartbeat`, payload)
}, },
offline(payload: { deviceId: string }) {
return http.postVoid(`${base}/offline`, payload)
},
} }

View File

@@ -2,10 +2,13 @@
export type HttpMethod = 'GET' | 'POST'; export type HttpMethod = 'GET' | 'POST';
const BASE_CLIENT = 'http://localhost:8081'; // erp_client_sb const BASE_CLIENT = 'http://localhost:8081'; // erp_client_sb
const BASE_RUOYI = 'http://localhost:8080'; // ruoyi-admin const BASE_RUOYI = 'http://localhost:8080';
function resolveBase(path: string): string { function resolveBase(path: string): string {
if (path.startsWith('/tool/banma')) return BASE_RUOYI; // 走 ruoyi-admin 的路径:鉴权与版本、平台工具路由
if (path.startsWith('/system/')) return BASE_RUOYI; // 版本控制器 VersionController
if (path.startsWith('/tool/banma')) return BASE_RUOYI; // 既有规则保留
// 其他默认走客户端服务
return BASE_CLIENT; return BASE_CLIENT;
} }

View File

@@ -31,6 +31,7 @@ export interface BanmaAccount {
id?: number; id?: number;
name?: string; name?: string;
username?: string; username?: string;
password?: string;
token?: string; token?: string;
tokenExpireAt?: string | number; tokenExpireAt?: string | number;
isDefault?: number; isDefault?: number;
@@ -53,12 +54,12 @@ export const zebraApi = {
}, },
// 业务采集(仍走客户端微服务 8081 // 业务采集(仍走客户端微服务 8081
getShops() { getShops(params?: { accountId?: number }) {
return http.get<{ data?: { list?: Array<{ id: string; shopName: string }> } }>( return http.get<{ data?: { list?: Array<{ id: string; shopName: string }> } }>(
'/api/banma/shops' '/api/banma/shops', params as unknown as Record<string, unknown>
); );
}, },
getOrders(params: { startDate?: string; endDate?: string; page?: number; pageSize?: number; shopIds?: string }) { getOrders(params: { accountId?: number; startDate?: string; endDate?: string; page?: number; pageSize?: number; shopIds?: string }) {
return http.get<ZebraOrdersResp>('/api/banma/orders', params as unknown as Record<string, unknown>); return http.get<ZebraOrdersResp>('/api/banma/orders', params as unknown as Record<string, unknown>);
}, },

View File

@@ -63,21 +63,24 @@ function showRegister() {
<template> <template>
<el-dialog <el-dialog
title="用户登录" title=""
v-model="visible" v-model="visible"
:close-on-click-modal="false" :close-on-click-modal="false"
width="400px" width="360px"
center> center
<div style="text-align: center; padding: 20px 0;"> class="auth-dialog">
<div style="margin-bottom: 30px; color: #666;"> <div style="padding: 20px 0;">
<el-icon size="48" color="#409EFF"><User /></el-icon> <div style="text-align: center; margin-bottom: 16px; color: #666;">
<p style="margin-top: 15px; font-size: 16px;">请登录以使用系统功能</p> <img src="/icon/image.png" alt="logo" class="auth-logo" />
</div>
<div class="auth-title-wrap">
<h3 class="auth-title">账号登录</h3>
<p class="auth-subtitle">登录账户以获取完整服务体验</p>
</div> </div>
<el-input <el-input
v-model="authForm.username" v-model="authForm.username"
placeholder="请输入用户名" placeholder="请输入用户名"
prefix-icon="User"
size="large" size="large"
style="margin-bottom: 15px;" style="margin-bottom: 15px;"
:disabled="authLoading" :disabled="authLoading"
@@ -101,16 +104,9 @@ function showRegister() {
:loading="authLoading" :loading="authLoading"
:disabled="!authForm.username || !authForm.password || authLoading" :disabled="!authForm.username || !authForm.password || authLoading"
@click="handleAuth" @click="handleAuth"
style="width: 120px; margin-right: 10px;"> style="width: 100%;">
登录 登录
</el-button> </el-button>
<el-button
size="large"
:disabled="authLoading"
@click="cancelAuth"
style="width: 120px;">
取消
</el-button>
</div> </div>
<div style="margin-top: 20px; text-align: center;"> <div style="margin-top: 20px; text-align: center;">
@@ -121,3 +117,38 @@ function showRegister() {
</div> </div>
</el-dialog> </el-dialog>
</template> </template>
<style scoped>
.auth-logo {
width: 160px;
height: auto;
}
.auth-dialog {
--el-color-primary: #1677FF;
}
.auth-dialog :deep(.el-button--primary) {
background-color: #1677FF;
border-color: #1677FF;
}
.auth-title-wrap {
margin-bottom: 12px;
}
.auth-title {
margin: 0;
font-size: 18px;
font-weight: 700;
color: #1f1f1f;
text-align: left;
}
.auth-subtitle {
margin: 6px 0 0;
font-size: 12px;
color: #8c8c8c;
text-align: left;
}
</style>

View File

@@ -85,21 +85,24 @@ function backToLogin() {
<template> <template>
<el-dialog <el-dialog
title="账号注册" title=""
v-model="visible" v-model="visible"
:close-on-click-modal="false" :close-on-click-modal="false"
width="450px" width="380px"
center> center
<div style="text-align: center; padding: 20px 0;"> class="auth-dialog">
<div style="margin-bottom: 20px; color: #666;"> <div style="padding: 20px 0;">
<el-icon size="48" color="#67C23A"><User /></el-icon> <div style="text-align: center; margin-bottom: 16px; color: #666;">
<p style="margin-top: 15px; font-size: 16px;">创建新账号</p> <img src="/icon/image.png" alt="logo" class="auth-logo" />
</div>
<div class="auth-title-wrap">
<h3 class="auth-title">创建账号</h3>
<p class="auth-subtitle">创建账号以获取完整服务体验</p>
</div> </div>
<el-input <el-input
v-model="registerForm.username" v-model="registerForm.username"
placeholder="请输入用户名" placeholder="请输入用户名"
prefix-icon="User"
size="large" size="large"
style="margin-bottom: 15px;" style="margin-bottom: 15px;"
:disabled="registerLoading" :disabled="registerLoading"
@@ -135,21 +138,14 @@ function backToLogin() {
<div> <div>
<el-button <el-button
type="success" type="primary"
size="large" size="large"
:loading="registerLoading" :loading="registerLoading"
:disabled="!canRegister || registerLoading" :disabled="!canRegister || registerLoading"
@click="handleRegister" @click="handleRegister"
style="width: 120px; margin-right: 10px;"> style="width: 100%;">
注册 注册
</el-button> </el-button>
<el-button
size="large"
:disabled="registerLoading"
@click="cancelRegister"
style="width: 120px;">
取消
</el-button>
</div> </div>
<div style="margin-top: 20px; text-align: center;"> <div style="margin-top: 20px; text-align: center;">
@@ -160,3 +156,37 @@ function backToLogin() {
</div> </div>
</el-dialog> </el-dialog>
</template> </template>
<style scoped>
.auth-logo {
width: 160px;
height: auto;
}
.auth-dialog {
--el-color-primary: #1677FF;
}
.auth-dialog :deep(.el-button--primary) {
background-color: #1677FF;
border-color: #1677FF;
}
.auth-title-wrap {
margin-bottom: 12px;
}
.auth-title {
margin: 0;
font-size: 18px;
font-weight: 700;
color: #1f1f1f;
text-align: left;
}
.auth-subtitle {
margin: 6px 0 0;
font-size: 12px;
color: #8c8c8c;
text-align: left;
}
</style>

View File

@@ -53,6 +53,10 @@ const regionOptions = [
// 获取数据筛选:查询日期 // 获取数据筛选:查询日期
const dateRange = ref<string[] | null>(null) const dateRange = ref<string[] | null>(null)
// 示例弹窗与示例数据
const rakutenExampleVisible = ref(false)
const rakutenExampleRows = ref([{ shopName: 'GBAmarket' }, { shopName: 'jotimei' }])
function handleSizeChange(size: number) { function handleSizeChange(size: number) {
pageSize.value = size pageSize.value = size
currentPage.value = 1 currentPage.value = 1
@@ -66,6 +70,21 @@ function openRakutenUpload() {
uploadInputRef.value?.click() uploadInputRef.value?.click()
} }
function viewRakutenExample() { rakutenExampleVisible.value = true }
function downloadRakutenTemplate() {
const html = '<table><tr><th>店铺名</th></tr><tr><td>GBAmarket</td></tr><tr><td>jotimei</td></tr></table>'
const blob = new Blob([html], { type: 'application/vnd.ms-excel' })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = 'rakuten店铺模板.xls'
document.body.appendChild(a)
a.click()
document.body.removeChild(a)
URL.revokeObjectURL(url)
}
function parseSkuPrices(input: any) { function parseSkuPrices(input: any) {
try { try {
let skuSource: any = input let skuSource: any = input
@@ -315,18 +334,18 @@ onMounted(loadLatest)
</div> </div>
<div class="desc">请导入店铺信息仅限 Excel 文件表格第一列必须为乐天店铺名</div> <div class="desc">请导入店铺信息仅限 Excel 文件表格第一列必须为乐天店铺名</div>
<div class="links"> <div class="links">
<a class="link" @click.prevent>点击查看示例</a> <a class="link" @click.prevent="viewRakutenExample">点击查看示例</a>
<span class="sep">|</span> <span class="sep">|</span>
<a class="link" @click.prevent>点击下载模板</a> <a class="link" @click.prevent="downloadRakutenTemplate">点击下载模板</a>
</div> </div>
<div class="dropzone" @dragover.prevent="onDragOver" @dragleave="onDragLeave" @drop="onDrop" @click="openRakutenUpload" :class="{ disabled: loading }"> <div class="dropzone" @dragover.prevent="onDragOver" @dragleave="onDragLeave" @drop="onDrop" @click="openRakutenUpload" :class="{ disabled: loading }">
<div class="dz-el-icon">📤</div> <div class="dz-el-icon">📤</div>
<div class="dz-text">点击或将文件拖拽到这里上传</div> <div class="dz-text">点击或将文件拖拽到这里上传</div>
<div class="dz-sub">支持扩展名.xls .xlsx .numbers</div> <div class="dz-sub">支持扩展名.xls .xlsx</div>
<div class="dz-sub">文件单列1/1</div> <div class="dz-sub">文件单列1/1</div>
</div> </div>
<input ref="uploadInputRef" style="display:none" type="file" accept=".xlsx,.xls" @change="handleExcelUpload" :disabled="loading"/> <input ref="uploadInputRef" style="display:none" type="file" accept=".xls,.xlsx" @change="handleExcelUpload" :disabled="loading"/>
<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>
@@ -357,7 +376,10 @@ onMounted(loadLatest)
<div class="title">获取数据</div> <div class="title">获取数据</div>
</div> </div>
<div class="desc">导入表格后点击下方按钮开始获取店铺商品数据</div> <div class="desc">导入表格后点击下方按钮开始获取店铺商品数据</div>
<el-button size="small" class="w100 btn-blue" :loading="loading" @click="handleStartSearch" :disabled="loading || (!pendingFile && allProducts.length === 0)">获取数据</el-button> <div class="action-buttons column">
<el-button size="small" class="w100 btn-blue" :loading="loading" @click="handleStartSearch" :disabled="loading || !pendingFile">获取数据</el-button>
<el-button size="small" class="w100" :disabled="!loading" @click="stopTask">停止获取</el-button>
</div>
</div> </div>
</div> </div>
@@ -380,6 +402,14 @@ onMounted(loadLatest)
<!-- 右侧主区域 --> <!-- 右侧主区域 -->
<section class="content-panel"> <section class="content-panel">
<el-dialog v-model="rakutenExampleVisible" title="示例 - 乐天店铺Excel格式" width="420px">
<el-table :data="rakutenExampleRows" size="small" border>
<el-table-column prop="shopName" label="店铺名" />
</el-table>
<template #footer>
<el-button type="primary" class="btn-blue" @click="rakutenExampleVisible = false">我知道了</el-button>
</template>
</el-dialog>
<!-- 数据显示区域 --> <!-- 数据显示区域 -->
<div class="table-container"> <div class="table-container">
<div class="table-section"> <div class="table-section">

View File

@@ -1,5 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, onMounted } from 'vue' import { ref, computed, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { zebraApi, type ZebraOrder, type BanmaAccount } from '../../api/zebra' import { zebraApi, type ZebraOrder, type BanmaAccount } from '../../api/zebra'
import AccountManager from '../common/AccountManager.vue' import AccountManager from '../common/AccountManager.vue'
@@ -49,7 +50,7 @@ function formatCny(v?: number) {
async function loadShops() { async function loadShops() {
try { try {
const resp = await zebraApi.getShops() const resp = await zebraApi.getShops({ accountId: Number(accountId.value) || undefined })
const list = (resp as any)?.data?.data?.list ?? (resp as any)?.list ?? [] const list = (resp as any)?.data?.data?.list ?? (resp as any)?.list ?? []
shopList.value = list shopList.value = list
} catch (e) { } catch (e) {
@@ -99,6 +100,7 @@ async function fetchPageData(startDate: string, endDate: string) {
try { try {
const data = await zebraApi.getOrders({ const data = await zebraApi.getOrders({
accountId: Number(accountId.value) || undefined,
startDate, startDate,
endDate, endDate,
page: fetchCurrentPage.value, page: fetchCurrentPage.value,
@@ -146,9 +148,9 @@ async function exportToExcel() {
exportLoading.value = true exportLoading.value = true
try { try {
const result = await zebraApi.exportAndSaveOrders({ orders: allOrderData.value }) const result = await zebraApi.exportAndSaveOrders({ orders: allOrderData.value })
alert(`Excel文件已保存到: ${result.filePath}`) ElMessage({ message: `Excel文件已保存到: ${result.filePath}` as any, type: 'success' })
} catch (e) { } catch (e: any) {
alert('导出Excel失败') ElMessage({ message: e?.message || '导出Excel失败', type: 'error' })
} finally { } finally {
exportLoading.value = false exportLoading.value = false
} }
@@ -190,28 +192,32 @@ function openManageAccount() {
} }
async function submitAccount() { async function submitAccount() {
if (!formUsername.value) { alert('请输入账号'); return } if (!formUsername.value) { ElMessage({ message: '请输入账号', type: 'warning' }); return }
const payload: BanmaAccount = { const payload: BanmaAccount = {
id: accountForm.value.id, id: accountForm.value.id,
name: accountForm.value.name || formUsername.value, name: accountForm.value.name || formUsername.value,
username: formUsername.value, username: formUsername.value,
password: formPassword.value || '',
isDefault: accountForm.value.isDefault || 0, isDefault: accountForm.value.isDefault || 0,
status: accountForm.value.status || 1, status: accountForm.value.status || 1,
} }
const { id } = await zebraApi.saveAccount(payload) try {
if (rememberPwd.value && formPassword.value) { const res = await zebraApi.saveAccount(payload)
localStorage.setItem(`banma:pwd:${formUsername.value}`, formPassword.value) const id = (res as any)?.data?.id || (res as any)?.id
} else { if (!id) throw new Error((res as any)?.msg || '保存失败')
localStorage.removeItem(`banma:pwd:${formUsername.value}`)
}
accountDialogVisible.value = false accountDialogVisible.value = false
await loadAccounts() await loadAccounts()
if (id) accountId.value = id if (id) accountId.value = id
} catch (e: any) {
ElMessage({ message: e?.message || '账号或密码错误无法获取Token', type: 'error' })
}
} }
async function removeCurrentAccount() { async function removeCurrentAccount() {
if (!isEditMode.value || !accountForm.value.id) return if (!isEditMode.value || !accountForm.value.id) return
if (!confirm('确认删除该账号?')) return try {
await ElMessageBox.confirm('确认删除该账号?', '提示', { type: 'warning' })
} catch { return }
await zebraApi.removeAccount(accountForm.value.id) await zebraApi.removeAccount(accountForm.value.id)
accountDialogVisible.value = false accountDialogVisible.value = false
await loadAccounts() await loadAccounts()
@@ -304,6 +310,16 @@ async function removeCurrentAccount() {
<div class="content"> <div class="content">
<!-- 数据表格无数据时也显示表头 --> <!-- 数据表格无数据时也显示表头 -->
<div class="table-section"> <div class="table-section">
<div v-if="showProgress" class="progress-section">
<div class="progress-box">
<div class="progress-container">
<div class="progress-bar">
<div class="progress-fill" :style="{ width: progressPercentage + '%' }"></div>
</div>
<div class="progress-text">{{ progressPercentage }}%</div>
</div>
</div>
</div>
<div class="table-wrapper"> <div class="table-wrapper">
<table class="table"> <table class="table">
<thead> <thead>
@@ -365,18 +381,14 @@ async function removeCurrentAccount() {
<div>加载中...</div> <div>加载中...</div>
</div> </div>
<div v-else class="empty-container"> <div v-else class="empty-container">
<div class="empty-icon">📄</div> <div class="empty-icon" style="font-size:48px;">📄</div>
<div class="empty-text">暂无数据请获取订单</div> <div class="empty-text">暂无数据请获取订单</div>
</div> </div>
</div> </div>
</div> </div>
<!-- 底部区域进度条 + 分页器 --> <!-- 底部区域分页器 -->
<div class="pagination-fixed"> <div class="pagination-fixed">
<div v-if="showProgress" class="progress-bottom">
<div class="progress-bar"><div class="progress-fill" :style="{ width: progressPercentage + '%' }"></div></div>
<div class="progress-text">{{ progressPercentage }}%</div>
</div>
<el-pagination <el-pagination
background background
:current-page="currentPage" :current-page="currentPage"
@@ -458,9 +470,10 @@ export default {
.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; }
.tip { color: #909399; font-size: 12px; margin-bottom: 8px; text-align: left; } .tip { color: #909399; font-size: 12px; margin-bottom: 8px; text-align: left; }
.avatar { width: 18px; height: 18px; border-radius: 50%; margin-right: 6px; vertical-align: -2px; } .avatar { width: 22px; height: 22px; border-radius: 50%; margin-right: 6px; vertical-align: -2px; }
.acct-text { vertical-align: middle; } .acct-text { vertical-align: middle; }
.acct-row { display: grid; grid-template-columns: 8px 18px 1fr auto; align-items: center; gap: 6px; width: 100%; } .acct-row { display: grid; grid-template-columns: 8px 18px 1fr auto; align-items: center; gap: 6px; width: 100%; }
.acct-text { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; font-size: 12px; }
.status-dot { width: 6px; height: 6px; border-radius: 50%; display: inline-block; } .status-dot { width: 6px; height: 6px; border-radius: 50%; display: inline-block; }
.status-dot.on { background: #22c55e; } .status-dot.on { background: #22c55e; }
.status-dot.off { background: #f87171; } .status-dot.off { background: #f87171; }
@@ -489,8 +502,8 @@ export default {
.table td { padding: 10px 8px; border-bottom: 1px solid #f0f0f0; vertical-align: middle; } .table td { padding: 10px 8px; border-bottom: 1px solid #f0f0f0; vertical-align: middle; }
.table tbody tr:hover { background: #f9f9f9; } .table tbody tr:hover { background: #f9f9f9; }
.truncate { max-width: 180px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .truncate { max-width: 180px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.image-container { display: flex; justify-content: center; align-items: center; width: 24px; height: 20px; margin: 0 auto; background: #f8f9fa; border-radius: 2px; } .image-container { display: flex; justify-content: center; align-items: center; width: 28px; height: 24px; margin: 0 auto; background: #f8f9fa; border-radius: 2px; }
.thumb { width: 16px; height: 16px; object-fit: contain; border-radius: 2px; } .thumb { width: 22px; height: 22px; object-fit: contain; border-radius: 2px; }
.price-tag { color: #e6a23c; font-weight: bold; } .price-tag { color: #e6a23c; font-weight: bold; }
.fee-tag { color: #909399; font-weight: 500; } .fee-tag { color: #909399; font-weight: 500; }
.table-loading { position: absolute; inset: 0; background: rgba(255, 255, 255, 0.95); display: flex; flex-direction: column; justify-content: center; align-items: center; font-size: 14px; color: #606266; } .table-loading { position: absolute; inset: 0; background: rgba(255, 255, 255, 0.95); display: flex; flex-direction: column; justify-content: center; align-items: center; font-size: 14px; color: #606266; }
@@ -499,10 +512,12 @@ export default {
.pagination-fixed { position: sticky; bottom: 0; z-index: 2; padding: 8px 12px; background: #f9f9f9; border-radius: 4px; display: flex; justify-content: center; border-top: 1px solid #ebeef5; margin-top: 8px; } .pagination-fixed { position: sticky; bottom: 0; z-index: 2; padding: 8px 12px; background: #f9f9f9; border-radius: 4px; display: flex; justify-content: center; border-top: 1px solid #ebeef5; margin-top: 8px; }
.tag { display: inline-block; padding: 0 6px; margin-left: 6px; font-size: 12px; background: #ecf5ff; color: #409EFF; border-radius: 3px; } .tag { display: inline-block; padding: 0 6px; margin-left: 6px; font-size: 12px; background: #ecf5ff; color: #409EFF; border-radius: 3px; }
.empty-abs { position: absolute; left: 0; right: 0; top: 48px; bottom: 0; display: flex; align-items: center; justify-content: center; pointer-events: none; } .empty-abs { position: absolute; left: 0; right: 0; top: 48px; bottom: 0; display: flex; align-items: center; justify-content: center; pointer-events: none; }
.progress-bottom { display: flex; align-items: center; gap: 8px; margin-right: auto; } .progress-section { margin: 0px 12px 0px 12px; }
.progress-bottom .progress-bar { width: 100%; height: 6px; background: #e3eeff; border-radius: 999px; overflow: hidden; } .progress-box { padding: 4px 0; }
.progress-bottom .progress-fill { height: 100%; background: #1677FF; border-radius: 999px; transition: width 0.3s ease; } .progress-container { display: flex; align-items: center; gap: 8px; }
.progress-bottom .progress-text { font-size: 13px; color: #1677FF; font-weight: 500; min-width: 44px; text-align: right; } .progress-bar { flex: 1; height: 6px; background: #e3eeff; border-radius: 999px; overflow: hidden; }
.progress-fill { height: 100%; background: #1677FF; border-radius: 999px; transition: width 0.3s ease; }
.progress-text { font-size: 13px; color: #1677FF; font-weight: 500; min-width: 44px; text-align: right; }
</style> </style>

View File

@@ -2,7 +2,7 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<title>Vite + Vue template</title> <title>erpClient</title>
<link rel="icon" href="/icon/icon.png"> <link rel="icon" href="/icon/icon.png">
<meta name="theme-color" content="#ffffff"> <meta name="theme-color" content="#ffffff">
</head> </head>

View File

@@ -25,6 +25,8 @@ import java.time.ZoneOffset;
import java.util.*; import java.util.*;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.Base64; import java.util.Base64;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@RestController @RestController
@RequestMapping("/api/rakuten") @RequestMapping("/api/rakuten")
@@ -40,24 +42,21 @@ public class RakutenController {
private JavaBridge javaBridge; private JavaBridge javaBridge;
@Autowired @Autowired
private DataReportUtil dataReportUtil; private DataReportUtil dataReportUtil;
/** /**
* 获取乐天商品数据(支持单个店铺名或 Excel 文件上传) * 获取乐天商品数据
* *
* @param file 可选,Excel 文件(首列为店铺名) * @param file Excel文件首列为店铺名
* @param shopName 可选,单个店铺名
* @param batchId 可选,批次号 * @param batchId 可选,批次号
* @return JsonData 响应 * @return JsonData 响应
*/ */
@PostMapping(value = "/products") @PostMapping(value = "/products")
public JsonData getProducts(@RequestParam(value = "file", required = false) MultipartFile file, @RequestParam(value = "shopName", required = false) String shopName, @RequestParam(value = "batchId", required = false) String batchId) { public JsonData getProducts(@RequestParam("file") MultipartFile file, @RequestParam(value = "batchId", required = false) String batchId) {
try { try {
// 1. 获取店铺名集合(优先 shopName其次 Excel List<String> shopNames = ExcelParseUtil.parseFirstColumn(file);
List<String> shopNames = Optional.ofNullable(shopName).filter(s -> !s.trim().isEmpty()).map(s -> List.of(s.trim())).orElseGet(() -> file != null ? ExcelParseUtil.parseFirstColumn(file) : new ArrayList<>());
if (CollectionUtils.isEmpty(shopNames)) { if (CollectionUtils.isEmpty(shopNames)) {
return JsonData.buildError("未从 Excel解析到店铺名,且 shopName 参数为空"); return JsonData.buildError("Excel文件中未解析到店铺名");
} }
List<RakutenProduct> allProducts = new ArrayList<>(); List<RakutenProduct> allProducts = new ArrayList<>();
List<String> skippedShops = new ArrayList<>(); List<String> skippedShops = new ArrayList<>();
@@ -89,10 +88,7 @@ public class RakutenController {
dataReportUtil.reportDataCollection("RAKUTEN_CACHE", cachedCount, "0"); dataReportUtil.reportDataCollection("RAKUTEN_CACHE", cachedCount, "0");
} }
// 5. 如果是单店铺查询,只返回该店铺的商品 return JsonData.buildSuccess(Map.of("products", allProducts, "total", allProducts.size(), "sessionId", batchId, "skippedShops", skippedShops, "newProductsCount", newProducts.size()));
List<RakutenProduct> finalProducts = (shopName != null && !shopName.trim().isEmpty()) ? allProducts.stream().filter(p -> shopName.trim().equals(p.getOriginalShopName())).toList() : allProducts;
return JsonData.buildSuccess(Map.of("products", finalProducts, "total", finalProducts.size(), "sessionId", batchId, "skippedShops", skippedShops, "newProductsCount", newProducts.size()));
} catch (Exception e) { } catch (Exception e) {
log.error("获取乐天商品失败", e); log.error("获取乐天商品失败", e);
return JsonData.buildError("获取乐天商品失败: " + e.getMessage()); return JsonData.buildError("获取乐天商品失败: " + e.getMessage());
@@ -129,6 +125,7 @@ public class RakutenController {
return JsonData.buildError("获取最新数据失败: " + e.getMessage()); return JsonData.buildError("获取最新数据失败: " + e.getMessage());
} }
} }
@PostMapping("/export-and-save") @PostMapping("/export-and-save")
public JsonData exportAndSave(@RequestBody Map<String, Object> body) { public JsonData exportAndSave(@RequestBody Map<String, Object> body) {
try { try {
@@ -138,8 +135,52 @@ public class RakutenController {
boolean skipImages = Optional.ofNullable((Boolean) body.get("skipImages")).orElse(false); boolean skipImages = Optional.ofNullable((Boolean) body.get("skipImages")).orElse(false);
String fileName = Optional.ofNullable((String) body.get("fileName")).filter(name -> !name.trim().isEmpty()).orElse("乐天商品数据_" + java.time.LocalDate.now() + ".xlsx"); String fileName = Optional.ofNullable((String) body.get("fileName")).filter(name -> !name.trim().isEmpty()).orElse("乐天商品数据_" + java.time.LocalDate.now() + ".xlsx");
String[] headers = {"店铺名", "商品图片", "商品链接", "排名", "商品标题", "价格", "1688识图链接", "1688价格", "1688重量"}; // 构建与前端表格一致的列顺序与字段
byte[] excelData = com.tashow.erp.utils.ExcelExportUtil.createExcelWithImages("乐天商品数据", headers, products, skipImages ? -1 : 1, skipImages ? null : "imgUrl"); String[] headers = {
"店铺名",
"商品链接",
"商品图片",
"排名",
"商品标题",
"价格",
"1688识图链接",
"1688运费",
"1688中位价",
"1688最低价",
"1688中间价",
"1688最高价"
};
List<Map<String, Object>> rows = new ArrayList<>();
for (Map<String, Object> p : products) {
LinkedHashMap<String, Object> row = new LinkedHashMap<>();
List<Double> priceList = parseSkuPriceList(p.get("skuPriceJson"), p.get("skuPrice"));
Double minPrice = priceList.isEmpty() ? null : priceList.get(0);
Double midPrice = priceList.isEmpty() ? null : priceList.get(priceList.size() / 2);
Double maxPrice = priceList.isEmpty() ? null : priceList.get(priceList.size() - 1);
row.put("店铺名", p.get("originalShopName"));
row.put("商品链接", p.get("productUrl"));
row.put("商品图片", p.get("imgUrl"));
row.put("排名", p.get("ranking"));
row.put("商品标题", p.get("productTitle"));
row.put("价格", p.get("price"));
row.put("1688识图链接", p.get("mapRecognitionLink"));
row.put("1688运费", p.get("freight"));
row.put("1688中位价", p.get("median"));
row.put("1688最低价", minPrice);
row.put("1688中间价", midPrice);
row.put("1688最高价", maxPrice);
rows.add(row);
}
byte[] excelData = com.tashow.erp.utils.ExcelExportUtil.createExcelWithImages(
"乐天商品数据",
headers,
rows,
skipImages ? -1 : 1,
skipImages ? null : "商品图片"
);
if (excelData == null || excelData.length == 0) return JsonData.buildError("生成Excel失败"); if (excelData == null || excelData.length == 0) return JsonData.buildError("生成Excel失败");
@@ -155,5 +196,24 @@ public class RakutenController {
} }
} }
// 解析 skuPriceJson 或 skuPrice 字段中的价格键,返回从小到大排序的价格列表
private static List<Double> parseSkuPriceList(Object skuPriceJson, Object skuPrice) {
String src = skuPriceJson != null ? String.valueOf(skuPriceJson) : (skuPrice != null ? String.valueOf(skuPrice) : null);
if (src == null || src.isEmpty()) return Collections.emptyList();
try {
Pattern pattern = Pattern.compile("(\\d+(?:\\.\\d+)?)\\s*:");
Matcher m = pattern.matcher(src);
List<Double> prices = new ArrayList<>();
while (m.find()) {
String num = m.group(1);
try { prices.add(Double.parseDouble(num)); } catch (NumberFormatException ignored) {}
}
Collections.sort(prices);
return prices;
} catch (Exception ignored) {
return Collections.emptyList();
}
}
} }

View File

@@ -1,7 +1,7 @@
package com.tashow.erp.service; package com.tashow.erp.service;
import com.tashow.erp.entity.AmazonProductEntity;
import java.util.List; import java.util.List;
import java.util.Map;
/** /**
* 亚马逊数据采集服务接口 * 亚马逊数据采集服务接口
@@ -17,7 +17,7 @@ public interface IAmazonScrapingService {
* @param batchId 批次ID * @param batchId 批次ID
* @return 产品信息列表 * @return 产品信息列表
*/ */
Map<String, Object> batchGetProductInfo(List<String> asinList, String batchId); List<AmazonProductEntity> batchGetProductInfo(List<String> asinList, String batchId);

View File

@@ -10,16 +10,13 @@ import java.util.Map;
*/ */
public interface IBanmaOrderService { public interface IBanmaOrderService {
/** // 客户端不再暴露刷新认证Token
* 刷新认证Token
*/
void refreshToken();
/** /**
* 获取店铺列表 * 获取店铺列表
* @return 店铺列表数据 * @return 店铺列表数据
*/ */
Map<String, Object> getShops(); Map<String, Object> getShops(Long accountId);
/** /**
* 分页获取订单数据支持batchId * 分页获取订单数据支持batchId
@@ -31,5 +28,5 @@ public interface IBanmaOrderService {
* @param shopIds 店铺ID列表 * @param shopIds 店铺ID列表
* @return 订单数据列表和总数 * @return 订单数据列表和总数
*/ */
Map<String, Object> getOrdersByPage(String startDate, String endDate, int page, int pageSize, String batchId, List<String> shopIds); Map<String, Object> getOrdersByPage(Long accountId, String startDate, String endDate, int page, int pageSize, String batchId, List<String> shopIds);
} }

View File

@@ -1,11 +1,11 @@
package com.tashow.erp.service.impl; package com.tashow.erp.service.impl;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import com.tashow.erp.entity.BanmaOrderEntity; import com.tashow.erp.entity.BanmaOrderEntity;
import com.tashow.erp.repository.BanmaOrderRepository; import com.tashow.erp.repository.BanmaOrderRepository;
import com.tashow.erp.service.ICacheService; import com.tashow.erp.service.ICacheService;
import com.tashow.erp.service.IBanmaOrderService; import com.tashow.erp.service.IBanmaOrderService;
import com.tashow.erp.utils.DataReportUtil; import com.tashow.erp.utils.DataReportUtil;
import com.tashow.erp.utils.ErrorReporter;
import com.tashow.erp.utils.LoggerUtil; import com.tashow.erp.utils.LoggerUtil;
import com.tashow.erp.utils.SagawaExpressSdk; import com.tashow.erp.utils.SagawaExpressSdk;
import com.tashow.erp.utils.StringUtils; import com.tashow.erp.utils.StringUtils;
@@ -14,15 +14,13 @@ import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.http.*; import org.springframework.http.*;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate; import org.springframework.web.client.RestTemplate;
import java.time.Duration; import java.time.Duration;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.*; import java.util.*;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors; import java.util.stream.Collectors;
/** /**
* 斑马订单服务实现类 - 极简版 * 斑马订单服务实现类
* 所有功能统一到核心方法,彻底消除代码分散
* *
* @author ruoyi * @author ruoyi
*/ */
@@ -30,76 +28,74 @@ import java.util.stream.Collectors;
public class BanmaOrderServiceImpl implements IBanmaOrderService { public class BanmaOrderServiceImpl implements IBanmaOrderService {
private static final Logger logger = LoggerUtil.getLogger(BanmaOrderServiceImpl.class); private static final Logger logger = LoggerUtil.getLogger(BanmaOrderServiceImpl.class);
private static final String SERVICE_NAME = "banma"; private static final String SERVICE_NAME = "banma";
private static final String LOGIN_URL = "https://banma365.cn/api/login"; private static final String RUOYI_ADMIN_BASE = "http://127.0.0.1:8080";
private static final String LOGIN_USERNAME = "大赢家网络科技(主账号)";
private static final String LOGIN_PASSWORD = "banma123456";
private static final String API_URL = "https://banma365.cn/api/order/list?%srecipientName=&page=%d&size=%d&markFlag=0&state=4&_t=%d"; private static final String API_URL = "https://banma365.cn/api/order/list?%srecipientName=&page=%d&size=%d&markFlag=0&state=4&_t=%d";
private static final String API_URL_WITH_TIME = "https://banma365.cn/api/order/list?%srecipientName=&page=%d&size=%d&markFlag=0&state=4&orderedAtStart=%s&orderedAtEnd=%s&_t=%d"; private static final String API_URL_WITH_TIME = "https://banma365.cn/api/order/list?%srecipientName=&page=%d&size=%d&markFlag=0&state=4&orderedAtStart=%s&orderedAtEnd=%s&_t=%d";
private static final String TRACKING_URL = "https://banma365.cn/zebraExpressHub/web/tracking/getByExpressNumber/%s"; private static final String TRACKING_URL = "https://banma365.cn/zebraExpressHub/web/tracking/getByExpressNumber/%s";
private static final long TOKEN_EXPIRE_TIME = 24 * 60 * 60 * 1000;
private RestTemplate restTemplate; private RestTemplate restTemplate;
private final ObjectMapper objectMapper = new ObjectMapper(); private final ObjectMapper objectMapper = new ObjectMapper();
private final ICacheService cacheService; private final ICacheService cacheService;
private final BanmaOrderRepository banmaOrderRepository; private final BanmaOrderRepository banmaOrderRepository;
private final DataReportUtil dataReportUtil; private final DataReportUtil dataReportUtil;
private final ErrorReporter errorReporter;
private String currentAuthToken; private String currentAuthToken;
private Long currentAccountId;
// 当前批量采集的sessionId // 当前批量采集的sessionId
private String currentBatchSessionId = null; private String currentBatchSessionId = null;
// 物流信息缓存,避免重复查询 // 物流信息缓存,避免重复查询
private final Map<String, String> trackingInfoCache = new ConcurrentHashMap<>(); private final Map<String, String> trackingInfoCache = new ConcurrentHashMap<>();
public BanmaOrderServiceImpl(BanmaOrderRepository banmaOrderRepository, ICacheService cacheService, DataReportUtil dataReportUtil) { public BanmaOrderServiceImpl(BanmaOrderRepository banmaOrderRepository, ICacheService cacheService, DataReportUtil dataReportUtil, ErrorReporter errorReporter) {
this.banmaOrderRepository = banmaOrderRepository; this.banmaOrderRepository = banmaOrderRepository;
this.cacheService = cacheService; this.cacheService = cacheService;
this.dataReportUtil = dataReportUtil; this.dataReportUtil = dataReportUtil;
this.errorReporter = errorReporter;
RestTemplateBuilder builder = new RestTemplateBuilder(); RestTemplateBuilder builder = new RestTemplateBuilder();
builder.connectTimeout(Duration.ofSeconds(5)); builder.connectTimeout(Duration.ofSeconds(5));
builder.readTimeout(Duration.ofSeconds(10)); builder.readTimeout(Duration.ofSeconds(10));
restTemplate = builder.build(); restTemplate = builder.build();
initializeAuthToken();
} }
/**
* 初始化认证令牌
*/
private void initializeAuthToken() {
refreshToken();
/* currentAuthToken = cacheService.getAuthToken(SERVICE_NAME);
if (currentAuthToken == null) {
@SuppressWarnings("unchecked")
private void fetchTokenFromServer(Long accountId) {
ResponseEntity<Map> resp = restTemplate.getForEntity(RUOYI_ADMIN_BASE + "/tool/banma/accounts", Map.class);
Object body = resp.getBody();
if (body == null) return;
Object data = ((Map<String, Object>) body).get("data");
List<Map<String, Object>> list;
if (data instanceof List) {
list = (List<Map<String, Object>>) data;
} else if (body instanceof Map && ((Map) body).get("list") instanceof List) {
list = (List<Map<String, Object>>) ((Map) body).get("list");
} else { } else {
logger.info("从缓存加载斑马认证令牌成功"); return;
}*/ }
if (list.isEmpty()) return;
Map<String, Object> picked;
if (accountId != null) {
picked = list.stream().filter(m -> Objects.equals(((Number) m.get("id")).longValue(), accountId)).findFirst().orElse(null);
if (picked == null) return;
} else {
picked = list.stream()
.filter(m -> Objects.equals(((Number) m.getOrDefault("status", 1)).intValue(), 1))
.sorted((a,b) -> Integer.compare(((Number) b.getOrDefault("isDefault", 0)).intValue(), ((Number) a.getOrDefault("isDefault", 0)).intValue()))
.findFirst().orElse(list.get(0));
}
Object token = picked.get("token");
if (token instanceof String && !((String) token).isEmpty()) {
String t = (String) token;
currentAuthToken = t.startsWith("Bearer ") ? t : ("Bearer " + t);
currentAccountId = accountId;
} }
/**
* 刷新斑马认证令牌
*/
@Override
public void refreshToken() {
Map<String, String> params = new HashMap<>();
params.put("username", LOGIN_USERNAME);
params.put("password", LOGIN_PASSWORD);
HttpHeaders headers = new HttpHeaders();
headers.set("Content-Type", "application/json");
ResponseEntity<Map> response = restTemplate.postForEntity(LOGIN_URL, new HttpEntity<>(params, headers), Map.class);
Optional.ofNullable(response.getBody())
.filter(body -> Integer.valueOf(0).equals(body.get("code")))
.map(body -> (Map<String, Object>) body.get("data"))
.map(data -> (String) data.get("token"))
.filter(StringUtils::isNotEmpty)
.ifPresent(token -> {
currentAuthToken = "Bearer " + token;
cacheService.saveAuthToken(SERVICE_NAME, currentAuthToken, System.currentTimeMillis() + TOKEN_EXPIRE_TIME);
});
} }
/** /**
* 获取店铺列表 * 获取店铺列表
*/ */
@Override @Override
public Map<String, Object> getShops() { public Map<String, Object> getShops(Long accountId) {
fetchTokenFromServer(accountId);
HttpHeaders headers = new HttpHeaders(); HttpHeaders headers = new HttpHeaders();
headers.set("Authorization", currentAuthToken); if (currentAuthToken != null) headers.set("Authorization", currentAuthToken);
HttpEntity<String> httpEntity = new HttpEntity<>(headers); HttpEntity<String> httpEntity = new HttpEntity<>(headers);
String url = "https://banma365.cn/api/shop/list?_t=" + System.currentTimeMillis(); String url = "https://banma365.cn/api/shop/list?_t=" + System.currentTimeMillis();
@@ -112,13 +108,14 @@ public class BanmaOrderServiceImpl implements IBanmaOrderService {
* 分页获取订单数据 * 分页获取订单数据
*/ */
@Override @Override
public Map<String, Object> getOrdersByPage(String startDate, String endDate, int page, int pageSize, String batchId, List<String> shopIds) { public Map<String, Object> getOrdersByPage(Long accountId, String startDate, String endDate, int page, int pageSize, String batchId, List<String> shopIds) {
if (page == 1) { if (page == 1) {
currentBatchSessionId = batchId; currentBatchSessionId = batchId;
trackingInfoCache.clear(); trackingInfoCache.clear();
} }
fetchTokenFromServer(accountId);
HttpHeaders headers = new HttpHeaders(); HttpHeaders headers = new HttpHeaders();
headers.set("Authorization", currentAuthToken); if (currentAuthToken != null) headers.set("Authorization", currentAuthToken);
HttpEntity<String> httpEntity = new HttpEntity<>(headers); HttpEntity<String> httpEntity = new HttpEntity<>(headers);
String shopIdsParam = ""; String shopIdsParam = "";
@@ -138,7 +135,7 @@ public class BanmaOrderServiceImpl implements IBanmaOrderService {
if (response.getBody() == null || !Integer.valueOf(0).equals(response.getBody().get("code"))) { if (response.getBody() == null || !Integer.valueOf(0).equals(response.getBody().get("code"))) {
Map<String, Object> errorResult = new HashMap<>(); Map<String, Object> errorResult = new HashMap<>();
errorResult.put("success", false); errorResult.put("success", false);
errorResult.put("message", "获取订单数据失败,请点击'刷新认证'按钮重试"); errorResult.put("message", "获取订单数据失败,请稍后重试或联系管理员刷新认证");
return errorResult; return errorResult;
} }
@@ -209,10 +206,15 @@ public class BanmaOrderServiceImpl implements IBanmaOrderService {
BanmaOrderEntity entity = new BanmaOrderEntity(); BanmaOrderEntity entity = new BanmaOrderEntity();
String entityTrackingNumber = (String) result.get("internationalTrackingNumber"); String entityTrackingNumber = (String) result.get("internationalTrackingNumber");
if (StringUtils.isEmpty(entityTrackingNumber)) {
String shopOrderNumber = (String) result.get("shopOrderNumber"); String shopOrderNumber = (String) result.get("shopOrderNumber");
String productTitle = (String) result.get("productTitle"); String productTitle = (String) result.get("productTitle");
// 检查并上报空数据
if (StringUtils.isEmpty(entityTrackingNumber)) errorReporter.reportDataEmpty("banma", String.valueOf(result.get("id")), entityTrackingNumber);
if (StringUtils.isEmpty(shopOrderNumber)) errorReporter.reportDataEmpty("banma", String.valueOf(result.get("id")), shopOrderNumber);
if (StringUtils.isEmpty(productTitle)) errorReporter.reportDataEmpty("banma", String.valueOf(result.get("id")), productTitle);
if (StringUtils.isEmpty(entityTrackingNumber)) {
if (StringUtils.isNotEmpty(shopOrderNumber)) { if (StringUtils.isNotEmpty(shopOrderNumber)) {
entityTrackingNumber = "ORDER_" + shopOrderNumber; entityTrackingNumber = "ORDER_" + shopOrderNumber;
} else if (StringUtils.isNotEmpty(productTitle)) { } else if (StringUtils.isNotEmpty(productTitle)) {

View File

@@ -9,6 +9,7 @@ import com.tashow.erp.model.RakutenProduct;
import com.tashow.erp.model.SearchResult; import com.tashow.erp.model.SearchResult;
import com.tashow.erp.repository.RakutenProductRepository; import com.tashow.erp.repository.RakutenProductRepository;
import com.tashow.erp.service.RakutenScrapingService; import com.tashow.erp.service.RakutenScrapingService;
import com.tashow.erp.utils.ErrorReporter;
import com.tashow.erp.utils.RakutenProxyUtil; import com.tashow.erp.utils.RakutenProxyUtil;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
@@ -36,6 +37,8 @@ public class RakutenScrapingServiceImpl implements RakutenScrapingService {
@Autowired @Autowired
private RakutenProductRepository rakutenProductRepository; private RakutenProductRepository rakutenProductRepository;
@Autowired @Autowired
private ErrorReporter errorReporter;
@Autowired
ObjectMapper objectMapper; ObjectMapper objectMapper;
/** /**
@@ -46,7 +49,7 @@ public class RakutenScrapingServiceImpl implements RakutenScrapingService {
String url = "https://ranking.rakuten.co.jp/search?stx=" + URLEncoder.encode(shopName, StandardCharsets.UTF_8); String url = "https://ranking.rakuten.co.jp/search?stx=" + URLEncoder.encode(shopName, StandardCharsets.UTF_8);
List<RakutenProduct> products = new ArrayList<>(); List<RakutenProduct> products = new ArrayList<>();
Spider spider = Spider.create(new RakutenPageProcessor(products)).addUrl(url).setDownloader(new RakutenProxyUtil().createProxyDownloader(new RakutenProxyUtil().detectSystemProxy(url))).thread(1); Spider spider = Spider.create(new RakutenPageProcessor(products, errorReporter)).addUrl(url).setDownloader(new RakutenProxyUtil().createProxyDownloader(new RakutenProxyUtil().detectSystemProxy(url))).thread(1);
spider.run(); spider.run();
log.info("采集完成,店铺: {},数量: {}", shopName, products.size()); log.info("采集完成,店铺: {},数量: {}", shopName, products.size());
@@ -55,9 +58,11 @@ public class RakutenScrapingServiceImpl implements RakutenScrapingService {
private class RakutenPageProcessor implements PageProcessor { private class RakutenPageProcessor implements PageProcessor {
private final List<RakutenProduct> products; private final List<RakutenProduct> products;
private final ErrorReporter errorReporter;
RakutenPageProcessor(List<RakutenProduct> products) { RakutenPageProcessor(List<RakutenProduct> products, ErrorReporter errorReporter) {
this.products = products; this.products = products;
this.errorReporter = errorReporter;
} }
@Override @Override
@@ -83,6 +88,11 @@ public class RakutenScrapingServiceImpl implements RakutenScrapingService {
product.setOriginalShopName(shopName); // 设置原始店铺名称 product.setOriginalShopName(shopName); // 设置原始店铺名称
product.setImgUrl(imageUrls.get(i)); product.setImgUrl(imageUrls.get(i));
String title = titles.get(i).trim(); String title = titles.get(i).trim();
// 检查并上报空数据
if (title == null || title.trim().isEmpty()) errorReporter.reportDataEmpty("rakuten", productUrl, title);
if (prices.get(i) == null || prices.get(i).replaceAll("[^0-9]", "").isEmpty()) errorReporter.reportDataEmpty("rakuten", productUrl, prices.get(i));
if (shopName == null || shopName.isEmpty()) errorReporter.reportDataEmpty("rakuten", productUrl, shopName);
product.setProductTitle(title); product.setProductTitle(title);
product.setProductName(title); product.setProductName(title);
product.setPrice(prices.get(i).replaceAll("[^0-9]", "")); product.setPrice(prices.get(i).replaceAll("[^0-9]", ""));

View File

@@ -97,6 +97,14 @@ public class ErrorReporter {
ex.printStackTrace(pw); ex.printStackTrace(pw);
return sw.toString(); return sw.toString();
} }
/**
* 上报数据为空异常
*/
public void reportDataEmpty(String serviceName, String dataId, Object data) {
String message = String.format("数据为空 - ID: %s, 数据: %s", dataId, data);
reportError("DATA_EMPTY", serviceName + " - " + message, new RuntimeException(message));
}
/** /**
* 设置全局异常处理器 * 设置全局异常处理器
*/ */

View File

@@ -4,6 +4,7 @@ import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.scheduling.annotation.EnableAsync; import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.annotation.EnableScheduling;
/** /**
* 启动程序 * 启动程序
@@ -11,6 +12,7 @@ import org.springframework.scheduling.annotation.EnableAsync;
* @author ruoyi * @author ruoyi
*/ */
@SpringBootApplication(exclude = { DataSourceAutoConfiguration.class }) @SpringBootApplication(exclude = { DataSourceAutoConfiguration.class })
@EnableScheduling
public class RuoYiApplication public class RuoYiApplication
{ {
public static void main(String[] args) public static void main(String[] args)