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' }) - 在线账号管理(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) - @@ -480,7 +471,7 @@ onMounted(loadLatest) 点击下载模板 - + 📤 点击或将文件拖拽到这里上传 支持 .xls .xlsx @@ -532,12 +523,8 @@ onMounted(loadLatest) 点击下方按钮导出所有商品数据到 Excel 文件 {{ exportLoading ? '导出中...' : '导出数据' }} - - - - diff --git a/electron-vue-template/src/renderer/components/zebra/ZebraDashboard.vue b/electron-vue-template/src/renderer/components/zebra/ZebraDashboard.vue index ec9beae..bfcd2d5 100644 --- a/electron-vue-template/src/renderer/components/zebra/ZebraDashboard.vue +++ b/electron-vue-template/src/renderer/components/zebra/ZebraDashboard.vue @@ -289,7 +289,19 @@ const rememberPwd = ref(true) const managerVisible = ref(false) const accountManagerRef = ref() -function openAddAccount() { +async function openAddAccount() { + try { + const username = getUsernameFromToken() + const limitRes = await zebraApi.getAccountLimit(username) + const limitData = (limitRes as any)?.data ?? limitRes + const { limit = 1, count = 0 } = limitData + if (count >= limit) { + ElMessage({ message: `账号数量已达上限(${limit}个),${limit < 3 ? '请升级订阅或' : ''}请先删除其他账号`, type: 'warning' }) + return + } + } catch (e) { + console.error('检查账号限制失败:', e) + } isEditMode.value = false accountForm.value = { name: '', username: '', isDefault: 0, status: 1 } formUsername.value = '' diff --git a/electron-vue-template/src/renderer/composables/useFileDrop.ts b/electron-vue-template/src/renderer/composables/useFileDrop.ts new file mode 100644 index 0000000..2c461e1 --- /dev/null +++ b/electron-vue-template/src/renderer/composables/useFileDrop.ts @@ -0,0 +1,64 @@ +import { ref } from 'vue' + +export interface UseFileDropOptions { + accept?: RegExp + onFile: (file: File) => void | Promise + onError?: (message: string) => void +} + +export function useFileDrop(options: UseFileDropOptions) { + const { accept, onFile, onError } = options + const dragActive = ref(false) + let dragCounter = 0 + + const onDragEnter = (e: DragEvent) => { + e.preventDefault() + e.stopPropagation() + dragCounter++ + dragActive.value = true + } + + const onDragOver = (e: DragEvent) => { + e.preventDefault() + e.stopPropagation() + if (e.dataTransfer) { + e.dataTransfer.dropEffect = 'copy' + } + } + + const onDragLeave = (e: DragEvent) => { + e.preventDefault() + e.stopPropagation() + dragCounter-- + if (dragCounter === 0) { + dragActive.value = false + } + } + + const onDrop = async (e: DragEvent) => { + e.preventDefault() + e.stopPropagation() + dragCounter = 0 + dragActive.value = false + + const file = e.dataTransfer?.files?.[0] + if (!file) return + + if (accept && !accept.test(file.name)) { + const fileTypes = accept.source.includes('xlsx?') ? '.xls 或 .xlsx' : accept.source + onError?.(`仅支持 ${fileTypes} 文件`) + return + } + + await onFile(file) + } + + return { + dragActive, + onDragEnter, + onDragOver, + onDragLeave, + onDrop + } +} + diff --git a/erp_client_sb/pom.xml b/erp_client_sb/pom.xml index 9c5c086..51e7e70 100644 --- a/erp_client_sb/pom.xml +++ b/erp_client_sb/pom.xml @@ -10,7 +10,7 @@ com.tashow.erp erp_client_sb - 2.4.7 + 2.4.8 erp_client_sb erp客户端 diff --git a/erp_client_sb/src/main/java/com/tashow/erp/service/impl/Alibaba1688ServiceImpl.java b/erp_client_sb/src/main/java/com/tashow/erp/service/impl/Alibaba1688ServiceImpl.java index fb9db1f..4238da5 100644 --- a/erp_client_sb/src/main/java/com/tashow/erp/service/impl/Alibaba1688ServiceImpl.java +++ b/erp_client_sb/src/main/java/com/tashow/erp/service/impl/Alibaba1688ServiceImpl.java @@ -92,12 +92,7 @@ public class Alibaba1688ServiceImpl implements Alibaba1688Service { MultiValueMap formData = new LinkedMultiValueMap<>(); formData.add("data", jsonData); HttpEntity> requestEntity = new HttpEntity<>(formData, headers); - Iterator offerIterator = null; - for (int retry = 0; retry < 3 && (offerIterator == null || !offerIterator.hasNext()); retry++) { - ResponseEntity response = restTemplate.exchange(url, HttpMethod.POST, requestEntity, String.class); - JsonNode root = objectMapper.readTree(response.getBody()); - offerIterator = root.path("data").path("offerList").path("offers").elements(); - } + Iterator offerIterator = getOfferIterator(url, requestEntity); //运费 - 收集所有运费数据 Set freight = new HashSet<>(); while (offerIterator.hasNext()) { @@ -111,12 +106,7 @@ public class Alibaba1688ServiceImpl implements Alibaba1688Service { .filter(fee -> fee > 0) .ifPresent(freight::add); } - offerIterator = null; - for (int retry = 0; retry < 3 && (offerIterator == null || !offerIterator.hasNext()); retry++) { - ResponseEntity response = restTemplate.exchange(url, HttpMethod.POST, requestEntity, String.class); - JsonNode root = objectMapper.readTree(response.getBody()); - offerIterator = root.path("data").path("offerList").path("offers").elements(); - } + offerIterator = getOfferIterator(url, requestEntity); for (int i = 0; i < 10 && offerIterator.hasNext(); i++) { JsonNode offer = offerIterator.next(); String offerId = offer.path("id").asText(); @@ -162,6 +152,15 @@ public class Alibaba1688ServiceImpl implements Alibaba1688Service { } } + private Iterator getOfferIterator(String url, HttpEntity> requestEntity) throws IOException { + for (int retry = 0; retry < 3; retry++) { + ResponseEntity response = restTemplate.exchange(url, HttpMethod.POST, requestEntity, String.class); + JsonNode root = objectMapper.readTree(response.getBody()); + Iterator iterator = root.path("data").path("offerList").path("offers").elements(); + if (iterator.hasNext()) return iterator; + } + return Collections.emptyIterator(); + } /** * 获取sku价格 @@ -252,7 +251,16 @@ public class Alibaba1688ServiceImpl implements Alibaba1688Service { public String uploadImageBase64(String imageUrl) { String token = Alibaba1688CookieUtil.getToken(); long timestamp = System.currentTimeMillis(); - byte[] imageBytes = noSslRestTemplate.getForObject(imageUrl, byte[].class); + byte[] imageBytes = null; + for (int i = 0; i < 3; i++) { + try { + imageBytes = noSslRestTemplate.getForObject(imageUrl, byte[].class); + break; + } catch (Exception e) { + if (i == 2) throw e; + try { Thread.sleep(1000); } catch (InterruptedException ignored) {} + } + } String base64Image = Base64.getEncoder().encodeToString(imageBytes); String jsonData = "{\"appId\":32517,\"params\":\"{\\\"searchScene\\\":\\\"imageEx\\\",\\\"interfaceName\\\":\\\"imageBase64ToImageId\\\",\\\"serviceParam.extendParam[imageBase64]\\\":\\\"" + base64Image + "\\\",\\\"subChannel\\\":\\\"pc_image_search_image_id\\\"}\"}"; String sign = Alibaba1688CookieUtil.generateSign(token, String.valueOf(timestamp), jsonData); diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/monitor/ClientAccountController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/monitor/ClientAccountController.java index 949cff8..6b695c2 100644 --- a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/monitor/ClientAccountController.java +++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/monitor/ClientAccountController.java @@ -31,6 +31,8 @@ import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; import com.ruoyi.web.sse.SseHubService; import com.ruoyi.system.mapper.ClientDeviceMapper; import com.ruoyi.system.domain.ClientDevice; +import com.ruoyi.system.mapper.ClientAccountDeviceMapper; +import com.ruoyi.system.domain.ClientAccountDevice; /** @@ -56,14 +58,18 @@ public class ClientAccountController extends BaseController { private SseHubService sseHubService; @Autowired private ClientDeviceMapper clientDeviceMapper; + @Autowired + private ClientAccountDeviceMapper accountDeviceMapper; - private AjaxResult checkDeviceLimit(String username, String deviceId, int deviceLimit) { - List userDevices = clientDeviceMapper.selectByUsername(username); - int userDevice = userDevices.size(); - boolean exists = userDevices.stream().anyMatch(d -> deviceId.equals(d.getDeviceId())); - if (exists) userDevice--; - if (userDevice >= deviceLimit) { - return AjaxResult.error("设备数量已达上限(" + deviceLimit + "个),请先移除其他设备"); + private AjaxResult checkDeviceLimit(Long accountId, String deviceId, int deviceLimit) { + int activeDeviceCount = accountDeviceMapper.countActiveDevicesByAccountId(accountId); + ClientAccountDevice binding = accountDeviceMapper.selectByAccountIdAndDeviceId(accountId, deviceId); + boolean exists = (binding != null && "active".equals(binding.getStatus())); + if (exists) activeDeviceCount--; + if (activeDeviceCount >= deviceLimit) { + AjaxResult result = AjaxResult.error("设备数量已达上限(" + deviceLimit + "个),请先移除其他设备"); + result.put("code", 501); + return result; } return null; } @@ -150,8 +156,10 @@ public class ClientAccountController extends BaseController { return AjaxResult.error("账号已被停用"); } - AjaxResult limitCheck = checkDeviceLimit(username, clientId, account.getDeviceLimit()); + // 检查设备限制 + AjaxResult limitCheck = checkDeviceLimit(account.getId(), clientId, account.getDeviceLimit()); if (limitCheck != null) return limitCheck; + String token = Jwts.builder() .setHeaderParam("kid", jwtRsaKeyService.getKeyId()) .setSubject(username) @@ -163,25 +171,16 @@ public class ClientAccountController extends BaseController { .signWith(SignatureAlgorithm.RS256, jwtRsaKeyService.getPrivateKey()) .compact(); - boolean deviceTrialExpired = false; - if ("trial".equals(account.getAccountType())) { - ClientDevice device = clientDeviceMapper.selectByDeviceIdAndUsername(clientId, username); - deviceTrialExpired = device != null - && device.getTrialExpireTime() != null - && new Date().after(device.getTrialExpireTime()); - } - return AjaxResult.success(Map.of( "token", token, "permissions", account.getPermissions(), "accountName", account.getAccountName(), "expireTime", account.getExpireTime(), - "accountType", account.getAccountType(), - "deviceTrialExpired", deviceTrialExpired + "accountType", account.getAccountType() )); } /** - * 验证token + * 验证token */ @PostMapping("/verify") public AjaxResult verifyToken(@RequestBody Map data) { @@ -204,9 +203,10 @@ public class ClientAccountController extends BaseController { clientAccountService.updateClientAccount(account); } + // 只有试用账号才检查设备试用期 boolean deviceTrialExpired = false; if ("trial".equals(account.getAccountType())) { - ClientDevice device = clientDeviceMapper.selectByDeviceIdAndUsername(clientId, username); + ClientDevice device = clientDeviceMapper.selectByDeviceId(clientId); deviceTrialExpired = device != null && device.getTrialExpireTime() != null && new Date().after(device.getTrialExpireTime()); @@ -233,7 +233,6 @@ public class ClientAccountController extends BaseController { if (username == null || tokenClientId == null || !tokenClientId.equals(clientId)) { throw new RuntimeException("会话不匹配"); } - SseEmitter emitter = sseHubService.register(username, clientId, 0L); try { emitter.send(SseEmitter.event().data("{\"type\":\"ready\"}")); @@ -260,6 +259,7 @@ public class ClientAccountController extends BaseController { clientAccount.setPermissions("{\"amazon\":true,\"rakuten\":true,\"zebra\":true}"); clientAccount.setPassword(passwordEncoder.encode(password)); clientAccount.setAccountType("trial"); + clientAccount.setDeviceLimit(3); clientAccount.setExpireTime(new Date(System.currentTimeMillis() + 3 * 24L * 60 * 60 * 1000)); int result = clientAccountService.insertClientAccount(clientAccount); @@ -273,6 +273,7 @@ public class ClientAccountController extends BaseController { .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() + JWT_EXPIRATION)) .claim("accountId", clientAccount.getId()) + .claim("username", username) .claim("clientId", deviceId) .signWith(SignatureAlgorithm.RS256, jwtRsaKeyService.getPrivateKey()) .compact(); @@ -282,8 +283,7 @@ public class ClientAccountController extends BaseController { "permissions", clientAccount.getPermissions(), "accountName", clientAccount.getAccountName(), "expireTime", clientAccount.getExpireTime(), - "accountType", clientAccount.getAccountType(), - "deviceTrialExpired", false + "accountType", clientAccount.getAccountType() )); } diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/ClientDeviceController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/ClientDeviceController.java index 2d7691e..531f955 100644 --- a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/ClientDeviceController.java +++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/ClientDeviceController.java @@ -6,155 +6,124 @@ import com.ruoyi.system.mapper.ClientDeviceMapper; import com.ruoyi.system.mapper.ClientAccountMapper; import com.ruoyi.system.domain.ClientAccount; import com.ruoyi.web.sse.SseHubService; +import com.ruoyi.system.mapper.ClientAccountDeviceMapper; +import com.ruoyi.system.domain.ClientAccountDevice; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; -import javax.servlet.http.HttpServletRequest; import java.util.List; import java.util.Map; +import java.util.Date; + @RestController @RequestMapping("/monitor/device") @Anonymous public class ClientDeviceController { - @Autowired - private ClientDeviceMapper clientDeviceMapper; - @Autowired - private ClientAccountMapper clientAccountMapper; - @Autowired - private SseHubService sseHubService; - private AjaxResult checkDeviceLimit(String username, String deviceId) { - ClientAccount account = clientAccountMapper.selectClientAccountByUsername(username); - int deviceLimit = (account != null && account.getDeviceLimit() != null) ? account.getDeviceLimit() : 3; - List userDevices = clientDeviceMapper.selectByUsername(username); - int userDevice = userDevices.size(); - boolean deviceExists = userDevices.stream().anyMatch(d -> deviceId.equals(d.getDeviceId())); - if (deviceExists) userDevice--; - if (userDevice >= deviceLimit) { - return AjaxResult.error("设备数量已达上限(" + deviceLimit + "个),请先移除其他设备"); - } - return null; + @Autowired private ClientDeviceMapper deviceMapper; + @Autowired private ClientAccountMapper accountMapper; + @Autowired private ClientAccountDeviceMapper accountDeviceMapper; + @Autowired private SseHubService sseHubService; + + private ClientAccount getAccount(String username) { + return accountMapper.selectClientAccountByUsername(username); + } + + private boolean exceedDeviceLimit(Long accountId, String deviceId, int limit) { + int count = accountDeviceMapper.countActiveDevicesByAccountId(accountId); + ClientAccountDevice binding = accountDeviceMapper.selectByAccountIdAndDeviceId(accountId, deviceId); + if (binding != null && "active".equals(binding.getStatus())) count--; + return count >= limit; } @GetMapping("/quota") - public AjaxResult quota(@RequestParam(value = "username", required = false) String username) { - List all = clientDeviceMapper.selectByUsername(username); - int used = 0; - for (ClientDevice d : all) { - if (!"removed".equals(d.getStatus())) used++; - } - ClientAccount account = clientAccountMapper.selectClientAccountByUsername(username); - int limit = (account != null && account.getDeviceLimit() != null) ? account.getDeviceLimit() : 3; + public AjaxResult quota(@RequestParam String username) { + ClientAccount account = getAccount(username); + int used = accountDeviceMapper.countActiveDevicesByAccountId(account.getId()); + int limit = account.getDeviceLimit() != null ? account.getDeviceLimit() : 3; return AjaxResult.success(Map.of("limit", limit, "used", used)); } - /** - * 按用户名查询设备列表(最近活动优先) - * @param username 用户名,必需参数 - * @return 设备列表 - */ + @GetMapping("/list") - public AjaxResult list(@RequestParam("username") String username) { - List list = clientDeviceMapper.selectByUsername(username); - java.util.ArrayList active = new java.util.ArrayList<>(); - for (ClientDevice d : list) { - if (!"removed".equals(d.getStatus())) active.add(d); - } - return AjaxResult.success(active); + public AjaxResult list(@RequestParam String username) { + return AjaxResult.success(accountDeviceMapper.selectDevicesByAccountId(getAccount(username).getId())); } - /** - * 设备注册 - */ @PostMapping("/register") - public AjaxResult register(@RequestBody ClientDevice device, HttpServletRequest request) { - String ip = device.getIp(); - String username = device.getUsername(); - String deviceId = device.getDeviceId(); - String os = device.getOs(); - String deviceName = username + "@" + ip + " (" + os + ")"; - - AjaxResult limitCheck = checkDeviceLimit(username, deviceId); - if (limitCheck != null) return limitCheck; - - ClientDevice exists = clientDeviceMapper.selectByDeviceIdAndUsername(deviceId, username); - if (exists == null) { - - device.setIp(ip); - device.setStatus("online"); - device.setLastActiveAt(new java.util.Date()); - device.setTrialExpireTime(new java.util.Date(System.currentTimeMillis() + 3 * 24L * 60 * 60 * 1000)); - device.setName(deviceName); - clientDeviceMapper.insert(device); - } else { - - exists.setName(deviceName); - exists.setOs(os); - exists.setStatus("online"); - exists.setIp(ip); - exists.setLocation(device.getLocation()); - exists.setLastActiveAt(new java.util.Date()); - clientDeviceMapper.updateByDeviceIdAndUsername(exists); + public AjaxResult register(@RequestBody Map data) { + String username = data.get("username"); + String deviceId = data.get("deviceId"); + ClientAccount account = getAccount(username); + int limit = account.getDeviceLimit() != null ? account.getDeviceLimit() : 3; + + if (exceedDeviceLimit(account.getId(), deviceId, limit)) { + return AjaxResult.error("设备数量已达上限(" + limit + "个)"); } + + ClientDevice device = deviceMapper.selectByDeviceId(deviceId); + Date now = new Date(); + if (device == null) { + device = new ClientDevice(); + device.setDeviceId(deviceId); + device.setName(username + "@" + data.get("ip") + " (" + data.get("os") + ")"); + device.setOs(data.get("os")); + device.setIp(data.get("ip")); + device.setLocation(data.get("location")); + device.setTrialExpireTime(new Date(System.currentTimeMillis() + 3 * 24L * 60 * 60 * 1000)); + deviceMapper.insert(device); + } + device.setStatus("online"); + device.setLastActiveAt(now); + device.setIp(data.get("ip")); + device.setLocation(data.get("location")); + deviceMapper.updateByDeviceId(device); + + ClientAccountDevice binding = accountDeviceMapper.selectByAccountIdAndDeviceId(account.getId(), deviceId); + if (binding == null) { + binding = new ClientAccountDevice(); + binding.setAccountId(account.getId()); + binding.setDeviceId(deviceId); + binding.setBindTime(now); + binding.setStatus("active"); + accountDeviceMapper.insert(binding); + } else if ("removed".equals(binding.getStatus())) { + accountDeviceMapper.updateStatus(account.getId(), deviceId, "active"); + } + return AjaxResult.success(); } - /** - * 重命名设备 - * - * 根据 deviceId 更新 name。 - */ @PostMapping("/rename") public AjaxResult rename(@RequestBody ClientDevice device) { - clientDeviceMapper.updateByDeviceId(device); + deviceMapper.updateByDeviceId(device); return AjaxResult.success(); } - /** - * 修改设备试用期过期时间 - */ @PostMapping("/updateExpire") public AjaxResult updateExpire(@RequestBody ClientDevice device) { - clientDeviceMapper.updateByDeviceIdAndUsername(device); - return AjaxResult.success(); - } - /** - * 移除设备 - * 根据 deviceId 删除设备绑定记录。 - */ - @PostMapping("/remove") - public AjaxResult remove(@RequestBody Map body) { - String deviceId = body.get("deviceId"); - String username = body.get("username"); - if (deviceId == null || deviceId.isEmpty()) { - return AjaxResult.error("deviceId不能为空"); - } - ClientDevice exists = clientDeviceMapper.selectByDeviceIdAndUsername(deviceId, username); - if (exists == null) { - return AjaxResult.success(); - } - if (!"removed".equals(exists.getStatus())) { - exists.setStatus("removed"); - exists.setLastActiveAt(new java.util.Date()); - clientDeviceMapper.updateByDeviceIdAndUsername(exists); - sseHubService.sendEvent(username, deviceId, "DEVICE_REMOVED", "{}"); - sseHubService.disconnectDevice(username, deviceId); - } + deviceMapper.updateByDeviceId(device); + return AjaxResult.success(); + } + + @PostMapping("/remove") + public AjaxResult remove(@RequestBody Map data) { + String deviceId = data.get("deviceId"); + String username = data.get("username"); + Long accountId = getAccount(username).getId(); + + accountDeviceMapper.updateStatus(accountId, deviceId, "removed"); + sseHubService.sendEvent(username, deviceId, "DEVICE_REMOVED", "{}"); + sseHubService.disconnectDevice(username, deviceId); return AjaxResult.success(); } - /** - * 设备离线 - */ @PostMapping("/offline") - public AjaxResult offline(@RequestBody Map body) { - String deviceId = body.get("deviceId"); - String username = body.get("username"); - ClientDevice device = clientDeviceMapper.selectByDeviceIdAndUsername(deviceId, username); + public AjaxResult offline(@RequestBody Map data) { + ClientDevice device = deviceMapper.selectByDeviceId(data.get("deviceId")); if (device != null) { device.setStatus("offline"); - device.setLastActiveAt(new java.util.Date()); - clientDeviceMapper.updateByDeviceIdAndUsername(device); + device.setLastActiveAt(new Date()); + deviceMapper.updateByDeviceId(device); } return AjaxResult.success(); } - } diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/tool/BanmaOrderController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/tool/BanmaOrderController.java index a22ddef..8a9489d 100644 --- a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/tool/BanmaOrderController.java +++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/tool/BanmaOrderController.java @@ -1,5 +1,6 @@ package com.ruoyi.web.controller.tool; +import java.util.Date; import java.util.Map; import org.springframework.beans.factory.annotation.Autowired; @@ -8,6 +9,9 @@ import com.ruoyi.common.annotation.Anonymous; import com.ruoyi.common.core.controller.BaseController; import com.ruoyi.common.core.domain.R; import com.ruoyi.system.domain.BanmaAccount; +import com.ruoyi.system.domain.ClientAccount; +import com.ruoyi.system.mapper.BanmaAccountMapper; +import com.ruoyi.system.mapper.ClientAccountMapper; import com.ruoyi.system.service.IBanmaAccountService; /** @@ -20,11 +24,32 @@ public class BanmaOrderController extends BaseController { @Autowired private IBanmaAccountService accountService; + @Autowired + private ClientAccountMapper clientAccountMapper; + @Autowired + private BanmaAccountMapper banmaAccountMapper; @GetMapping("/accounts") public R> listAccounts(String name) { return R.ok(accountService.listSimple(name)); } + + @GetMapping("/account-limit") + public R> getAccountLimit(String name) { + int limit = 1; + int count = 0; + if (name != null) { + ClientAccount client = clientAccountMapper.selectClientAccountByUsername(name); + if (client != null && "paid".equals(client.getAccountType()) + && client.getExpireTime() != null && new Date().before(client.getExpireTime())) { + limit = 3; + } + BanmaAccount query = new BanmaAccount(); + query.setClientUsername(name); + count = banmaAccountMapper.selectList(query).size(); + } + return R.ok(Map.of("limit", limit, "count", count)); + } @PostMapping("/accounts") public R> saveAccount(@RequestBody BanmaAccount body, String name) { if (body.getId() == null && accountService.validateAndGetToken(body.getUsername(), body.getPassword()) == null) { diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/sse/SseHubService.java b/ruoyi-admin/src/main/java/com/ruoyi/web/sse/SseHubService.java index 4c8bdb3..d3c1f89 100644 --- a/ruoyi-admin/src/main/java/com/ruoyi/web/sse/SseHubService.java +++ b/ruoyi-admin/src/main/java/com/ruoyi/web/sse/SseHubService.java @@ -124,13 +124,6 @@ public class SseHubService { try { ClientDevice device = clientDeviceMapper.selectByDeviceId(deviceId); if (device != null) { - if ("removed".equals(device.getStatus()) && "offline".equals(status)) { - return; - } - - if ("removed".equals(status)) { - disconnectDevice(device.getUsername(), deviceId); - } device.setStatus(status); device.setLastActiveAt(new Date()); clientDeviceMapper.updateByDeviceId(device); diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/task/DeviceHeartbeatTask.java b/ruoyi-admin/src/main/java/com/ruoyi/web/task/DeviceHeartbeatTask.java index aa046c6..00c909f 100644 --- a/ruoyi-admin/src/main/java/com/ruoyi/web/task/DeviceHeartbeatTask.java +++ b/ruoyi-admin/src/main/java/com/ruoyi/web/task/DeviceHeartbeatTask.java @@ -2,6 +2,9 @@ package com.ruoyi.web.task; import com.ruoyi.system.domain.ClientDevice; import com.ruoyi.system.mapper.ClientDeviceMapper; +import com.ruoyi.system.mapper.ClientAccountDeviceMapper; +import com.ruoyi.system.mapper.ClientAccountMapper; +import com.ruoyi.system.domain.ClientAccount; import com.ruoyi.web.sse.SseHubService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.scheduling.annotation.Scheduled; @@ -20,6 +23,12 @@ public class DeviceHeartbeatTask { @Autowired private ClientDeviceMapper clientDeviceMapper; + @Autowired + private ClientAccountDeviceMapper accountDeviceMapper; + + @Autowired + private ClientAccountMapper accountMapper; + @Autowired private SseHubService sseHubService; @@ -30,7 +39,15 @@ public class DeviceHeartbeatTask { public void sendHeartbeatPing() { List onlineDevices = clientDeviceMapper.selectOnlineDevices(); for (ClientDevice device : onlineDevices) { - sseHubService.sendPing(device.getUsername(), device.getDeviceId()); + String deviceId = device.getDeviceId(); + // 查询该设备绑定的所有账号 + List accountIds = accountDeviceMapper.selectAccountIdsByDeviceId(deviceId); + for (Long accountId : accountIds) { + ClientAccount account = accountMapper.selectClientAccountById(accountId); + if (account != null) { + sseHubService.sendPing(account.getUsername(), deviceId); + } + } } } } \ No newline at end of file diff --git a/ruoyi-admin/src/main/resources/application.yml b/ruoyi-admin/src/main/resources/application.yml index 3625ed9..24897d9 100644 --- a/ruoyi-admin/src/main/resources/application.yml +++ b/ruoyi-admin/src/main/resources/application.yml @@ -62,8 +62,15 @@ spring: messages: # 国际化资源文件路径 basename: i18n/messages + encoding: UTF-8 profiles: active: druid + # HTTP编码 + http: + encoding: + charset: UTF-8 + enabled: true + force: true # 文件上传 servlet: multipart: diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/domain/ClientAccountDevice.java b/ruoyi-system/src/main/java/com/ruoyi/system/domain/ClientAccountDevice.java new file mode 100644 index 0000000..663e9bd --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/domain/ClientAccountDevice.java @@ -0,0 +1,61 @@ +package com.ruoyi.system.domain; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.ruoyi.common.core.domain.BaseEntity; + +import java.util.Date; + +/** + * 客户端账号-设备关联对象 client_account_device + */ +public class ClientAccountDevice extends BaseEntity { + private static final long serialVersionUID = 1L; + + private Long id; + private Long accountId; + private String deviceId; + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date bindTime; + private String status; // active/removed + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Long getAccountId() { + return accountId; + } + + public void setAccountId(Long accountId) { + this.accountId = accountId; + } + + public String getDeviceId() { + return deviceId; + } + + public void setDeviceId(String deviceId) { + this.deviceId = deviceId; + } + + public Date getBindTime() { + return bindTime; + } + + public void setBindTime(Date bindTime) { + this.bindTime = bindTime; + } + + public String getStatus() { + return status; + } + + public void setStatus(String status) { + this.status = status; + } +} + diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/domain/ClientDevice.java b/ruoyi-system/src/main/java/com/ruoyi/system/domain/ClientDevice.java index 72352d0..5c718d4 100644 --- a/ruoyi-system/src/main/java/com/ruoyi/system/domain/ClientDevice.java +++ b/ruoyi-system/src/main/java/com/ruoyi/system/domain/ClientDevice.java @@ -13,8 +13,6 @@ public class ClientDevice extends BaseEntity { private static final long serialVersionUID = 1L; private Long id; - @Excel(name = "用户名") - private String username; @Excel(name = "设备ID") private String deviceId; @Excel(name = "设备名") @@ -36,8 +34,6 @@ public class ClientDevice extends BaseEntity { public Long getId() { return id; } public void setId(Long id) { this.id = id; } - public String getUsername() { return username; } - public void setUsername(String username) { this.username = username; } public String getDeviceId() { return deviceId; } public void setDeviceId(String deviceId) { this.deviceId = deviceId; } public String getName() { return name; } diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/mapper/ClientAccountDeviceMapper.java b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/ClientAccountDeviceMapper.java new file mode 100644 index 0000000..fadb24c --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/ClientAccountDeviceMapper.java @@ -0,0 +1,48 @@ +package com.ruoyi.system.mapper; + +import com.ruoyi.system.domain.ClientAccountDevice; +import com.ruoyi.system.domain.ClientDevice; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + * 客户端账号-设备关联Mapper接口 + */ +public interface ClientAccountDeviceMapper { + /** + * 根据账号ID查询已绑定的设备列表 + */ + List selectDevicesByAccountId(@Param("accountId") Long accountId); + + /** + * 根据设备ID查询绑定的账号ID列表 + */ + List selectAccountIdsByDeviceId(@Param("deviceId") String deviceId); + + /** + * 查询账号绑定的设备数量(不包括已移除的) + */ + int countActiveDevicesByAccountId(@Param("accountId") Long accountId); + + /** + * 检查账号和设备是否已绑定 + */ + ClientAccountDevice selectByAccountIdAndDeviceId(@Param("accountId") Long accountId, @Param("deviceId") String deviceId); + + /** + * 插入账号-设备绑定 + */ + int insert(ClientAccountDevice binding); + + /** + * 更新绑定状态 + */ + int updateStatus(@Param("accountId") Long accountId, @Param("deviceId") String deviceId, @Param("status") String status); + + /** + * 删除绑定 + */ + int delete(@Param("accountId") Long accountId, @Param("deviceId") String deviceId); +} + diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/mapper/ClientDeviceMapper.java b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/ClientDeviceMapper.java index 7cf16cd..fc7cbd3 100644 --- a/ruoyi-system/src/main/java/com/ruoyi/system/mapper/ClientDeviceMapper.java +++ b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/ClientDeviceMapper.java @@ -7,14 +7,10 @@ import java.util.List; public interface ClientDeviceMapper { ClientDevice selectByDeviceId(@Param("deviceId") String deviceId); - ClientDevice selectByDeviceIdAndUsername(@Param("deviceId") String deviceId, @Param("username") String username); - List selectByUsername(@Param("username") String username); List selectOnlineDevices(); int insert(ClientDevice device); int updateByDeviceId(ClientDevice device); - int updateByDeviceIdAndUsername(ClientDevice device); int deleteByDeviceId(@Param("deviceId") String deviceId); - int countByUsername(@Param("username") String username); } diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/BanmaAccountServiceImpl.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/BanmaAccountServiceImpl.java index bbecd7e..64fb53b 100644 --- a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/BanmaAccountServiceImpl.java +++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/BanmaAccountServiceImpl.java @@ -10,7 +10,9 @@ import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.web.client.RestTemplate; import com.ruoyi.system.domain.BanmaAccount; +import com.ruoyi.system.domain.ClientAccount; import com.ruoyi.system.mapper.BanmaAccountMapper; +import com.ruoyi.system.mapper.ClientAccountMapper; import com.ruoyi.system.service.IBanmaAccountService; /** @@ -21,9 +23,21 @@ public class BanmaAccountServiceImpl implements IBanmaAccountService { @Autowired private BanmaAccountMapper mapper; + @Autowired + private ClientAccountMapper clientAccountMapper; private final RestTemplate restTemplate = new RestTemplate(); private static final String LOGIN_URL = "https://banma365.cn/api/login"; + private int getAccountLimit(String clientUsername) { + if (clientUsername == null) return 3; + ClientAccount client = clientAccountMapper.selectClientAccountByUsername(clientUsername); + if (client == null) return 1; + if ("paid".equals(client.getAccountType()) && client.getExpireTime() != null && new Date().before(client.getExpireTime())) { + return 3; + } + return 1; + } + @Override public List listSimple() { return listSimple(null); @@ -51,6 +65,17 @@ public class BanmaAccountServiceImpl implements IBanmaAccountService { entity.setClientUsername(clientUsername); } + // 新增时检查数量限制 + if (entity.getId() == null && entity.getClientUsername() != null) { + int limit = getAccountLimit(entity.getClientUsername()); + BanmaAccount query = new BanmaAccount(); + query.setClientUsername(entity.getClientUsername()); + int count = mapper.selectList(query).size(); + if (count >= limit) { + throw new RuntimeException("账号数量已达上限(" + limit + "个),请升级订阅或删除其他账号"); + } + } + if (entity.getId() == null) { mapper.insert(entity); } else { diff --git a/ruoyi-system/src/main/resources/mapper/system/ClientAccountDeviceMapper.xml b/ruoyi-system/src/main/resources/mapper/system/ClientAccountDeviceMapper.xml new file mode 100644 index 0000000..4e23085 --- /dev/null +++ b/ruoyi-system/src/main/resources/mapper/system/ClientAccountDeviceMapper.xml @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + SELECT + d.id, d.device_id, d.name, d.os, d.status, d.ip, d.location, + d.last_active_at, d.trial_expire_time, d.create_time, d.update_time + FROM client_device d + INNER JOIN client_account_device ad ON d.device_id = ad.device_id + WHERE ad.account_id = #{accountId} + AND ad.status = 'active' + ORDER BY d.last_active_at DESC + + + + + SELECT account_id + FROM client_account_device + WHERE device_id = #{deviceId} + AND status = 'active' + + + + + SELECT COUNT(1) + FROM client_account_device + WHERE account_id = #{accountId} + AND status = 'active' + + + + + SELECT * + FROM client_account_device + WHERE account_id = #{accountId} + AND device_id = #{deviceId} + + + + + INSERT INTO client_account_device(account_id, device_id, bind_time, status, create_time, update_time) + VALUES(#{accountId}, #{deviceId}, #{bindTime}, #{status}, now(), now()) + + + + + UPDATE client_account_device + SET status = #{status}, update_time = now() + WHERE account_id = #{accountId} + AND device_id = #{deviceId} + + + + + DELETE FROM client_account_device + WHERE account_id = #{accountId} + AND device_id = #{deviceId} + + + + diff --git a/ruoyi-system/src/main/resources/mapper/system/ClientDeviceMapper.xml b/ruoyi-system/src/main/resources/mapper/system/ClientDeviceMapper.xml index 0bc1d60..2615716 100644 --- a/ruoyi-system/src/main/resources/mapper/system/ClientDeviceMapper.xml +++ b/ruoyi-system/src/main/resources/mapper/system/ClientDeviceMapper.xml @@ -3,7 +3,6 @@ - @@ -17,38 +16,16 @@ - select * from client_device where device_id = #{deviceId} - - - - select * from client_device where device_id = #{deviceId} and username = #{username} - - - - select * from client_device where username = #{username} and status != 'removed' order by update_time desc + SELECT * FROM client_device WHERE device_id = #{deviceId} - insert into client_device(username, device_id, name, os, status, ip, location, last_active_at, trial_expire_time, create_time, update_time) - values(#{username}, #{deviceId}, #{name}, #{os}, #{status}, #{ip}, #{location}, #{lastActiveAt}, #{trialExpireTime}, now(), now()) + INSERT INTO client_device(device_id, name, os, status, ip, location, last_active_at, trial_expire_time, create_time, update_time) + VALUES(#{deviceId}, #{name}, #{os}, #{status}, #{ip}, #{location}, #{lastActiveAt}, #{trialExpireTime}, now(), now()) - update client_device - set username = #{username}, - name = #{name}, - os = #{os}, - status = #{status}, - ip = #{ip}, - location = #{location}, - last_active_at = #{lastActiveAt}, - trial_expire_time = #{trialExpireTime}, - update_time = now() - where device_id = #{deviceId} - - - - update client_device + UPDATE client_device name = #{name}, os = #{os}, @@ -59,19 +36,15 @@ trial_expire_time = #{trialExpireTime}, update_time = now() - where device_id = #{deviceId} and username = #{username} + WHERE device_id = #{deviceId} - delete from client_device where device_id = #{deviceId} + DELETE FROM client_device WHERE device_id = #{deviceId} - - select count(1) from client_device where username = #{username} - - - select * from client_device where status = 'online' order by last_active_at desc + SELECT * FROM client_device WHERE status = 'online' ORDER BY last_active_at DESC diff --git a/ruoyi-system/src/main/resources/mapper/system/ClientMonitorMapper.xml b/ruoyi-system/src/main/resources/mapper/system/ClientMonitorMapper.xml index ba5a298..5fa1f6e 100644 --- a/ruoyi-system/src/main/resources/mapper/system/ClientMonitorMapper.xml +++ b/ruoyi-system/src/main/resources/mapper/system/ClientMonitorMapper.xml @@ -169,21 +169,22 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" select d.device_id as client_id, - d.username, + a.username, d.os as os_name, d.ip as ip_address, d.last_active_at as last_active_time, - d.create_time as auth_time, + ad.bind_time as auth_time, CASE WHEN d.status = 'online' THEN '1' ELSE '0' END as online, a.account_name as hostname, '' as app_version, '' as os_version, '' as java_version from client_device d - left join client_account a on d.username COLLATE utf8mb4_unicode_ci = a.username + left join client_account_device ad on d.device_id = ad.device_id and ad.status = 'active' + left join client_account a on ad.account_id = a.id AND d.device_id like concat('%', #{clientId}, '%') - AND d.username like concat('%', #{username}, '%') + AND a.username like concat('%', #{username}, '%') AND d.os like concat('%', #{osName}, '%') AND d.status = diff --git a/sql/banma_account.sql b/sql/banma_account.sql deleted file mode 100644 index 7579376..0000000 --- a/sql/banma_account.sql +++ /dev/null @@ -1,20 +0,0 @@ --- 斑马账号表(与 BanmaAccount 实体、BanmaAccountMapper.xml 一致) -DROP TABLE IF EXISTS `banma_account`; -CREATE TABLE `banma_account` ( - `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键', - `name` VARCHAR(64) DEFAULT NULL COMMENT '显示名', - `username` VARCHAR(128) DEFAULT NULL COMMENT '登录用户名', - `token` VARCHAR(512) DEFAULT NULL COMMENT '访问Token(客户端刷新后回写)', - `token_expire_at` DATETIME DEFAULT NULL COMMENT 'Token过期时间', - `is_default` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否默认 1是 0否', - `status` TINYINT(1) NOT NULL DEFAULT 1 COMMENT '状态 1启用 0停用', - `remark` VARCHAR(255) DEFAULT NULL COMMENT '备注', - `create_by` VARCHAR(64) DEFAULT NULL, - `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, - `update_by` VARCHAR(64) DEFAULT NULL, - `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - PRIMARY KEY (`id`), - KEY `idx_banma_is_default` (`is_default`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='斑马账号表'; - -