From 17b6a7b9f9933180fa6d6053c03b20222a5aa716 Mon Sep 17 00:00:00 2001 From: zhangzijienbplus <17738440858@163.com> Date: Wed, 22 Oct 2025 09:51:55 +0800 Subject: [PATCH] =?UTF-8?q?feat(device):=20=E5=AE=9E=E7=8E=B0=E8=AE=BE?= =?UTF-8?q?=E5=A4=87=E4=B8=8E=E8=B4=A6=E5=8F=B7=E7=BB=91=E5=AE=9A=E7=AE=A1?= =?UTF-8?q?=E7=90=86=E6=9C=BA=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 引入 ClientAccountDevice 表管理设备与账号绑定关系 - 重构设备注册逻辑,支持多账号绑定同一设备 - 新增设备配额检查,基于账号维度限制设备数量 -优化设备移除逻辑,仅解除绑定而非物理删除- 改进设备列表查询,通过账号ID关联获取设备信息 - 更新心跳任务,支持向设备绑定的所有账号发送心跳 - 调整设备API参数,增加username字段用于权限校验 -修复HTTP请求编码问题,统一使用UTF-8字符集 - 增强错误处理,携带错误码信息便于前端识别 - 移除设备表中的username字段,解耦设备与用户名关联 --- electron-vue-template/src/main/main.ts | 38 +++- electron-vue-template/src/renderer/App.vue | 23 ++- .../src/renderer/api/device.ts | 2 +- .../src/renderer/api/http.ts | 10 +- .../src/renderer/api/zebra.ts | 4 + .../components/amazon/AmazonDashboard.vue | 21 +- .../renderer/components/auth/LoginDialog.vue | 11 +- .../components/common/AccountManager.vue | 14 +- .../components/common/SettingsDialog.vue | 2 +- .../components/rakuten/RakutenDashboard.vue | 29 +-- .../components/zebra/ZebraDashboard.vue | 14 +- .../src/renderer/composables/useFileDrop.ts | 64 ++++++ erp_client_sb/pom.xml | 2 +- .../service/impl/Alibaba1688ServiceImpl.java | 34 +-- .../monitor/ClientAccountController.java | 46 ++--- .../system/ClientDeviceController.java | 195 ++++++++---------- .../controller/tool/BanmaOrderController.java | 25 +++ .../java/com/ruoyi/web/sse/SseHubService.java | 7 - .../ruoyi/web/task/DeviceHeartbeatTask.java | 19 +- .../src/main/resources/application.yml | 7 + .../system/domain/ClientAccountDevice.java | 61 ++++++ .../com/ruoyi/system/domain/ClientDevice.java | 4 - .../mapper/ClientAccountDeviceMapper.java | 48 +++++ .../system/mapper/ClientDeviceMapper.java | 4 - .../service/impl/BanmaAccountServiceImpl.java | 25 +++ .../system/ClientAccountDeviceMapper.xml | 87 ++++++++ .../mapper/system/ClientDeviceMapper.xml | 41 +--- .../mapper/system/ClientMonitorMapper.xml | 9 +- sql/banma_account.sql | 20 -- 29 files changed, 589 insertions(+), 277 deletions(-) create mode 100644 electron-vue-template/src/renderer/composables/useFileDrop.ts create mode 100644 ruoyi-system/src/main/java/com/ruoyi/system/domain/ClientAccountDevice.java create mode 100644 ruoyi-system/src/main/java/com/ruoyi/system/mapper/ClientAccountDeviceMapper.java create mode 100644 ruoyi-system/src/main/resources/mapper/system/ClientAccountDeviceMapper.xml delete mode 100644 sql/banma_account.sql diff --git a/electron-vue-template/src/main/main.ts b/electron-vue-template/src/main/main.ts index 4a730c6..26d0c7a 100644 --- a/electron-vue-template/src/main/main.ts +++ b/electron-vue-template/src/main/main.ts @@ -213,7 +213,7 @@ function startSpringBoot() { } } -// startSpringBoot(); +startSpringBoot(); function stopSpringBoot() { if (!springProcess) return; @@ -253,12 +253,23 @@ function createWindow() { Menu.setApplicationMenu(null); mainWindow.setMenuBarVisibility(false); + // 阻止默认的文件拖拽导航行为,让渲染进程的 JavaScript 处理拖拽上传 + mainWindow.webContents.on('will-navigate', (event, url) => { + // 允许开发模式下的热重载导航 + if (isDev && url.startsWith('http://localhost')) return; + // 阻止所有其他导航(包括拖拽文件) + event.preventDefault(); + }); + + // 同样阻止新窗口的文件拖拽 + mainWindow.webContents.setWindowOpenHandler(() => ({ action: 'deny' })); + // 拦截关闭事件 mainWindow.on('close', (event) => { if (isQuitting) return; const config = loadConfig(); - const closeAction = config.closeAction || 'tray'; + const closeAction = config.closeAction || 'quit'; if (closeAction === 'quit') { isQuitting = true; @@ -280,6 +291,21 @@ function createWindow() { } +// 单实例锁定 +const gotTheLock = app.requestSingleInstanceLock(); + +if (!gotTheLock) { + app.quit(); +} else { + app.on('second-instance', () => { + if (mainWindow) { + if (mainWindow.isMinimized()) mainWindow.restore(); + mainWindow.show(); + mainWindow.focus(); + } + }); +} + app.whenReady().then(() => { // 应用开机自启动配置 const config = loadConfig(); @@ -324,9 +350,9 @@ app.whenReady().then(() => { } } - setTimeout(() => { - openAppIfNotOpened(); - }, 2000); + // setTimeout(() => { + // openAppIfNotOpened(); + // }, 2000); app.on('activate', () => { if (mainWindow && !mainWindow.isDestroyed()) { @@ -727,7 +753,7 @@ ipcMain.handle('read-log-file', async (event, logDate: string) => { // 获取关闭行为配置 ipcMain.handle('get-close-action', () => { const config = loadConfig(); - return config.closeAction || 'tray'; + return config.closeAction; }); // 保存关闭行为配置 diff --git a/electron-vue-template/src/renderer/App.vue b/electron-vue-template/src/renderer/App.vue index d4b2f41..f537c10 100644 --- a/electron-vue-template/src/renderer/App.vue +++ b/electron-vue-template/src/renderer/App.vue @@ -415,6 +415,14 @@ async function openDeviceManager() { await fetchDeviceData() } +async function handleDeviceConflict(username: string) { + showAuthDialog.value = false + currentUsername.value = username + showDeviceDialog.value = true + ElMessage.warning('该设备已在其他位置登录,请先移除冲突的设备') + await fetchDeviceData() +} + function openSettings() { showSettingsDialog.value = true } @@ -470,6 +478,18 @@ onMounted(async () => { // 检查是否有待安装的更新 await checkPendingUpdate() + + // 全局阻止文件拖拽到窗口(避免意外打开文件) + // 只在指定的 dropzone 区域处理拖拽上传 + document.addEventListener('dragover', (e) => { + e.preventDefault() + e.stopPropagation() + }, false) + + document.addEventListener('drop', (e) => { + e.preventDefault() + e.stopPropagation() + }, false) }) async function checkPendingUpdate() { @@ -580,7 +600,8 @@ onUnmounted(() => { + @show-register="showRegisterDialog" + @device-conflict="handleDeviceConflict"/> (path: string, options: RequestInit & { signal?: AbortS cache: 'no-store', ...options, headers: { - 'Content-Type': 'application/json', + 'Content-Type': 'application/json;charset=UTF-8', ...(token ? { 'Authorization': `Bearer ${token}` } : {}), ...options.headers } @@ -54,7 +54,9 @@ async function request(path: string, options: RequestInit & { signal?: AbortS if (contentType.includes('application/json')) { const json: any = await res.json(); if (json.code !== undefined && json.code !== 0 && json.code !== 200) { - throw new Error(json.msg || '请求失败'); + const error: any = new Error(json.msg || '请求失败'); + error.code = json.code; + throw error; } return json as T; } @@ -98,7 +100,9 @@ export const http = { if (contentType.includes('application/json')) { const json: any = await res.json(); if (json.code !== undefined && json.code !== 0 && json.code !== 200) { - throw new Error(json.msg || '请求失败'); + const error: any = new Error(json.msg || '请求失败'); + error.code = json.code; + throw error; } return json as T; } diff --git a/electron-vue-template/src/renderer/api/zebra.ts b/electron-vue-template/src/renderer/api/zebra.ts index c2cf3e8..be2d990 100644 --- a/electron-vue-template/src/renderer/api/zebra.ts +++ b/electron-vue-template/src/renderer/api/zebra.ts @@ -5,6 +5,10 @@ export const zebraApi = { return http.get('/tool/banma/accounts', name ? { name } : undefined) }, + getAccountLimit(name?: string) { + return http.get('/tool/banma/account-limit', name ? { name } : undefined) + }, + saveAccount(body: any, name?: string) { const url = name ? `/tool/banma/accounts?name=${encodeURIComponent(name)}` : '/tool/banma/accounts' return http.post(url, body) diff --git a/electron-vue-template/src/renderer/components/amazon/AmazonDashboard.vue b/electron-vue-template/src/renderer/components/amazon/AmazonDashboard.vue index ebcbaff..1b8c2b4 100644 --- a/electron-vue-template/src/renderer/components/amazon/AmazonDashboard.vue +++ b/electron-vue-template/src/renderer/components/amazon/AmazonDashboard.vue @@ -4,6 +4,7 @@ import { ElMessage, ElMessageBox } from 'element-plus' import { amazonApi } from '../../api/amazon' import { systemApi } from '../../api/system' import { handlePlatformFileExport } from '../../utils/settings' +import { useFileDrop } from '../../composables/useFileDrop' const TrialExpiredDialog = defineAsyncComponent(() => import('../common/TrialExpiredDialog.vue')) @@ -28,7 +29,6 @@ let abortController: AbortController | null = null // 请求取消控制器 const currentPage = ref(1) const pageSize = ref(15) const amazonUpload = ref(null) -const dragActive = ref(false) // 试用期过期弹框 const showTrialExpiredDialog = ref(false) @@ -88,17 +88,12 @@ async function handleExcelUpload(e: Event) { input.value = '' } -function onDragOver(e: DragEvent) { e.preventDefault(); dragActive.value = true } -function onDragLeave() { dragActive.value = false } -async function onDrop(e: DragEvent) { - e.preventDefault() - dragActive.value = false - const file = e.dataTransfer?.files?.[0] - if (!file) return - const ok = /\.xlsx?$/i.test(file.name) - if (!ok) return showMessage('仅支持 .xls/.xlsx 文件', 'warning') - await processExcelFile(file) -} +// 拖拽上传 +const { dragActive, onDragEnter, onDragOver, onDragLeave, onDrop } = useFileDrop({ + accept: /\.xlsx?$/i, + onFile: processExcelFile, + onError: (msg) => showMessage(msg, 'warning') +}) // 批量获取产品信息 - 核心数据处理逻辑 async function batchGetProductInfo(asinList: string[]) { @@ -347,7 +342,7 @@ onMounted(async () => { | 点击下载模板 -
+
📤
点击或将文件拖拽到这里上传
支持 .xls .xlsx
diff --git a/electron-vue-template/src/renderer/components/auth/LoginDialog.vue b/electron-vue-template/src/renderer/components/auth/LoginDialog.vue index b5aa71a..90f69eb 100644 --- a/electron-vue-template/src/renderer/components/auth/LoginDialog.vue +++ b/electron-vue-template/src/renderer/components/auth/LoginDialog.vue @@ -13,6 +13,7 @@ interface Emits { (e: 'update:modelValue', value: boolean): void (e: 'loginSuccess', data: { token: string; permissions?: string; expireTime?: string; accountType?: string; deviceTrialExpired?: boolean }): void (e: 'showRegister'): void + (e: 'deviceConflict', username: string): void } const props = defineProps() @@ -49,8 +50,14 @@ async function handleAuth() { }) ElMessage.success('登录成功') resetForm() - } catch (err) { - ElMessage.error((err as Error).message) + } catch (err: any) { + // 设备冲突/数量达上限:触发设备管理 + if (err.code === 501 ) { + emit('deviceConflict', authForm.value.username) + resetForm() + } else { + ElMessage.error(err.message || '登录失败') + } } finally { authLoading.value = false } diff --git a/electron-vue-template/src/renderer/components/common/AccountManager.vue b/electron-vue-template/src/renderer/components/common/AccountManager.vue index d3e0f2a..2ebcfca 100644 --- a/electron-vue-template/src/renderer/components/common/AccountManager.vue +++ b/electron-vue-template/src/renderer/components/common/AccountManager.vue @@ -18,11 +18,17 @@ const PLATFORM_LABEL: Record = { } const accounts = ref([]) +const accountLimit = ref({ limit: 1, count: 0 }) async function load() { const username = getUsernameFromToken() - const res = await zebraApi.getAccounts(username) + const [res, limitRes] = await Promise.all([ + zebraApi.getAccounts(username), + zebraApi.getAccountLimit(username) + ]) const list = (res as any)?.data ?? res accounts.value = Array.isArray(list) ? list : [] + const limitData = (limitRes as any)?.data ?? limitRes + accountLimit.value = { limit: limitData?.limit ?? 1, count: limitData?.count ?? 0 } } // 暴露方法供父组件调用 @@ -74,10 +80,10 @@ export default defineComponent({ name: 'AccountManager' })
logo
-
在线账号管理(3/3)
+
在线账号管理({{ accountLimit.count }}/{{ accountLimit.limit }})
- 您当前订阅可同时托管3家 Shopee 店铺
- 如需扩增同时托管店铺数,请 升级订阅。 + 您当前订阅可同时托管{{ accountLimit.limit }}个斑马账号
+ 如需扩增账号数量,请 升级订阅
diff --git a/electron-vue-template/src/renderer/components/common/SettingsDialog.vue b/electron-vue-template/src/renderer/components/common/SettingsDialog.vue index 5b729fa..d496d49 100644 --- a/electron-vue-template/src/renderer/components/common/SettingsDialog.vue +++ b/electron-vue-template/src/renderer/components/common/SettingsDialog.vue @@ -281,7 +281,7 @@ async function loadLogDates() { async function loadCloseAction() { try { const action = await (window as any).electronAPI.getCloseAction() - closeAction.value = action || 'tray' + if (action) closeAction.value = action } catch (error) { console.warn('获取关闭行为配置失败:', error) } diff --git a/electron-vue-template/src/renderer/components/rakuten/RakutenDashboard.vue b/electron-vue-template/src/renderer/components/rakuten/RakutenDashboard.vue index 6b7d1eb..221c8d1 100644 --- a/electron-vue-template/src/renderer/components/rakuten/RakutenDashboard.vue +++ b/electron-vue-template/src/renderer/components/rakuten/RakutenDashboard.vue @@ -4,6 +4,7 @@ import { ElMessage, ElMessageBox } from 'element-plus' import {rakutenApi} from '../../api/rakuten' import { batchConvertImages } from '../../utils/imageProxy' import { handlePlatformFileExport } from '../../utils/settings' +import { useFileDrop } from '../../composables/useFileDrop' const TrialExpiredDialog = defineAsyncComponent(() => import('../common/TrialExpiredDialog.vue')) @@ -26,7 +27,6 @@ let abortController: AbortController | null = null const singleShopName = ref('') const currentBatchId = ref('') const uploadInputRef = ref(null) -const dragActive = ref(false) // 数据与分页 const allProducts = ref([]) @@ -223,15 +223,12 @@ async function handleExcelUpload(e: Event) { input.value = '' } -function onDragOver(e: DragEvent) { e.preventDefault(); dragActive.value = true } -function onDragLeave() { dragActive.value = false } -async function onDrop(e: DragEvent) { - e.preventDefault() - dragActive.value = false - const file = e.dataTransfer?.files?.[0] - if (!file) return - await processFile(file) -} +// 拖拽上传 +const { dragActive, onDragEnter, onDragOver, onDragLeave, onDrop } = useFileDrop({ + accept: /\.xlsx?$/i, + onFile: processFile, + onError: (msg) => ElMessage({ message: msg, type: 'warning' }) +}) // 点击"获取数据 @@ -358,7 +355,6 @@ function delay(ms: number) { } function nextTickSafe() { - // 不额外引入 nextTick,使用微任务刷新即可,保持体积精简 return Promise.resolve() } @@ -424,12 +420,10 @@ async function exportToExcel() { base64: base64Data, extension: 'jpeg', }) - worksheet.addImage(imageId, { tl: { col: 1, row: row.number - 1 }, ext: { width: 60, height: 60 } }) - row.height = 50 } } @@ -440,9 +434,7 @@ async function exportToExcel() { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' }) const fileName = `乐天商品数据_${new Date().toISOString().slice(0, 10)}.xlsx` - const success = await handlePlatformFileExport('rakuten', blob, fileName) - if (success) { showMessage('Excel文件导出成功!', 'success') } @@ -458,7 +450,6 @@ onMounted(loadLatest)