feat(device): 实现设备与账号绑定管理机制
- 引入 ClientAccountDevice 表管理设备与账号绑定关系 - 重构设备注册逻辑,支持多账号绑定同一设备 - 新增设备配额检查,基于账号维度限制设备数量 -优化设备移除逻辑,仅解除绑定而非物理删除- 改进设备列表查询,通过账号ID关联获取设备信息 - 更新心跳任务,支持向设备绑定的所有账号发送心跳 - 调整设备API参数,增加username字段用于权限校验 -修复HTTP请求编码问题,统一使用UTF-8字符集 - 增强错误处理,携带错误码信息便于前端识别 - 移除设备表中的username字段,解耦设备与用户名关联
This commit is contained in:
@@ -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;
|
||||
});
|
||||
|
||||
// 保存关闭行为配置
|
||||
|
||||
@@ -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(() => {
|
||||
<LoginDialog
|
||||
v-model="showAuthDialog"
|
||||
@login-success="handleLoginSuccess"
|
||||
@show-register="showRegisterDialog"/>
|
||||
@show-register="showRegisterDialog"
|
||||
@device-conflict="handleDeviceConflict"/>
|
||||
|
||||
<RegisterDialog
|
||||
v-model="showRegDialog"
|
||||
|
||||
@@ -40,7 +40,7 @@ export const deviceApi = {
|
||||
return http.post('/monitor/device/register', { ...payload, ip })
|
||||
},
|
||||
|
||||
remove(payload: { deviceId: string }) {
|
||||
remove(payload: { deviceId: string; username: string }) {
|
||||
return http.post('/monitor/device/remove', payload)
|
||||
},
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ async function request<T>(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<T>(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;
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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<HTMLInputElement | null>(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 () => {
|
||||
<span class="sep">|</span>
|
||||
<a class="link" @click.prevent="downloadAmazonTemplate">点击下载模板</a>
|
||||
</div>
|
||||
<div class="dropzone" @dragover.prevent="onDragOver" @dragleave="onDragLeave" @drop="onDrop" @click="openAmazonUpload">
|
||||
<div class="dropzone" :class="{ active: dragActive }" @dragenter="onDragEnter" @dragover="onDragOver" @dragleave="onDragLeave" @drop="onDrop" @click="openAmazonUpload">
|
||||
<div class="dz-el-icon">📤</div>
|
||||
<div class="dz-text">点击或将文件拖拽到这里上传</div>
|
||||
<div class="dz-sub">支持 .xls .xlsx</div>
|
||||
|
||||
@@ -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<Props>()
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -18,11 +18,17 @@ const PLATFORM_LABEL: Record<PlatformKey, string> = {
|
||||
}
|
||||
|
||||
const accounts = ref<BanmaAccount[]>([])
|
||||
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' })
|
||||
<div class="top">
|
||||
<img src="/icon/image.png" class="hero" alt="logo" />
|
||||
<div class="head-main">
|
||||
<div class="main-title">在线账号管理(3/3)</div>
|
||||
<div class="main-title">在线账号管理({{ accountLimit.count }}/{{ accountLimit.limit }})</div>
|
||||
<div class="main-sub">
|
||||
您当前订阅可同时托管3家 Shopee 店铺<br>
|
||||
如需扩增同时托管店铺数,请 <span class="upgrade">升级订阅</span>。
|
||||
您当前订阅可同时托管{{ accountLimit.limit }}个斑马账号<br>
|
||||
<span v-if="accountLimit.limit < 3">如需扩增账号数量,请 <span class="upgrade">升级订阅</span>。</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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<HTMLInputElement | null>(null)
|
||||
const dragActive = ref(false)
|
||||
|
||||
// 数据与分页
|
||||
const allProducts = ref<any[]>([])
|
||||
@@ -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)
|
||||
</script>
|
||||
<template>
|
||||
<div class="rakuten-root">
|
||||
|
||||
<div class="main-container">
|
||||
<div class="body-layout">
|
||||
<!-- 左侧步骤栏 -->
|
||||
@@ -480,7 +471,7 @@ onMounted(loadLatest)
|
||||
<a class="link" @click.prevent="downloadRakutenTemplate">点击下载模板</a>
|
||||
</div>
|
||||
|
||||
<div class="dropzone" @dragover.prevent="onDragOver" @dragleave="onDragLeave" @drop="onDrop" @click="openRakutenUpload" :class="{ disabled: loading }">
|
||||
<div class="dropzone" :class="{ disabled: loading, active: dragActive }" @dragenter="onDragEnter" @dragover="onDragOver" @dragleave="onDragLeave" @drop="onDrop" @click="openRakutenUpload">
|
||||
<div class="dz-el-icon">📤</div>
|
||||
<div class="dz-text">点击或将文件拖拽到这里上传</div>
|
||||
<div class="dz-sub">支持 .xls .xlsx</div>
|
||||
@@ -532,12 +523,8 @@ onMounted(loadLatest)
|
||||
</div>
|
||||
<div class="desc">点击下方按钮导出所有商品数据到 Excel 文件</div>
|
||||
<el-button size="small" class="w100 btn-blue" :disabled="!allProducts.length || loading || exportLoading" :loading="exportLoading" @click="exportToExcel">{{ exportLoading ? '导出中...' : '导出数据' }}</el-button>
|
||||
<!-- 导出进度条 -->
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
|
||||
@@ -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 = ''
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
import { ref } from 'vue'
|
||||
|
||||
export interface UseFileDropOptions {
|
||||
accept?: RegExp
|
||||
onFile: (file: File) => void | Promise<void>
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
</parent>
|
||||
<groupId>com.tashow.erp</groupId>
|
||||
<artifactId>erp_client_sb</artifactId>
|
||||
<version>2.4.7</version>
|
||||
<version>2.4.8</version>
|
||||
<name>erp_client_sb</name>
|
||||
<description>erp客户端</description>
|
||||
<properties>
|
||||
|
||||
@@ -92,12 +92,7 @@ public class Alibaba1688ServiceImpl implements Alibaba1688Service {
|
||||
MultiValueMap<String, String> formData = new LinkedMultiValueMap<>();
|
||||
formData.add("data", jsonData);
|
||||
HttpEntity<MultiValueMap<String, String>> requestEntity = new HttpEntity<>(formData, headers);
|
||||
Iterator<JsonNode> offerIterator = null;
|
||||
for (int retry = 0; retry < 3 && (offerIterator == null || !offerIterator.hasNext()); retry++) {
|
||||
ResponseEntity<String> 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<JsonNode> offerIterator = getOfferIterator(url, requestEntity);
|
||||
//运费 - 收集所有运费数据
|
||||
Set<Double> 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<String> 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<JsonNode> getOfferIterator(String url, HttpEntity<MultiValueMap<String, String>> requestEntity) throws IOException {
|
||||
for (int retry = 0; retry < 3; retry++) {
|
||||
ResponseEntity<String> response = restTemplate.exchange(url, HttpMethod.POST, requestEntity, String.class);
|
||||
JsonNode root = objectMapper.readTree(response.getBody());
|
||||
Iterator<JsonNode> 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);
|
||||
|
||||
@@ -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<ClientDevice> 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<String, String> 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()
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
@@ -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<ClientDevice> 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<ClientDevice> 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<ClientDevice> list = clientDeviceMapper.selectByUsername(username);
|
||||
java.util.ArrayList<ClientDevice> 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<String, String> 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<String, String> 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<String, String> 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<String, String> body) {
|
||||
String deviceId = body.get("deviceId");
|
||||
String username = body.get("username");
|
||||
ClientDevice device = clientDeviceMapper.selectByDeviceIdAndUsername(deviceId, username);
|
||||
public AjaxResult offline(@RequestBody Map<String, String> 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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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<ClientDevice> onlineDevices = clientDeviceMapper.selectOnlineDevices();
|
||||
for (ClientDevice device : onlineDevices) {
|
||||
sseHubService.sendPing(device.getUsername(), device.getDeviceId());
|
||||
String deviceId = device.getDeviceId();
|
||||
// 查询该设备绑定的所有账号
|
||||
List<Long> accountIds = accountDeviceMapper.selectAccountIdsByDeviceId(deviceId);
|
||||
for (Long accountId : accountIds) {
|
||||
ClientAccount account = accountMapper.selectClientAccountById(accountId);
|
||||
if (account != null) {
|
||||
sseHubService.sendPing(account.getUsername(), deviceId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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:
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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<ClientDevice> selectDevicesByAccountId(@Param("accountId") Long accountId);
|
||||
|
||||
/**
|
||||
* 根据设备ID查询绑定的账号ID列表
|
||||
*/
|
||||
List<Long> 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);
|
||||
}
|
||||
|
||||
@@ -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<ClientDevice> selectByUsername(@Param("username") String username);
|
||||
List<ClientDevice> 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);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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<BanmaAccount> 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 {
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="com.ruoyi.system.mapper.ClientAccountDeviceMapper">
|
||||
|
||||
<resultMap id="ClientAccountDeviceMap" type="com.ruoyi.system.domain.ClientAccountDevice">
|
||||
<id property="id" column="id"/>
|
||||
<result property="accountId" column="account_id"/>
|
||||
<result property="deviceId" column="device_id"/>
|
||||
<result property="bindTime" column="bind_time"/>
|
||||
<result property="status" column="status"/>
|
||||
<result property="createTime" column="create_time"/>
|
||||
<result property="updateTime" column="update_time"/>
|
||||
</resultMap>
|
||||
|
||||
<resultMap id="ClientDeviceResultMap" type="com.ruoyi.system.domain.ClientDevice">
|
||||
<id property="id" column="id"/>
|
||||
<result property="deviceId" column="device_id"/>
|
||||
<result property="name" column="name"/>
|
||||
<result property="os" column="os"/>
|
||||
<result property="status" column="status"/>
|
||||
<result property="ip" column="ip"/>
|
||||
<result property="location" column="location"/>
|
||||
<result property="lastActiveAt" column="last_active_at"/>
|
||||
<result property="trialExpireTime" column="trial_expire_time"/>
|
||||
<result property="createTime" column="create_time"/>
|
||||
<result property="updateTime" column="update_time"/>
|
||||
</resultMap>
|
||||
|
||||
<!-- 根据账号ID查询已绑定的设备列表 -->
|
||||
<select id="selectDevicesByAccountId" resultMap="ClientDeviceResultMap">
|
||||
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>
|
||||
|
||||
<!-- 根据设备ID查询绑定的账号ID列表 -->
|
||||
<select id="selectAccountIdsByDeviceId" resultType="java.lang.Long">
|
||||
SELECT account_id
|
||||
FROM client_account_device
|
||||
WHERE device_id = #{deviceId}
|
||||
AND status = 'active'
|
||||
</select>
|
||||
|
||||
<!-- 查询账号绑定的设备数量(不包括已移除的) -->
|
||||
<select id="countActiveDevicesByAccountId" resultType="int">
|
||||
SELECT COUNT(1)
|
||||
FROM client_account_device
|
||||
WHERE account_id = #{accountId}
|
||||
AND status = 'active'
|
||||
</select>
|
||||
|
||||
<!-- 检查账号和设备是否已绑定 -->
|
||||
<select id="selectByAccountIdAndDeviceId" resultMap="ClientAccountDeviceMap">
|
||||
SELECT *
|
||||
FROM client_account_device
|
||||
WHERE account_id = #{accountId}
|
||||
AND device_id = #{deviceId}
|
||||
</select>
|
||||
|
||||
<!-- 插入账号-设备绑定 -->
|
||||
<insert id="insert" parameterType="com.ruoyi.system.domain.ClientAccountDevice" useGeneratedKeys="true" keyProperty="id">
|
||||
INSERT INTO client_account_device(account_id, device_id, bind_time, status, create_time, update_time)
|
||||
VALUES(#{accountId}, #{deviceId}, #{bindTime}, #{status}, now(), now())
|
||||
</insert>
|
||||
|
||||
<!-- 更新绑定状态 -->
|
||||
<update id="updateStatus">
|
||||
UPDATE client_account_device
|
||||
SET status = #{status}, update_time = now()
|
||||
WHERE account_id = #{accountId}
|
||||
AND device_id = #{deviceId}
|
||||
</update>
|
||||
|
||||
<!-- 删除绑定 -->
|
||||
<delete id="delete">
|
||||
DELETE FROM client_account_device
|
||||
WHERE account_id = #{accountId}
|
||||
AND device_id = #{deviceId}
|
||||
</delete>
|
||||
|
||||
</mapper>
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
<mapper namespace="com.ruoyi.system.mapper.ClientDeviceMapper">
|
||||
<resultMap id="ClientDeviceMap" type="com.ruoyi.system.domain.ClientDevice">
|
||||
<id property="id" column="id"/>
|
||||
<result property="username" column="username"/>
|
||||
<result property="deviceId" column="device_id"/>
|
||||
<result property="name" column="name"/>
|
||||
<result property="os" column="os"/>
|
||||
@@ -17,38 +16,16 @@
|
||||
</resultMap>
|
||||
|
||||
<select id="selectByDeviceId" resultMap="ClientDeviceMap">
|
||||
select * from client_device where device_id = #{deviceId}
|
||||
</select>
|
||||
|
||||
<select id="selectByDeviceIdAndUsername" resultMap="ClientDeviceMap">
|
||||
select * from client_device where device_id = #{deviceId} and username = #{username}
|
||||
</select>
|
||||
|
||||
<select id="selectByUsername" resultMap="ClientDeviceMap">
|
||||
select * from client_device where username = #{username} and status != 'removed' order by update_time desc
|
||||
SELECT * FROM client_device WHERE device_id = #{deviceId}
|
||||
</select>
|
||||
|
||||
<insert id="insert" parameterType="com.ruoyi.system.domain.ClientDevice" useGeneratedKeys="true" keyProperty="id">
|
||||
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())
|
||||
</insert>
|
||||
|
||||
<update id="updateByDeviceId" parameterType="com.ruoyi.system.domain.ClientDevice">
|
||||
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>
|
||||
|
||||
<update id="updateByDeviceIdAndUsername" parameterType="com.ruoyi.system.domain.ClientDevice">
|
||||
update client_device
|
||||
UPDATE client_device
|
||||
<set>
|
||||
<if test="name != null">name = #{name},</if>
|
||||
<if test="os != null">os = #{os},</if>
|
||||
@@ -59,19 +36,15 @@
|
||||
<if test="trialExpireTime != null">trial_expire_time = #{trialExpireTime},</if>
|
||||
update_time = now()
|
||||
</set>
|
||||
where device_id = #{deviceId} and username = #{username}
|
||||
WHERE device_id = #{deviceId}
|
||||
</update>
|
||||
|
||||
<delete id="deleteByDeviceId">
|
||||
delete from client_device where device_id = #{deviceId}
|
||||
DELETE FROM client_device WHERE device_id = #{deviceId}
|
||||
</delete>
|
||||
|
||||
<select id="countByUsername" resultType="int">
|
||||
select count(1) from client_device where username = #{username}
|
||||
</select>
|
||||
|
||||
<select id="selectOnlineDevices" resultMap="ClientDeviceMap">
|
||||
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
|
||||
</select>
|
||||
</mapper>
|
||||
|
||||
|
||||
@@ -169,21 +169,22 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
<select id="selectClientInfoList" parameterType="ClientInfo" resultMap="ClientInfoResult">
|
||||
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
|
||||
<where>
|
||||
<if test="clientId != null and clientId != ''">AND d.device_id like concat('%', #{clientId}, '%')</if>
|
||||
<if test="username != null and username != ''">AND d.username like concat('%', #{username}, '%')</if>
|
||||
<if test="username != null and username != ''">AND a.username like concat('%', #{username}, '%')</if>
|
||||
<if test="osName != null and osName != ''">AND d.os like concat('%', #{osName}, '%')</if>
|
||||
<if test="online != null and online != ''">
|
||||
AND d.status =
|
||||
|
||||
@@ -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='斑马账号表';
|
||||
|
||||
|
||||
Reference in New Issue
Block a user