feat(electron-vue-template):重构认证与设备管理模块
- 统一token存取逻辑,封装getToken/setToken/removeToken方法 -优化设备ID获取逻辑,调整API路径 - 完善设备管理接口类型定义,增强类型安全 - 调整SSE连接逻辑,使用统一配置管理- 重构HTTP客户端,集中管理后端服务配置 - 更新认证相关API接口,完善请求/响应类型 - 优化设备列表展示逻辑,移除冗余字段 - 调整图片代理路径,统一API前缀 - 完善用户反馈列表展示功能,增强交互体验 - 移除冗余的错误处理逻辑,简化代码结构
This commit is contained in:
@@ -495,6 +495,63 @@ ipcMain.handle('write-file', async (event, filePath: string, data: Uint8Array) =
|
|||||||
return { success: true };
|
return { success: true };
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 获取日志日期列表
|
||||||
|
ipcMain.handle('get-log-dates', async () => {
|
||||||
|
try {
|
||||||
|
const logDir = 'C:/ProgramData/erp-logs';
|
||||||
|
if (!existsSync(logDir)) {
|
||||||
|
return { dates: [] };
|
||||||
|
}
|
||||||
|
|
||||||
|
const files = await fs.readdir(logDir);
|
||||||
|
const dates: string[] = [];
|
||||||
|
|
||||||
|
// 获取今天的日期(YYYY-MM-DD格式)
|
||||||
|
const today = new Date().toISOString().split('T')[0];
|
||||||
|
|
||||||
|
files.forEach(file => {
|
||||||
|
if (file === 'spring-boot.log') {
|
||||||
|
// 当天的日志文件,使用今天的日期
|
||||||
|
dates.push(today);
|
||||||
|
} else if (file.startsWith('spring-boot-') && file.endsWith('.log')) {
|
||||||
|
// 历史日志文件,提取日期
|
||||||
|
const date = file.replace('spring-boot-', '').replace('.log', '');
|
||||||
|
dates.push(date);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 排序,最新的在前面
|
||||||
|
dates.sort().reverse();
|
||||||
|
|
||||||
|
return { dates };
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取日志日期列表失败:', error);
|
||||||
|
return { dates: [] };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 读取指定日期的日志文件
|
||||||
|
ipcMain.handle('read-log-file', async (event, logDate: string) => {
|
||||||
|
try {
|
||||||
|
const logDir = 'C:/ProgramData/erp-logs';
|
||||||
|
const today = new Date().toISOString().split('T')[0];
|
||||||
|
|
||||||
|
// 如果是今天的日期,读取 spring-boot.log,否则读取带日期的文件
|
||||||
|
const fileName = logDate === today ? 'spring-boot.log' : `spring-boot-${logDate}.log`;
|
||||||
|
const logFilePath = `${logDir}/${fileName}`;
|
||||||
|
|
||||||
|
if (!existsSync(logFilePath)) {
|
||||||
|
return { success: false, error: '日志文件不存在' };
|
||||||
|
}
|
||||||
|
|
||||||
|
const content = await fs.readFile(logFilePath, 'utf-8');
|
||||||
|
return { success: true, content };
|
||||||
|
} catch (error) {
|
||||||
|
console.error('读取日志文件失败:', error);
|
||||||
|
return { success: false, error: error instanceof Error ? error.message : '读取失败' };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
async function getFileSize(url: string): Promise<number> {
|
async function getFileSize(url: string): Promise<number> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
|
|||||||
@@ -17,6 +17,9 @@ const electronAPI = {
|
|||||||
showOpenDialog: (options: any) => ipcRenderer.invoke('show-open-dialog', options),
|
showOpenDialog: (options: any) => ipcRenderer.invoke('show-open-dialog', options),
|
||||||
// 添加文件写入 API
|
// 添加文件写入 API
|
||||||
writeFile: (filePath: string, data: Uint8Array) => ipcRenderer.invoke('write-file', filePath, data),
|
writeFile: (filePath: string, data: Uint8Array) => ipcRenderer.invoke('write-file', filePath, data),
|
||||||
|
// 添加日志相关 API
|
||||||
|
getLogDates: () => ipcRenderer.invoke('get-log-dates'),
|
||||||
|
readLogFile: (logDate: string) => ipcRenderer.invoke('read-log-file', logDate),
|
||||||
|
|
||||||
onDownloadProgress: (callback: (progress: any) => void) => {
|
onDownloadProgress: (callback: (progress: any) => void) => {
|
||||||
ipcRenderer.on('download-progress', (event, progress) => callback(progress))
|
ipcRenderer.on('download-progress', (event, progress) => callback(progress))
|
||||||
|
|||||||
@@ -3,9 +3,11 @@ import {onMounted, ref, computed, defineAsyncComponent, type Component, onUnmoun
|
|||||||
import {ElMessage, ElMessageBox} from 'element-plus'
|
import {ElMessage, ElMessageBox} from 'element-plus'
|
||||||
import zhCn from 'element-plus/es/locale/lang/zh-cn'
|
import zhCn from 'element-plus/es/locale/lang/zh-cn'
|
||||||
import 'element-plus/dist/index.css'
|
import 'element-plus/dist/index.css'
|
||||||
import {authApi, TOKEN_KEY} from './api/auth'
|
import {authApi} from './api/auth'
|
||||||
import {deviceApi, type DeviceItem, type DeviceQuota} from './api/device'
|
import {deviceApi, type DeviceItem, type DeviceQuota} from './api/device'
|
||||||
import {getOrCreateDeviceId} from './utils/deviceId'
|
import {getOrCreateDeviceId} from './utils/deviceId'
|
||||||
|
import {getToken, setToken, removeToken, getUsernameFromToken, getClientIdFromToken} from './utils/token'
|
||||||
|
import {CONFIG} from './api/http'
|
||||||
const LoginDialog = defineAsyncComponent(() => import('./components/auth/LoginDialog.vue'))
|
const LoginDialog = defineAsyncComponent(() => import('./components/auth/LoginDialog.vue'))
|
||||||
const RegisterDialog = defineAsyncComponent(() => import('./components/auth/RegisterDialog.vue'))
|
const RegisterDialog = defineAsyncComponent(() => import('./components/auth/RegisterDialog.vue'))
|
||||||
const NavigationBar = defineAsyncComponent(() => import('./components/layout/NavigationBar.vue'))
|
const NavigationBar = defineAsyncComponent(() => import('./components/layout/NavigationBar.vue'))
|
||||||
@@ -147,7 +149,7 @@ function handleMenuSelect(key: string) {
|
|||||||
|
|
||||||
async function handleLoginSuccess(data: { token: string; permissions?: string; expireTime?: string }) {
|
async function handleLoginSuccess(data: { token: string; permissions?: string; expireTime?: string }) {
|
||||||
try {
|
try {
|
||||||
localStorage.setItem(TOKEN_KEY, data.token)
|
setToken(data.token)
|
||||||
isAuthenticated.value = true
|
isAuthenticated.value = true
|
||||||
showAuthDialog.value = false
|
showAuthDialog.value = false
|
||||||
showRegDialog.value = false
|
showRegDialog.value = false
|
||||||
@@ -166,13 +168,13 @@ async function handleLoginSuccess(data: { token: string; permissions?: string; e
|
|||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
isAuthenticated.value = false
|
isAuthenticated.value = false
|
||||||
showAuthDialog.value = true
|
showAuthDialog.value = true
|
||||||
localStorage.removeItem(TOKEN_KEY)
|
removeToken()
|
||||||
ElMessage.error(e?.message || '设备注册失败')
|
ElMessage.error(e?.message || '设备注册失败')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearLocalAuth() {
|
function clearLocalAuth() {
|
||||||
localStorage.removeItem(TOKEN_KEY)
|
removeToken()
|
||||||
isAuthenticated.value = false
|
isAuthenticated.value = false
|
||||||
currentUsername.value = ''
|
currentUsername.value = ''
|
||||||
userPermissions.value = ''
|
userPermissions.value = ''
|
||||||
@@ -221,7 +223,7 @@ function backToLogin() {
|
|||||||
|
|
||||||
async function checkAuth() {
|
async function checkAuth() {
|
||||||
try {
|
try {
|
||||||
const token = localStorage.getItem(TOKEN_KEY)
|
const token = getToken()
|
||||||
if (!token) {
|
if (!token) {
|
||||||
if (['rakuten', 'amazon', 'zebra', 'shopee'].includes(activeMenu.value)) {
|
if (['rakuten', 'amazon', 'zebra', 'shopee'].includes(activeMenu.value)) {
|
||||||
showAuthDialog.value = true
|
showAuthDialog.value = true
|
||||||
@@ -229,40 +231,24 @@ async function checkAuth() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const res: any = await authApi.verifyToken(token)
|
const res = await authApi.verifyToken(token)
|
||||||
isAuthenticated.value = true
|
isAuthenticated.value = true
|
||||||
currentUsername.value = getUsernameFromToken(token) || ''
|
currentUsername.value = getUsernameFromToken(token)
|
||||||
userPermissions.value = res?.data?.permissions || res?.permissions || ''
|
userPermissions.value = res.data.permissions || ''
|
||||||
|
|
||||||
const expireTime = res?.data?.expireTime || res?.expireTime
|
if (res.data.expireTime) {
|
||||||
if (expireTime) vipExpireTime.value = new Date(expireTime)
|
vipExpireTime.value = new Date(res.data.expireTime)
|
||||||
|
}
|
||||||
|
|
||||||
SSEManager.connect()
|
SSEManager.connect()
|
||||||
} catch {
|
} catch {
|
||||||
localStorage.removeItem(TOKEN_KEY)
|
removeToken()
|
||||||
if (['rakuten', 'amazon', 'zebra', 'shopee'].includes(activeMenu.value)) {
|
if (['rakuten', 'amazon', 'zebra', 'shopee'].includes(activeMenu.value)) {
|
||||||
showAuthDialog.value = true
|
showAuthDialog.value = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getClientIdFromToken(token?: string) {
|
|
||||||
try {
|
|
||||||
const t = token || localStorage.getItem(TOKEN_KEY)
|
|
||||||
if (!t) return ''
|
|
||||||
return JSON.parse(atob(t.split('.')[1])).clientId || ''
|
|
||||||
} catch {
|
|
||||||
return ''
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getUsernameFromToken(token: string) {
|
|
||||||
try {
|
|
||||||
return JSON.parse(atob(token.split('.')[1])).username || ''
|
|
||||||
} catch {
|
|
||||||
return ''
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const SSEManager = {
|
const SSEManager = {
|
||||||
connection: null as EventSource | null,
|
connection: null as EventSource | null,
|
||||||
@@ -270,19 +256,13 @@ const SSEManager = {
|
|||||||
if (this.connection) return
|
if (this.connection) return
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const token = localStorage.getItem(TOKEN_KEY)
|
const token = getToken()
|
||||||
if (!token) return console.warn('SSE连接失败: 没有token')
|
if (!token) return
|
||||||
|
|
||||||
const clientId = getClientIdFromToken(token)
|
const clientId = getClientIdFromToken()
|
||||||
if (!clientId) return console.warn('SSE连接失败: 无法获取clientId')
|
if (!clientId) return
|
||||||
|
|
||||||
let sseUrl = 'http://192.168.1.89:8085/monitor/account/events'
|
const src = new EventSource(`${CONFIG.SSE_URL}?clientId=${clientId}&token=${token}`)
|
||||||
try {
|
|
||||||
const resp = await fetch('/api/config/server')
|
|
||||||
if (resp.ok) sseUrl = (await resp.json()).sseUrl || sseUrl
|
|
||||||
} catch {}
|
|
||||||
|
|
||||||
const src = new EventSource(`${sseUrl}?clientId=${clientId}&token=${token}`)
|
|
||||||
this.connection = src
|
this.connection = src
|
||||||
src.onopen = () => console.log('SSE连接已建立')
|
src.onopen = () => console.log('SSE连接已建立')
|
||||||
src.onmessage = (e) => this.handleMessage(e)
|
src.onmessage = (e) => this.handleMessage(e)
|
||||||
@@ -335,10 +315,10 @@ const SSEManager = {
|
|||||||
|
|
||||||
disconnect() {
|
disconnect() {
|
||||||
if (this.connection) {
|
if (this.connection) {
|
||||||
try { this.connection.close() } catch {}
|
this.connection.close()
|
||||||
this.connection = null
|
this.connection = null
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function openDeviceManager() {
|
async function openDeviceManager() {
|
||||||
@@ -363,13 +343,12 @@ async function fetchDeviceData() {
|
|||||||
deviceLoading.value = true
|
deviceLoading.value = true
|
||||||
const [quotaRes, listRes] = await Promise.all([
|
const [quotaRes, listRes] = await Promise.all([
|
||||||
deviceApi.getQuota(currentUsername.value),
|
deviceApi.getQuota(currentUsername.value),
|
||||||
deviceApi.list(currentUsername.value),
|
deviceApi.list(currentUsername.value)
|
||||||
]) as any[]
|
])
|
||||||
|
|
||||||
deviceQuota.value = quotaRes?.data || quotaRes || {limit: 0, used: 0}
|
deviceQuota.value = quotaRes.data
|
||||||
const clientId = await getClientIdFromToken()
|
const clientId = getClientIdFromToken()
|
||||||
const list = listRes?.data || listRes || []
|
devices.value = listRes.data.map(d => ({...d, isCurrent: d.deviceId === clientId}))
|
||||||
devices.value = list.map(d => ({...d, isCurrent: d.deviceId === clientId}))
|
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
ElMessage.error(e?.message || '获取设备列表失败')
|
ElMessage.error(e?.message || '获取设备列表失败')
|
||||||
} finally {
|
} finally {
|
||||||
@@ -377,7 +356,7 @@ async function fetchDeviceData() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function confirmRemoveDevice(row: DeviceItem & { isCurrent?: boolean }) {
|
async function confirmRemoveDevice(row: DeviceItem) {
|
||||||
try {
|
try {
|
||||||
await ElMessageBox.confirm('确定要移除该设备吗?', '你确定要移除设备吗?', {
|
await ElMessageBox.confirm('确定要移除该设备吗?', '你确定要移除设备吗?', {
|
||||||
confirmButtonText: '确定移除',
|
confirmButtonText: '确定移除',
|
||||||
@@ -387,7 +366,7 @@ async function confirmRemoveDevice(row: DeviceItem & { isCurrent?: boolean }) {
|
|||||||
|
|
||||||
await deviceApi.remove({deviceId: row.deviceId})
|
await deviceApi.remove({deviceId: row.deviceId})
|
||||||
devices.value = devices.value.filter(d => d.deviceId !== row.deviceId)
|
devices.value = devices.value.filter(d => d.deviceId !== row.deviceId)
|
||||||
deviceQuota.value.used = Math.max(0, (deviceQuota.value.used || 0) - 1)
|
deviceQuota.value.used = Math.max(0, deviceQuota.value.used - 1)
|
||||||
|
|
||||||
if (row.deviceId === getClientIdFromToken()) {
|
if (row.deviceId === getClientIdFromToken()) {
|
||||||
clearLocalAuth()
|
clearLocalAuth()
|
||||||
|
|||||||
@@ -30,6 +30,6 @@ export const amazonApi = {
|
|||||||
return http.get('/api/amazon/products/search', searchParams);
|
return http.get('/api/amazon/products/search', searchParams);
|
||||||
},
|
},
|
||||||
openGenmaiSpirit() {
|
openGenmaiSpirit() {
|
||||||
return http.post('/api/genmai/open');
|
return http.post('/api/system/genmai/open');
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,21 +1,32 @@
|
|||||||
import { http } from './http'
|
import { http } from './http'
|
||||||
|
|
||||||
export const TOKEN_KEY = 'auth_token'
|
export interface LoginParams {
|
||||||
|
username: string
|
||||||
|
password: string
|
||||||
|
clientId?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AuthResponse {
|
||||||
|
token: string
|
||||||
|
permissions?: string
|
||||||
|
accountName?: string
|
||||||
|
expireTime?: string
|
||||||
|
}
|
||||||
|
|
||||||
export const authApi = {
|
export const authApi = {
|
||||||
login(params: { username: string; password: string }) {
|
login(params: LoginParams) {
|
||||||
return http.post('/monitor/account/login', params)
|
return http.post<{ data: AuthResponse }>('/monitor/account/login', params)
|
||||||
},
|
},
|
||||||
|
|
||||||
register(params: { username: string; password: string }) {
|
register(params: { username: string; password: string; deviceId?: string }) {
|
||||||
return http.post('/monitor/account/register', params)
|
return http.post<{ data: AuthResponse }>('/monitor/account/register', params)
|
||||||
},
|
},
|
||||||
|
|
||||||
checkUsername(username: string) {
|
checkUsername(username: string) {
|
||||||
return http.get('/monitor/account/check-username', { username })
|
return http.get<{ data: boolean }>('/monitor/account/check-username', { username })
|
||||||
},
|
},
|
||||||
|
|
||||||
verifyToken(token: string) {
|
verifyToken(token: string) {
|
||||||
return http.post('/monitor/account/verify', { token })
|
return http.post<{ data: AuthResponse }>('/monitor/account/verify', { token })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,28 +1,37 @@
|
|||||||
import { http } from './http'
|
import { http } from './http'
|
||||||
|
|
||||||
|
export interface DeviceItem {
|
||||||
|
deviceId: string
|
||||||
|
name?: string
|
||||||
|
os?: string
|
||||||
|
status: 'online' | 'offline'
|
||||||
|
lastActiveAt?: string
|
||||||
|
isCurrent?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DeviceQuota {
|
||||||
|
limit: number
|
||||||
|
used: number
|
||||||
|
}
|
||||||
|
|
||||||
export const deviceApi = {
|
export const deviceApi = {
|
||||||
getQuota(username: string) {
|
getQuota(username: string) {
|
||||||
// 直接调用 RuoYi 后端的设备配额接口
|
return http.get<{ data: DeviceQuota }>('/monitor/device/quota', { username })
|
||||||
return http.get('/monitor/device/quota', { username })
|
|
||||||
},
|
},
|
||||||
|
|
||||||
list(username: string) {
|
list(username: string) {
|
||||||
// 直接调用 RuoYi 后端的设备列表接口
|
return http.get<{ data: DeviceItem[] }>('/monitor/device/list', { username })
|
||||||
return http.get('/monitor/device/list', { username })
|
|
||||||
},
|
},
|
||||||
|
|
||||||
register(payload: { username: string }) {
|
register(payload: { username: string; deviceId: string; os?: string }) {
|
||||||
// 直接调用 RuoYi 后端的设备注册接口
|
|
||||||
return http.post('/monitor/device/register', payload)
|
return http.post('/monitor/device/register', payload)
|
||||||
},
|
},
|
||||||
|
|
||||||
remove(payload: { deviceId: string }) {
|
remove(payload: { deviceId: string }) {
|
||||||
// 直接调用 RuoYi 后端的设备移除接口
|
|
||||||
return http.post('/monitor/device/remove', payload)
|
return http.post('/monitor/device/remove', payload)
|
||||||
},
|
},
|
||||||
|
|
||||||
offline(payload: { deviceId: string }) {
|
offline(payload: { deviceId: string }) {
|
||||||
// 直接调用 RuoYi 后端的离线接口
|
|
||||||
return http.post('/monitor/device/offline', payload)
|
return http.post('/monitor/device/offline', payload)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
21
electron-vue-template/src/renderer/api/feedback.ts
Normal file
21
electron-vue-template/src/renderer/api/feedback.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { http } from './http'
|
||||||
|
|
||||||
|
export interface FeedbackParams {
|
||||||
|
username: string
|
||||||
|
deviceId: string
|
||||||
|
feedbackContent: string
|
||||||
|
logDate?: string
|
||||||
|
logFile?: File
|
||||||
|
}
|
||||||
|
|
||||||
|
export const feedbackApi = {
|
||||||
|
submit(data: FeedbackParams) {
|
||||||
|
const formData = new FormData()
|
||||||
|
formData.append('username', data.username)
|
||||||
|
formData.append('deviceId', data.deviceId)
|
||||||
|
formData.append('feedbackContent', data.feedbackContent)
|
||||||
|
if (data.logDate) formData.append('logDate', data.logDate)
|
||||||
|
if (data.logFile) formData.append('logFile', data.logFile)
|
||||||
|
return http.upload('/monitor/feedback/submit', formData)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,32 +1,31 @@
|
|||||||
// 极简 HTTP 工具:封装 GET/POST,按路径选择后端服务
|
// HTTP 工具:统一管理后端服务配置和请求
|
||||||
export type HttpMethod = 'GET' | 'POST';
|
export type HttpMethod = 'GET' | 'POST' | 'DELETE';
|
||||||
|
|
||||||
const BASE_CLIENT = 'http://localhost:8081'; // erp_client_sb
|
// 集中管理所有后端服务配置
|
||||||
const BASE_RUOYI = 'http://192.168.1.89:8085';
|
export const CONFIG = {
|
||||||
|
CLIENT_BASE: 'http://localhost:8081',
|
||||||
|
RUOYI_BASE: 'http://192.168.1.89:8085',
|
||||||
|
SSE_URL: 'http://192.168.1.89:8085/monitor/account/events'
|
||||||
|
} as const;
|
||||||
|
|
||||||
function resolveBase(path: string): string {
|
function resolveBase(path: string): string {
|
||||||
// 走 ruoyi-admin 的路径:鉴权、设备管理、版本、平台工具路由
|
// RuoYi 后端路径:鉴权、设备、反馈、版本、工具
|
||||||
if (path.startsWith('/monitor/account')) return BASE_RUOYI; // 账号认证相关
|
if (path.startsWith('/monitor/') || path.startsWith('/system/') || path.startsWith('/tool/banma')) {
|
||||||
if (path.startsWith('/monitor/device')) return BASE_RUOYI; // 设备管理
|
return CONFIG.RUOYI_BASE;
|
||||||
if (path.startsWith('/system/')) return BASE_RUOYI; // 版本控制器 VersionController
|
}
|
||||||
if (path.startsWith('/tool/banma')) return BASE_RUOYI; // 既有规则保留
|
// 其他走客户端服务
|
||||||
// 其他默认走客户端服务
|
return CONFIG.CLIENT_BASE;
|
||||||
return BASE_CLIENT;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 将对象转为查询字符串
|
|
||||||
function buildQuery(params?: Record<string, unknown>): string {
|
function buildQuery(params?: Record<string, unknown>): string {
|
||||||
if (!params) return '';
|
if (!params) return '';
|
||||||
const usp = new URLSearchParams();
|
const query = new URLSearchParams();
|
||||||
Object.entries(params).forEach(([key, value]) => {
|
Object.entries(params).forEach(([key, value]) => {
|
||||||
if (value === undefined || value === null) return;
|
if (value != null) query.append(key, String(value));
|
||||||
usp.append(key, String(value));
|
|
||||||
});
|
});
|
||||||
const queryString = usp.toString();
|
return query.toString() ? `?${query}` : '';
|
||||||
return queryString ? `?${queryString}` : '';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 统一请求入口:自动加上 BASE_URL、JSON 头与错误处理
|
|
||||||
async function request<T>(path: string, options: RequestInit): Promise<T> {
|
async function request<T>(path: string, options: RequestInit): Promise<T> {
|
||||||
const res = await fetch(`${resolveBase(path)}${path}`, {
|
const res = await fetch(`${resolveBase(path)}${path}`, {
|
||||||
credentials: 'omit',
|
credentials: 'omit',
|
||||||
@@ -34,22 +33,27 @@ async function request<T>(path: string, options: RequestInit): Promise<T> {
|
|||||||
...options,
|
...options,
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
...(options.headers || {}),
|
...options.headers
|
||||||
},
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
const text = await res.text().catch(() => '');
|
const text = await res.text().catch(() => '');
|
||||||
throw new Error(text || `HTTP ${res.status}`);
|
throw new Error(text || `HTTP ${res.status}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const contentType = res.headers.get('content-type') || '';
|
const contentType = res.headers.get('content-type') || '';
|
||||||
if (contentType.includes('application/json')) {
|
if (contentType.includes('application/json')) {
|
||||||
const json: any = await res.json();
|
const json: any = await res.json();
|
||||||
// 检查业务状态码
|
// 业务状态码判断:支持两种格式
|
||||||
|
// - erp_client_sb (本地服务): code=0 表示成功
|
||||||
|
// - RuoYi 后端: code=200 表示成功
|
||||||
if (json.code !== undefined && json.code !== 0 && json.code !== 200) {
|
if (json.code !== undefined && json.code !== 0 && json.code !== 200) {
|
||||||
throw new Error(json.msg || json.message || '请求失败');
|
throw new Error(json.msg || '请求失败');
|
||||||
}
|
}
|
||||||
return json as T;
|
return json as T;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (await res.text()) as unknown as T;
|
return (await res.text()) as unknown as T;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,40 +62,38 @@ export const http = {
|
|||||||
return request<T>(`${path}${buildQuery(params)}`, { method: 'GET' });
|
return request<T>(`${path}${buildQuery(params)}`, { method: 'GET' });
|
||||||
},
|
},
|
||||||
post<T>(path: string, body?: unknown) {
|
post<T>(path: string, body?: unknown) {
|
||||||
return request<T>(path, { method: 'POST', body: body ? JSON.stringify(body) : undefined });
|
return request<T>(path, {
|
||||||
|
method: 'POST',
|
||||||
|
body: body ? JSON.stringify(body) : undefined
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
delete<T>(path: string) {
|
delete<T>(path: string) {
|
||||||
return request<T>(path, { method: 'DELETE' });
|
return request<T>(path, { method: 'DELETE' });
|
||||||
},
|
},
|
||||||
// 用于无需读取响应体的 POST(如删除/心跳等),从根源避免读取中断
|
|
||||||
postVoid(path: string, body?: unknown) {
|
|
||||||
return fetch(`${resolveBase(path)}${path}`, {
|
|
||||||
method: 'POST',
|
|
||||||
body: body ? JSON.stringify(body) : undefined,
|
|
||||||
credentials: 'omit',
|
|
||||||
cache: 'no-store',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
}).then(res => {
|
|
||||||
if (!res.ok) return res.text().then(t => Promise.reject(new Error(t || `HTTP ${res.status}`)));
|
|
||||||
return undefined as unknown as void;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
// 文件上传:透传 FormData,不设置 Content-Type 让浏览器自动处理
|
|
||||||
upload<T>(path: string, form: FormData) {
|
upload<T>(path: string, form: FormData) {
|
||||||
const res = fetch(`${resolveBase(path)}${path}`, {
|
return fetch(`${resolveBase(path)}${path}`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: form,
|
body: form,
|
||||||
credentials: 'omit',
|
credentials: 'omit',
|
||||||
cache: 'no-store',
|
cache: 'no-store'
|
||||||
});
|
}).then(async res => {
|
||||||
return res.then(async response => {
|
if (!res.ok) {
|
||||||
if (!response.ok) {
|
const text = await res.text().catch(() => '');
|
||||||
const text = await response.text().catch(() => '');
|
throw new Error(text || `HTTP ${res.status}`);
|
||||||
throw new Error(text || `HTTP ${response.status}`);
|
|
||||||
}
|
}
|
||||||
return response.json() as Promise<T>;
|
const contentType = res.headers.get('content-type') || '';
|
||||||
|
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 || '请求失败');
|
||||||
|
}
|
||||||
|
return json as T;
|
||||||
|
}
|
||||||
|
return (await res.text()) as unknown as T;
|
||||||
});
|
});
|
||||||
},
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { http } from './http'
|
|||||||
|
|
||||||
export const updateApi = {
|
export const updateApi = {
|
||||||
getVersion() {
|
getVersion() {
|
||||||
return http.get('/api/update/version')
|
return http.get('/api/system/version')
|
||||||
},
|
},
|
||||||
|
|
||||||
checkUpdate(currentVersion: string) {
|
checkUpdate(currentVersion: string) {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, onMounted } from 'vue'
|
import { ref, computed, onMounted, nextTick } from 'vue'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
import {
|
import {
|
||||||
getSettings,
|
getSettings,
|
||||||
saveSettings,
|
saveSettings,
|
||||||
@@ -8,6 +8,9 @@ import {
|
|||||||
type Platform,
|
type Platform,
|
||||||
type PlatformExportSettings
|
type PlatformExportSettings
|
||||||
} from '../../utils/settings'
|
} from '../../utils/settings'
|
||||||
|
import { feedbackApi } from '../../api/feedback'
|
||||||
|
import { getToken, getUsernameFromToken } from '../../utils/token'
|
||||||
|
import { getOrCreateDeviceId } from '../../utils/deviceId'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
modelValue: boolean
|
modelValue: boolean
|
||||||
@@ -34,7 +37,15 @@ const platformSettings = ref<Record<Platform, PlatformExportSettings>>({
|
|||||||
zebra: { exportPath: '' }
|
zebra: { exportPath: '' }
|
||||||
})
|
})
|
||||||
|
|
||||||
const activeTab = ref<Platform>('amazon')
|
const activeTab = ref<string>('amazon')
|
||||||
|
const settingsMainRef = ref<HTMLElement | null>(null)
|
||||||
|
const isScrolling = ref(false)
|
||||||
|
|
||||||
|
// 反馈表单
|
||||||
|
const feedbackContent = ref('')
|
||||||
|
const selectedLogDate = ref('')
|
||||||
|
const feedbackSubmitting = ref(false)
|
||||||
|
const logDates = ref<string[]>([])
|
||||||
|
|
||||||
const show = computed({
|
const show = computed({
|
||||||
get: () => props.modelValue,
|
get: () => props.modelValue,
|
||||||
@@ -90,8 +101,114 @@ function resetAllSettings() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 滚动到指定区域
|
||||||
|
function scrollToSection(sectionKey: string) {
|
||||||
|
if (isScrolling.value) return
|
||||||
|
|
||||||
|
const element = document.getElementById(`section-${sectionKey}`)
|
||||||
|
if (element && settingsMainRef.value) {
|
||||||
|
isScrolling.value = true
|
||||||
|
activeTab.value = sectionKey
|
||||||
|
|
||||||
|
settingsMainRef.value.scrollTo({
|
||||||
|
top: element.offsetTop - 20,
|
||||||
|
behavior: 'smooth'
|
||||||
|
})
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
isScrolling.value = false
|
||||||
|
}, 500)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听滚动更新高亮
|
||||||
|
function handleScroll() {
|
||||||
|
if (isScrolling.value) return
|
||||||
|
|
||||||
|
const sections = ['amazon', 'rakuten', 'zebra', 'feedback']
|
||||||
|
const scrollTop = settingsMainRef.value?.scrollTop || 0
|
||||||
|
|
||||||
|
for (const key of sections) {
|
||||||
|
const element = document.getElementById(`section-${key}`)
|
||||||
|
if (element) {
|
||||||
|
const offsetTop = element.offsetTop - 50
|
||||||
|
const offsetBottom = offsetTop + element.offsetHeight
|
||||||
|
|
||||||
|
if (scrollTop >= offsetTop && scrollTop < offsetBottom) {
|
||||||
|
activeTab.value = key
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取可用的日志日期列表
|
||||||
|
async function loadLogDates() {
|
||||||
|
try {
|
||||||
|
const result = await (window as any).electronAPI.getLogDates()
|
||||||
|
if (result && result.dates) {
|
||||||
|
logDates.value = result.dates
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('获取日志日期列表失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提交反馈
|
||||||
|
async function submitFeedback() {
|
||||||
|
if (!feedbackContent.value.trim()) {
|
||||||
|
ElMessage.warning('请输入反馈内容')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const username = getUsernameFromToken()
|
||||||
|
if (!username) {
|
||||||
|
ElMessage.warning('请先登录')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
feedbackSubmitting.value = true
|
||||||
|
|
||||||
|
const deviceId = await getOrCreateDeviceId()
|
||||||
|
let logFile: File | undefined = undefined
|
||||||
|
|
||||||
|
// 如果选择了日志日期,读取日志文件
|
||||||
|
if (selectedLogDate.value) {
|
||||||
|
try {
|
||||||
|
const result = await (window as any).electronAPI.readLogFile(selectedLogDate.value)
|
||||||
|
if (result && result.content) {
|
||||||
|
// 将日志内容转换为File对象
|
||||||
|
const blob = new Blob([result.content], { type: 'text/plain' })
|
||||||
|
logFile = new File([blob], `spring-boot-${selectedLogDate.value}.log`, { type: 'text/plain' })
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('读取日志文件失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await feedbackApi.submit({
|
||||||
|
username,
|
||||||
|
deviceId,
|
||||||
|
feedbackContent: feedbackContent.value,
|
||||||
|
logDate: selectedLogDate.value || undefined,
|
||||||
|
logFile
|
||||||
|
})
|
||||||
|
|
||||||
|
ElMessage.success('反馈提交成功,感谢您的反馈!')
|
||||||
|
feedbackContent.value = ''
|
||||||
|
selectedLogDate.value = ''
|
||||||
|
show.value = false
|
||||||
|
} catch (error: any) {
|
||||||
|
ElMessage.error(error?.message || '提交失败,请稍后重试')
|
||||||
|
} finally {
|
||||||
|
feedbackSubmitting.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
loadAllSettings()
|
loadAllSettings()
|
||||||
|
loadLogDates()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -99,62 +216,191 @@ onMounted(() => {
|
|||||||
<el-dialog
|
<el-dialog
|
||||||
v-model="show"
|
v-model="show"
|
||||||
title="应用设置"
|
title="应用设置"
|
||||||
width="480px"
|
width="720px"
|
||||||
:close-on-click-modal="false"
|
:close-on-click-modal="false"
|
||||||
class="settings-dialog">
|
class="settings-dialog">
|
||||||
|
|
||||||
<div class="settings-content">
|
<div class="settings-layout">
|
||||||
<!-- 平台选择标签 -->
|
<!-- 左侧导航 -->
|
||||||
<div class="platform-tabs">
|
<div class="settings-sidebar">
|
||||||
<div
|
<div
|
||||||
v-for="platform in platforms"
|
v-for="platform in platforms"
|
||||||
:key="platform.key"
|
:key="platform.key"
|
||||||
:class="['platform-tab', { active: activeTab === platform.key }]"
|
:class="['sidebar-item', { active: activeTab === platform.key }]"
|
||||||
@click="activeTab = platform.key"
|
@click="scrollToSection(platform.key)">
|
||||||
:style="{ '--platform-color': platform.color }">
|
<span class="sidebar-icon">{{ platform.icon }}</span>
|
||||||
<span class="platform-icon">{{ platform.icon }}</span>
|
<span class="sidebar-text">{{ platform.name }}</span>
|
||||||
<span class="platform-name">{{ platform.name }}</span>
|
</div>
|
||||||
|
<div
|
||||||
|
:class="['sidebar-item', { active: activeTab === 'feedback' }]"
|
||||||
|
@click="scrollToSection('feedback')">
|
||||||
|
<span class="sidebar-icon">💬</span>
|
||||||
|
<span class="sidebar-text">反馈</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 当前平台设置 -->
|
<!-- 右侧内容 -->
|
||||||
<div class="setting-section">
|
<div class="settings-main" ref="settingsMainRef" @scroll="handleScroll">
|
||||||
<div class="section-title">
|
<!-- Amazon 设置 -->
|
||||||
<span class="title-icon">📁</span>
|
<div id="section-amazon" class="setting-section">
|
||||||
<span>{{ platforms.find(p => p.key === activeTab)?.name }} 导出设置</span>
|
<div class="section-title">
|
||||||
</div>
|
<span>Amazon 导出设置</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="setting-item">
|
<div class="setting-item">
|
||||||
<div class="setting-label">默认导出路径</div>
|
<div class="setting-label">默认导出路径</div>
|
||||||
<div class="setting-desc">设置 {{ platforms.find(p => p.key === activeTab)?.name }} Excel文件的默认保存位置</div>
|
<div class="setting-desc">设置 Amazon Excel文件的默认保存位置</div>
|
||||||
<div class="path-input-group">
|
<div class="path-input-group">
|
||||||
<el-input
|
<el-input
|
||||||
v-model="platformSettings[activeTab].exportPath"
|
v-model="platformSettings.amazon.exportPath"
|
||||||
placeholder="留空时自动弹出保存对话框"
|
placeholder="留空时自动弹出保存对话框"
|
||||||
readonly
|
readonly
|
||||||
class="path-input">
|
class="path-input">
|
||||||
<template #suffix>
|
<template #suffix>
|
||||||
<el-button
|
<el-button
|
||||||
size="small"
|
size="small"
|
||||||
type="primary"
|
type="primary"
|
||||||
@click="selectExportPath(activeTab)"
|
@click="selectExportPath('amazon')"
|
||||||
class="select-btn">
|
class="select-btn">
|
||||||
浏览
|
浏览
|
||||||
</el-button>
|
</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-input>
|
</el-input>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="setting-actions">
|
||||||
|
<el-button
|
||||||
|
size="small"
|
||||||
|
@click="resetPlatformSettings('amazon')">
|
||||||
|
重置此平台
|
||||||
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="setting-actions">
|
<!-- Rakuten 设置 -->
|
||||||
<el-button
|
<div id="section-rakuten" class="setting-section">
|
||||||
size="small"
|
<div class="section-title">
|
||||||
@click="resetPlatformSettings(activeTab)">
|
<span>Rakuten 导出设置</span>
|
||||||
重置此平台
|
</div>
|
||||||
</el-button>
|
|
||||||
|
<div class="setting-item">
|
||||||
|
<div class="setting-label">默认导出路径</div>
|
||||||
|
<div class="setting-desc">设置 Rakuten Excel文件的默认保存位置</div>
|
||||||
|
<div class="path-input-group">
|
||||||
|
<el-input
|
||||||
|
v-model="platformSettings.rakuten.exportPath"
|
||||||
|
placeholder="留空时自动弹出保存对话框"
|
||||||
|
readonly
|
||||||
|
class="path-input">
|
||||||
|
<template #suffix>
|
||||||
|
<el-button
|
||||||
|
size="small"
|
||||||
|
type="primary"
|
||||||
|
@click="selectExportPath('rakuten')"
|
||||||
|
class="select-btn">
|
||||||
|
浏览
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-input>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="setting-actions">
|
||||||
|
<el-button
|
||||||
|
size="small"
|
||||||
|
@click="resetPlatformSettings('rakuten')">
|
||||||
|
重置此平台
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Zebra 设置 -->
|
||||||
|
<div id="section-zebra" class="setting-section">
|
||||||
|
<div class="section-title">
|
||||||
|
<span>Zebra 导出设置</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="setting-item">
|
||||||
|
<div class="setting-label">默认导出路径</div>
|
||||||
|
<div class="setting-desc">设置 Zebra Excel文件的默认保存位置</div>
|
||||||
|
<div class="path-input-group">
|
||||||
|
<el-input
|
||||||
|
v-model="platformSettings.zebra.exportPath"
|
||||||
|
placeholder="留空时自动弹出保存对话框"
|
||||||
|
readonly
|
||||||
|
class="path-input">
|
||||||
|
<template #suffix>
|
||||||
|
<el-button
|
||||||
|
size="small"
|
||||||
|
type="primary"
|
||||||
|
@click="selectExportPath('zebra')"
|
||||||
|
class="select-btn">
|
||||||
|
浏览
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-input>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="setting-actions">
|
||||||
|
<el-button
|
||||||
|
size="small"
|
||||||
|
@click="resetPlatformSettings('zebra')">
|
||||||
|
重置此平台
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 反馈页面 -->
|
||||||
|
<div id="section-feedback" class="setting-section">
|
||||||
|
<div class="section-title">
|
||||||
|
<span>用户反馈</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="feedback-form">
|
||||||
|
<div class="setting-item">
|
||||||
|
<div class="setting-label">反馈内容 <span style="color: #F56C6C;">*</span></div>
|
||||||
|
<div class="setting-desc">请描述您遇到的问题或提出您的建议</div>
|
||||||
|
<el-input
|
||||||
|
v-model="feedbackContent"
|
||||||
|
type="textarea"
|
||||||
|
:rows="6"
|
||||||
|
placeholder="请输入您的反馈内容..."
|
||||||
|
maxlength="1000"
|
||||||
|
show-word-limit
|
||||||
|
style="margin-top: 8px;"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="setting-item">
|
||||||
|
<div class="setting-label">附带日志(可选)</div>
|
||||||
|
<div class="setting-desc">选择要附带的日志日期,有助于我们更快定位问题</div>
|
||||||
|
<el-select
|
||||||
|
v-model="selectedLogDate"
|
||||||
|
placeholder="请选择日志日期(可选)"
|
||||||
|
clearable
|
||||||
|
style="width: 100%; margin-top: 8px;">
|
||||||
|
<el-option
|
||||||
|
v-for="date in logDates"
|
||||||
|
:key="date"
|
||||||
|
:label="date"
|
||||||
|
:value="date"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="feedback-actions">
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
:loading="feedbackSubmitting"
|
||||||
|
@click="submitFeedback">
|
||||||
|
{{ feedbackSubmitting ? '提交中...' : '提交反馈' }}
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<template #footer>
|
<template #footer>
|
||||||
@@ -169,80 +415,102 @@ onMounted(() => {
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.settings-dialog :deep(.el-dialog__body) {
|
.settings-dialog :deep(.el-dialog__body) {
|
||||||
padding: 0 20px 20px 20px;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings-content {
|
.settings-layout {
|
||||||
max-height: 500px;
|
display: flex;
|
||||||
|
min-height: 450px;
|
||||||
|
max-height: 550px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 左侧导航 */
|
||||||
|
.settings-sidebar {
|
||||||
|
width: 160px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
background: #FFFFFF;
|
||||||
|
border-right: 1px solid #E5E6EB;
|
||||||
|
padding: 16px 0;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.platform-tabs {
|
.sidebar-item {
|
||||||
display: flex;
|
|
||||||
gap: 8px;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
padding: 4px;
|
|
||||||
background: #F8F9FA;
|
|
||||||
border-radius: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.platform-tab {
|
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
gap: 12px;
|
||||||
gap: 6px;
|
padding: 12px 20px;
|
||||||
padding: 8px 12px;
|
margin: 0 8px 4px;
|
||||||
border-radius: 6px;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
background: transparent;
|
color: #6B7280;
|
||||||
color: #606266;
|
font-size: 14px;
|
||||||
font-size: 13px;
|
user-select: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.platform-tab:hover {
|
.sidebar-item:hover {
|
||||||
background: rgba(255, 255, 255, 0.8);
|
background: #F3F6FF;
|
||||||
color: var(--platform-color);
|
color: #165DFF;
|
||||||
}
|
}
|
||||||
|
|
||||||
.platform-tab.active {
|
.sidebar-item.active {
|
||||||
background: #fff;
|
background: #F3F6FF;
|
||||||
color: var(--platform-color);
|
color: #165DFF;
|
||||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
.platform-icon {
|
.sidebar-item.active::before {
|
||||||
font-size: 16px;
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
width: 3px;
|
||||||
|
height: 60%;
|
||||||
|
background: #165DFF;
|
||||||
|
border-radius: 0 2px 2px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.platform-name {
|
.sidebar-icon {
|
||||||
font-size: 12px;
|
font-size: 18px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-text {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 右侧内容 */
|
||||||
|
.settings-main {
|
||||||
|
flex: 1;
|
||||||
|
padding: 24px;
|
||||||
|
overflow-y: auto;
|
||||||
|
background: #F9FAFB;
|
||||||
}
|
}
|
||||||
|
|
||||||
.setting-section {
|
.setting-section {
|
||||||
margin-bottom: 24px;
|
background: #FFFFFF;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 24px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06);
|
||||||
|
border: 1px solid #E5E6EB;
|
||||||
}
|
}
|
||||||
|
|
||||||
.section-title {
|
.section-title {
|
||||||
display: flex;
|
font-size: 17px;
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #303133;
|
color: #1F2937;
|
||||||
margin-bottom: 16px;
|
margin-bottom: 8px;
|
||||||
padding-bottom: 8px;
|
|
||||||
border-bottom: 1px solid #EBEEF5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.title-icon {
|
|
||||||
font-size: 18px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.setting-item {
|
.setting-item {
|
||||||
margin-bottom: 20px;
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-item:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.setting-row {
|
.setting-row {
|
||||||
@@ -258,18 +526,20 @@ onMounted(() => {
|
|||||||
.setting-label {
|
.setting-label {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
color: #303133;
|
color: #1F2937;
|
||||||
margin-bottom: 4px;
|
margin-bottom: 8px;
|
||||||
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.setting-desc {
|
.setting-desc {
|
||||||
font-size: 12px;
|
font-size: 13px;
|
||||||
color: #909399;
|
color: #86909C;
|
||||||
line-height: 1.4;
|
line-height: 1.6;
|
||||||
|
margin-bottom: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.path-input-group {
|
.path-input-group {
|
||||||
margin-top: 8px;
|
margin-top: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.path-input {
|
.path-input {
|
||||||
@@ -277,7 +547,19 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.path-input :deep(.el-input__wrapper) {
|
.path-input :deep(.el-input__wrapper) {
|
||||||
padding-right: 80px;
|
padding-right: 90px;
|
||||||
|
border-color: #E5E6EB;
|
||||||
|
box-shadow: none;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.path-input :deep(.el-input__wrapper:hover) {
|
||||||
|
border-color: #165DFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
.path-input :deep(.el-input__wrapper.is-focus) {
|
||||||
|
border-color: #165DFF;
|
||||||
|
box-shadow: 0 0 0 2px rgba(22, 93, 255, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.select-btn {
|
.select-btn {
|
||||||
@@ -285,9 +567,16 @@ onMounted(() => {
|
|||||||
right: 8px;
|
right: 8px;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
transform: translateY(-50%);
|
transform: translateY(-50%);
|
||||||
height: 24px;
|
height: 28px;
|
||||||
padding: 0 12px;
|
padding: 0 16px;
|
||||||
font-size: 12px;
|
font-size: 13px;
|
||||||
|
background: #165DFF;
|
||||||
|
border-color: #165DFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select-btn:hover {
|
||||||
|
background: #4080FF;
|
||||||
|
border-color: #4080FF;
|
||||||
}
|
}
|
||||||
|
|
||||||
.info-content {
|
.info-content {
|
||||||
@@ -308,20 +597,119 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.setting-actions {
|
.setting-actions {
|
||||||
margin-top: 16px;
|
margin-top: 20px;
|
||||||
padding-top: 12px;
|
padding-top: 20px;
|
||||||
border-top: 1px solid #EBEEF5;
|
border-top: 1px solid #E5E6EB;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-actions :deep(.el-button) {
|
||||||
|
border-color: #E5E6EB;
|
||||||
|
color: #6B7280;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-actions :deep(.el-button:hover) {
|
||||||
|
border-color: #165DFF;
|
||||||
|
color: #165DFF;
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings-dialog :deep(.el-dialog__header) {
|
.settings-dialog :deep(.el-dialog__header) {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding-right: 40px; /* 为右侧关闭按钮留出空间 */
|
padding-right: 40px;
|
||||||
|
border-bottom: 1px solid #E5E6EB;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-dialog :deep(.el-dialog__title) {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1F2937;
|
||||||
|
font-size: 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dialog-footer {
|
.dialog-footer {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
gap: 8px;
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-footer :deep(.el-button) {
|
||||||
|
padding: 8px 20px;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-footer :deep(.el-button--primary) {
|
||||||
|
background: #165DFF;
|
||||||
|
border-color: #165DFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-footer :deep(.el-button--primary:hover) {
|
||||||
|
background: #4080FF;
|
||||||
|
border-color: #4080FF;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-footer :deep(.el-button--default) {
|
||||||
|
border-color: #E5E6EB;
|
||||||
|
color: #6B7280;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-footer :deep(.el-button--default:hover) {
|
||||||
|
border-color: #165DFF;
|
||||||
|
color: #165DFF;
|
||||||
|
background: #F3F6FF;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feedback-form {
|
||||||
|
padding-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feedback-form :deep(.el-textarea__inner) {
|
||||||
|
border-color: #E5E6EB;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feedback-form :deep(.el-textarea__inner:hover) {
|
||||||
|
border-color: #165DFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feedback-form :deep(.el-textarea__inner:focus) {
|
||||||
|
border-color: #165DFF;
|
||||||
|
box-shadow: 0 0 0 2px rgba(22, 93, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.feedback-form :deep(.el-select) {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feedback-form :deep(.el-select .el-input__wrapper) {
|
||||||
|
border-color: #E5E6EB;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feedback-form :deep(.el-select .el-input__wrapper:hover) {
|
||||||
|
border-color: #165DFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feedback-form :deep(.el-select .el-input__wrapper.is-focus) {
|
||||||
|
border-color: #165DFF;
|
||||||
|
box-shadow: 0 0 0 2px rgba(22, 93, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.feedback-actions {
|
||||||
|
margin-top: 24px;
|
||||||
|
padding-top: 20px;
|
||||||
|
border-top: 1px solid #E5E6EB;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feedback-actions :deep(.el-button--primary) {
|
||||||
|
background: #165DFF;
|
||||||
|
border-color: #165DFF;
|
||||||
|
padding: 9px 24px;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feedback-actions :deep(.el-button--primary:hover) {
|
||||||
|
background: #4080FF;
|
||||||
|
border-color: #4080FF;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|||||||
@@ -1,29 +1,22 @@
|
|||||||
const BASE_CLIENT = 'http://localhost:8081'
|
import { CONFIG } from '../api/http'
|
||||||
|
|
||||||
const DEVICE_ID_KEY = 'device_id'
|
const DEVICE_ID_KEY = 'device_id'
|
||||||
|
|
||||||
// 从客户端服务获取硬件UUID(通过 wmic 命令)
|
|
||||||
async function fetchDeviceIdFromClient(): Promise<string> {
|
async function fetchDeviceIdFromClient(): Promise<string> {
|
||||||
const response = await fetch(`${BASE_CLIENT}/api/device-id`, {
|
const response = await fetch(`${CONFIG.CLIENT_BASE}/api/system/device-id`, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
credentials: 'omit',
|
credentials: 'omit',
|
||||||
cache: 'no-store'
|
cache: 'no-store'
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) throw new Error('获取设备ID失败')
|
||||||
throw new Error('获取设备ID失败')
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await response.json()
|
const result = await response.json()
|
||||||
const deviceId = result?.data
|
if (!result?.data) throw new Error('设备ID为空')
|
||||||
|
|
||||||
if (!deviceId) {
|
return result.data
|
||||||
throw new Error('设备ID为空')
|
|
||||||
}
|
|
||||||
|
|
||||||
return deviceId
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取或创建设备ID(优先读缓存,没有则从客户端服务获取硬件UUID)
|
|
||||||
export async function getOrCreateDeviceId(): Promise<string> {
|
export async function getOrCreateDeviceId(): Promise<string> {
|
||||||
const cached = localStorage.getItem(DEVICE_ID_KEY)
|
const cached = localStorage.getItem(DEVICE_ID_KEY)
|
||||||
if (cached) return cached
|
if (cached) return cached
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
export async function convertImageToBase64ViaProxy(imageUrl: string, maxSize: number = 80): Promise<string | null> {
|
export async function convertImageToBase64ViaProxy(imageUrl: string, maxSize: number = 80): Promise<string | null> {
|
||||||
if (!imageUrl) return null
|
if (!imageUrl) return null
|
||||||
try {
|
try {
|
||||||
const proxyUrl = `http://127.0.0.1:8081/api/proxy/image-url?url=${encodeURIComponent(imageUrl)}`
|
const proxyUrl = `http://127.0.0.1:8081/api/system/proxy/image?url=${encodeURIComponent(imageUrl)}`
|
||||||
const response = await fetch(proxyUrl)
|
const response = await fetch(proxyUrl)
|
||||||
if (!response.ok) return null
|
if (!response.ok) return null
|
||||||
|
|
||||||
|
|||||||
37
electron-vue-template/src/renderer/utils/token.ts
Normal file
37
electron-vue-template/src/renderer/utils/token.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
// Token 工具函数
|
||||||
|
export const TOKEN_KEY = 'auth_token';
|
||||||
|
|
||||||
|
export function getToken(): string | null {
|
||||||
|
return localStorage.getItem(TOKEN_KEY);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setToken(token: string): void {
|
||||||
|
localStorage.setItem(TOKEN_KEY, token);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function removeToken(): void {
|
||||||
|
localStorage.removeItem(TOKEN_KEY);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getUsernameFromToken(token?: string): string {
|
||||||
|
try {
|
||||||
|
const t = token || getToken();
|
||||||
|
if (!t) return '';
|
||||||
|
const payload = JSON.parse(atob(t.split('.')[1]));
|
||||||
|
return payload.username || payload.sub || '';
|
||||||
|
} catch {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getClientIdFromToken(token?: string): string {
|
||||||
|
try {
|
||||||
|
const t = token || getToken();
|
||||||
|
if (!t) return '';
|
||||||
|
const payload = JSON.parse(atob(t.split('.')[1]));
|
||||||
|
return payload.clientId || '';
|
||||||
|
} catch {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
package com.tashow.erp.controller;
|
|
||||||
import com.tashow.erp.entity.AuthTokenEntity;
|
|
||||||
import com.tashow.erp.repository.AuthTokenRepository;
|
|
||||||
import com.tashow.erp.utils.JsonData;
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.web.bind.annotation.*;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 客户端本地服务控制器
|
|
||||||
*/
|
|
||||||
@RestController
|
|
||||||
@RequestMapping("/api")
|
|
||||||
public class AuthController {
|
|
||||||
@Autowired
|
|
||||||
private AuthTokenRepository authTokenRepository;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 保存认证密钥
|
|
||||||
*/
|
|
||||||
@PostMapping("/auth/save")
|
|
||||||
public JsonData saveAuth(@RequestBody Map<String, Object> data) {
|
|
||||||
String serviceName = (String) data.get("serviceName");
|
|
||||||
String authKey = (String) data.get("authKey");
|
|
||||||
if (serviceName == null || authKey == null) return JsonData.buildError("serviceName和authKey不能为空");
|
|
||||||
AuthTokenEntity entity = authTokenRepository.findByServiceName(serviceName).orElse(new AuthTokenEntity());
|
|
||||||
entity.setServiceName(serviceName);
|
|
||||||
entity.setToken(authKey);
|
|
||||||
authTokenRepository.save(entity);
|
|
||||||
return JsonData.buildSuccess("认证信息保存成功");
|
|
||||||
}
|
|
||||||
|
|
||||||
@GetMapping("/auth/get")
|
|
||||||
public JsonData getAuth(@RequestParam String serviceName) {
|
|
||||||
return JsonData.buildSuccess(authTokenRepository.findByServiceName(serviceName).map(AuthTokenEntity::getToken).orElse(null));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 删除认证密钥
|
|
||||||
*/
|
|
||||||
@DeleteMapping("/auth/remove")
|
|
||||||
public JsonData removeAuth(@RequestParam String serviceName) {
|
|
||||||
authTokenRepository.findByServiceName(serviceName).ifPresent(authTokenRepository::delete);
|
|
||||||
return JsonData.buildSuccess("认证信息删除成功");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取设备ID
|
|
||||||
*/
|
|
||||||
@GetMapping("/device-id")
|
|
||||||
public JsonData getDeviceId() {
|
|
||||||
String deviceId = com.tashow.erp.utils.DeviceUtils.generateDeviceId();
|
|
||||||
return JsonData.buildSuccess(deviceId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
package com.tashow.erp.controller;
|
|
||||||
|
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
|
||||||
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 配置信息控制器
|
|
||||||
*/
|
|
||||||
@RestController
|
|
||||||
@RequestMapping("/api/config")
|
|
||||||
public class ConfigController {
|
|
||||||
|
|
||||||
@Value("${api.server.base-url}")
|
|
||||||
private String serverBaseUrl;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取服务器配置
|
|
||||||
*/
|
|
||||||
@GetMapping("/server")
|
|
||||||
public Map<String, Object> getServerConfig() {
|
|
||||||
return Map.of(
|
|
||||||
"baseUrl", serverBaseUrl,
|
|
||||||
"sseUrl", serverBaseUrl + "/monitor/account/events"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
package com.tashow.erp.controller;
|
|
||||||
|
|
||||||
import com.tashow.erp.service.IGenmaiService;
|
|
||||||
import com.tashow.erp.utils.JsonData;
|
|
||||||
import com.tashow.erp.utils.LoggerUtil;
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.web.bind.annotation.*;
|
|
||||||
|
|
||||||
@RestController
|
|
||||||
@RequestMapping("/api/genmai")
|
|
||||||
public class GenmaiController {
|
|
||||||
private static final Logger logger = LoggerUtil.getLogger(GenmaiController.class);
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
private IGenmaiService genmaiService;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 打开跟卖精灵网页
|
|
||||||
*/
|
|
||||||
@PostMapping("/open")
|
|
||||||
public void openGenmaiWebsite() {
|
|
||||||
genmaiService.openGenmaiWebsite();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
package com.tashow.erp.controller;
|
|
||||||
|
|
||||||
import org.springframework.stereotype.Controller;
|
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
|
||||||
|
|
||||||
@Controller
|
|
||||||
public class HomeController {
|
|
||||||
|
|
||||||
@GetMapping("/")
|
|
||||||
public String home() {
|
|
||||||
return "redirect:/html/erp-dashboard.html";
|
|
||||||
}
|
|
||||||
|
|
||||||
@GetMapping("/erp")
|
|
||||||
public String erp() {
|
|
||||||
return "redirect:/html/erp-dashboard.html";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,116 +0,0 @@
|
|||||||
package com.tashow.erp.controller;
|
|
||||||
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.http.*;
|
|
||||||
import org.springframework.web.bind.annotation.*;
|
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
|
||||||
import org.springframework.web.client.RestTemplate;
|
|
||||||
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 代理控制器,用于解决CORS跨域问题
|
|
||||||
*/
|
|
||||||
@RestController
|
|
||||||
@RequestMapping("/api/proxy")
|
|
||||||
public class ProxyController {
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
private RestTemplate restTemplate;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 代理获取图片
|
|
||||||
* @param requestBody 包含图片URL的请求体
|
|
||||||
* @return 图片字节数组
|
|
||||||
*/
|
|
||||||
@PostMapping("/image")
|
|
||||||
public ResponseEntity<byte[]> proxyImage(@RequestBody Map<String, String> requestBody) {
|
|
||||||
String imageUrl = requestBody.get("imageUrl");
|
|
||||||
if (imageUrl == null || imageUrl.isEmpty()) {
|
|
||||||
return ResponseEntity.badRequest().build();
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 设置请求头
|
|
||||||
HttpHeaders headers = new HttpHeaders();
|
|
||||||
headers.set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36");
|
|
||||||
headers.set("Accept", "image/jpeg, image/png, image/webp, image/*");
|
|
||||||
|
|
||||||
HttpEntity<String> entity = new HttpEntity<>(headers);
|
|
||||||
|
|
||||||
// 发送请求获取图片
|
|
||||||
ResponseEntity<byte[]> response = restTemplate.exchange(
|
|
||||||
imageUrl,
|
|
||||||
HttpMethod.GET,
|
|
||||||
entity,
|
|
||||||
byte[].class
|
|
||||||
);
|
|
||||||
|
|
||||||
// 设置响应头
|
|
||||||
HttpHeaders responseHeaders = new HttpHeaders();
|
|
||||||
responseHeaders.setContentType(MediaType.IMAGE_JPEG);
|
|
||||||
|
|
||||||
return new ResponseEntity<>(response.getBody(), responseHeaders, HttpStatus.OK);
|
|
||||||
} catch (Exception e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 通过URL参数代理获取图片
|
|
||||||
* @param imageUrl 图片URL
|
|
||||||
* @return 图片字节数组
|
|
||||||
*/
|
|
||||||
@GetMapping("/image-url")
|
|
||||||
public ResponseEntity<byte[]> proxyImageByUrl(@RequestParam("url") String imageUrl) {
|
|
||||||
if (imageUrl == null || imageUrl.isEmpty()) {
|
|
||||||
System.err.println("图片代理请求失败: 图片URL为空");
|
|
||||||
return ResponseEntity.badRequest().build();
|
|
||||||
}
|
|
||||||
|
|
||||||
System.out.println("代理图片请求: " + imageUrl);
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 设置请求头
|
|
||||||
HttpHeaders headers = new HttpHeaders();
|
|
||||||
headers.set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36");
|
|
||||||
headers.set("Accept", "image/jpeg, image/png, image/webp, image/*");
|
|
||||||
headers.set("Referer", "https://item.rakuten.co.jp/");
|
|
||||||
|
|
||||||
HttpEntity<String> entity = new HttpEntity<>(headers);
|
|
||||||
|
|
||||||
// 发送请求获取图片
|
|
||||||
ResponseEntity<byte[]> response = restTemplate.exchange(
|
|
||||||
imageUrl,
|
|
||||||
HttpMethod.GET,
|
|
||||||
entity,
|
|
||||||
byte[].class
|
|
||||||
);
|
|
||||||
|
|
||||||
System.out.println("图片代理成功,响应大小: " + (response.getBody() != null ? response.getBody().length : 0) + " bytes");
|
|
||||||
|
|
||||||
// 设置响应头,支持缓存以提升JavaFX WebView性能
|
|
||||||
HttpHeaders responseHeaders = new HttpHeaders();
|
|
||||||
|
|
||||||
// 尝试从原始响应中获取Content-Type
|
|
||||||
String contentType = response.getHeaders().getFirst("Content-Type");
|
|
||||||
if (contentType != null && contentType.startsWith("image/")) {
|
|
||||||
responseHeaders.setContentType(MediaType.parseMediaType(contentType));
|
|
||||||
} else {
|
|
||||||
responseHeaders.setContentType(MediaType.IMAGE_JPEG);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 设置缓存头以提升性能
|
|
||||||
responseHeaders.setCacheControl("max-age=3600");
|
|
||||||
// 删除手动CORS设置,使用WebConfig中的全局CORS配置
|
|
||||||
|
|
||||||
return new ResponseEntity<>(response.getBody(), responseHeaders, HttpStatus.OK);
|
|
||||||
} catch (Exception e) {
|
|
||||||
System.err.println("图片代理失败: " + imageUrl + " - " + e.getMessage());
|
|
||||||
e.printStackTrace();
|
|
||||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,172 @@
|
|||||||
|
package com.tashow.erp.controller;
|
||||||
|
|
||||||
|
import com.tashow.erp.entity.AuthTokenEntity;
|
||||||
|
import com.tashow.erp.repository.AuthTokenRepository;
|
||||||
|
import com.tashow.erp.service.IGenmaiService;
|
||||||
|
import com.tashow.erp.utils.JsonData;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.http.*;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
import org.springframework.web.client.RestTemplate;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 系统级接口控制器
|
||||||
|
* 整合:认证、配置、版本、工具、代理等功能
|
||||||
|
*/
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/system")
|
||||||
|
public class SystemController {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private AuthTokenRepository authTokenRepository;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private IGenmaiService genmaiService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private RestTemplate restTemplate;
|
||||||
|
|
||||||
|
@Value("${project.version:2.3.6}")
|
||||||
|
private String currentVersion;
|
||||||
|
|
||||||
|
@Value("${project.build.time:}")
|
||||||
|
private String buildTime;
|
||||||
|
|
||||||
|
@Value("${api.server.base-url}")
|
||||||
|
private String serverBaseUrl;
|
||||||
|
|
||||||
|
// ==================== 认证管理 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存认证密钥
|
||||||
|
*/
|
||||||
|
@PostMapping("/auth/save")
|
||||||
|
public JsonData saveAuth(@RequestBody Map<String, Object> data) {
|
||||||
|
String serviceName = (String) data.get("serviceName");
|
||||||
|
String authKey = (String) data.get("authKey");
|
||||||
|
if (serviceName == null || authKey == null) {
|
||||||
|
return JsonData.buildError("serviceName和authKey不能为空");
|
||||||
|
}
|
||||||
|
AuthTokenEntity entity = authTokenRepository.findByServiceName(serviceName)
|
||||||
|
.orElse(new AuthTokenEntity());
|
||||||
|
entity.setServiceName(serviceName);
|
||||||
|
entity.setToken(authKey);
|
||||||
|
authTokenRepository.save(entity);
|
||||||
|
return JsonData.buildSuccess("认证信息保存成功");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取认证密钥
|
||||||
|
*/
|
||||||
|
@GetMapping("/auth/get")
|
||||||
|
public JsonData getAuth(@RequestParam String serviceName) {
|
||||||
|
return JsonData.buildSuccess(authTokenRepository.findByServiceName(serviceName)
|
||||||
|
.map(AuthTokenEntity::getToken)
|
||||||
|
.orElse(null));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除认证密钥
|
||||||
|
*/
|
||||||
|
@DeleteMapping("/auth/remove")
|
||||||
|
public JsonData removeAuth(@RequestParam String serviceName) {
|
||||||
|
authTokenRepository.findByServiceName(serviceName)
|
||||||
|
.ifPresent(authTokenRepository::delete);
|
||||||
|
return JsonData.buildSuccess("认证信息删除成功");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 设备管理 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取设备ID
|
||||||
|
*/
|
||||||
|
@GetMapping("/device-id")
|
||||||
|
public JsonData getDeviceId() {
|
||||||
|
String deviceId = com.tashow.erp.utils.DeviceUtils.generateDeviceId();
|
||||||
|
return JsonData.buildSuccess(deviceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 版本信息 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前版本号
|
||||||
|
*/
|
||||||
|
@GetMapping("/version")
|
||||||
|
public Map<String, Object> getVersion() {
|
||||||
|
Map<String, Object> result = new HashMap<>();
|
||||||
|
result.put("success", true);
|
||||||
|
result.put("currentVersion", currentVersion);
|
||||||
|
result.put("buildTime", buildTime);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 配置信息 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取服务器配置
|
||||||
|
*/
|
||||||
|
@GetMapping("/config/server")
|
||||||
|
public Map<String, Object> getServerConfig() {
|
||||||
|
return Map.of(
|
||||||
|
"baseUrl", serverBaseUrl,
|
||||||
|
"sseUrl", serverBaseUrl + "/monitor/account/events"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 工具功能 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 打开跟卖精灵网页
|
||||||
|
*/
|
||||||
|
@PostMapping("/genmai/open")
|
||||||
|
public void openGenmaiWebsite() {
|
||||||
|
genmaiService.openGenmaiWebsite();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 图片代理 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 代理获取图片(解决CORS跨域问题)
|
||||||
|
*/
|
||||||
|
@GetMapping("/proxy/image")
|
||||||
|
public ResponseEntity<byte[]> proxyImage(@RequestParam("url") String imageUrl) {
|
||||||
|
if (imageUrl == null || imageUrl.isEmpty()) {
|
||||||
|
return ResponseEntity.badRequest().build();
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
HttpHeaders headers = new HttpHeaders();
|
||||||
|
headers.set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36");
|
||||||
|
headers.set("Accept", "image/jpeg, image/png, image/webp, image/*");
|
||||||
|
headers.set("Referer", "https://item.rakuten.co.jp/");
|
||||||
|
|
||||||
|
HttpEntity<String> entity = new HttpEntity<>(headers);
|
||||||
|
|
||||||
|
ResponseEntity<byte[]> response = restTemplate.exchange(
|
||||||
|
imageUrl,
|
||||||
|
HttpMethod.GET,
|
||||||
|
entity,
|
||||||
|
byte[].class
|
||||||
|
);
|
||||||
|
|
||||||
|
HttpHeaders responseHeaders = new HttpHeaders();
|
||||||
|
String contentType = response.getHeaders().getFirst("Content-Type");
|
||||||
|
if (contentType != null && contentType.startsWith("image/")) {
|
||||||
|
responseHeaders.setContentType(MediaType.parseMediaType(contentType));
|
||||||
|
} else {
|
||||||
|
responseHeaders.setContentType(MediaType.IMAGE_JPEG);
|
||||||
|
}
|
||||||
|
responseHeaders.setCacheControl("max-age=3600");
|
||||||
|
|
||||||
|
return new ResponseEntity<>(response.getBody(), responseHeaders, HttpStatus.OK);
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
package com.tashow.erp.controller;
|
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
|
||||||
import org.springframework.web.bind.annotation.*;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Web版本信息控制器
|
|
||||||
*
|
|
||||||
* @author Claude
|
|
||||||
*/
|
|
||||||
@RestController
|
|
||||||
@RequestMapping("/api/update")
|
|
||||||
public class UpdateController {
|
|
||||||
@Value("${project.version:2.3.6}")
|
|
||||||
private String currentVersion;
|
|
||||||
@Value("${project.build.time:}")
|
|
||||||
private String buildTime;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取当前版本号
|
|
||||||
*
|
|
||||||
* @return 当前版本号
|
|
||||||
*/
|
|
||||||
@GetMapping("/version")
|
|
||||||
public Map<String, Object> getVersion() {
|
|
||||||
Map<String, Object> result = new HashMap<>();
|
|
||||||
result.put("success", true);
|
|
||||||
result.put("currentVersion", currentVersion);
|
|
||||||
result.put("buildTime", buildTime);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
package com.tashow.erp.test;
|
||||||
|
|
||||||
|
import com.tashow.erp.utils.DeviceUtils;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设备ID获取测试
|
||||||
|
* 独立运行,不依赖 Spring Boot
|
||||||
|
*/
|
||||||
|
public class DeviceIdTest {
|
||||||
|
public static void main(String[] args) {
|
||||||
|
System.out.println("=================================");
|
||||||
|
System.out.println("设备ID获取测试");
|
||||||
|
System.out.println("=================================\n");
|
||||||
|
|
||||||
|
try {
|
||||||
|
String deviceId = DeviceUtils.generateDeviceId();
|
||||||
|
System.out.println("✓ 成功获取设备ID: " + deviceId);
|
||||||
|
System.out.println("\n设备ID格式说明:");
|
||||||
|
System.out.println(" MGUID_ - Windows MachineGuid(最可靠)");
|
||||||
|
System.out.println(" HW_ - 硬件UUID");
|
||||||
|
System.out.println(" CPU_ - 处理器ID");
|
||||||
|
System.out.println(" MB_ - 主板序列号");
|
||||||
|
System.out.println(" MAC_ - MAC地址");
|
||||||
|
System.out.println(" SYS_ - 系统信息组合");
|
||||||
|
|
||||||
|
// 再次获取,验证稳定性
|
||||||
|
System.out.println("\n验证稳定性(再次获取):");
|
||||||
|
String deviceId2 = DeviceUtils.generateDeviceId();
|
||||||
|
System.out.println("第二次获取: " + deviceId2);
|
||||||
|
|
||||||
|
if (deviceId.equals(deviceId2)) {
|
||||||
|
System.out.println("✓ 设备ID稳定,两次获取结果一致");
|
||||||
|
} else {
|
||||||
|
System.out.println("✗ 警告:设备ID不稳定,两次获取结果不同");
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
System.err.println("✗ 获取设备ID失败:");
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
|
||||||
|
System.out.println("\n=================================");
|
||||||
|
System.out.println("测试完成");
|
||||||
|
System.out.println("=================================");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -7,7 +7,7 @@ import org.springframework.web.bind.annotation.RestController;
|
|||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
public class aa {
|
public class aa {
|
||||||
@GetMapping("/a")
|
@GetMapping("/aa")
|
||||||
public String aa() {
|
public String aa() {
|
||||||
DeviceUtils deviceUtils = new DeviceUtils();
|
DeviceUtils deviceUtils = new DeviceUtils();
|
||||||
return deviceUtils.generateDeviceId();
|
return deviceUtils.generateDeviceId();
|
||||||
|
|||||||
@@ -24,8 +24,6 @@ public class DeviceUtils {
|
|||||||
|
|
||||||
public static String generateDeviceId() {
|
public static String generateDeviceId() {
|
||||||
String deviceId = null;
|
String deviceId = null;
|
||||||
log.info("========== 开始生成设备ID ==========");
|
|
||||||
|
|
||||||
// 策略1: Windows MachineGuid(注册表)
|
// 策略1: Windows MachineGuid(注册表)
|
||||||
deviceId = getMachineGuid();
|
deviceId = getMachineGuid();
|
||||||
if (deviceId != null) return deviceId;
|
if (deviceId != null) return deviceId;
|
||||||
|
|||||||
@@ -3,6 +3,24 @@
|
|||||||
<!-- 固定日志路径到系统公共数据目录 -->
|
<!-- 固定日志路径到系统公共数据目录 -->
|
||||||
<property name="LOG_HOME" value="C:/ProgramData/erp-logs" />
|
<property name="LOG_HOME" value="C:/ProgramData/erp-logs" />
|
||||||
|
|
||||||
|
<!--
|
||||||
|
日志上报说明:
|
||||||
|
日志文件按天滚动存储在 ${LOG_HOME} 目录下
|
||||||
|
格式:spring-boot-yyyy-MM-dd.log
|
||||||
|
|
||||||
|
用户反馈系统集成说明:
|
||||||
|
- 客户端应用(Electron)可以读取本地日志文件
|
||||||
|
- 用户在"设置-反馈"页面提交反馈时,可选择附带某一天的日志文件
|
||||||
|
- 日志文件将随反馈内容一起上传到服务器
|
||||||
|
- 服务器存储路径:C:/ProgramData/erp-logs/feedback/ (Windows) 或 /opt/erp/feedback-logs/ (Linux)
|
||||||
|
- 管理员可在后台管理界面查看反馈并下载相应日志文件
|
||||||
|
|
||||||
|
日志保留策略:
|
||||||
|
- 本地日志保留30天
|
||||||
|
- 总大小限制1GB
|
||||||
|
- 超过限制时自动删除最旧的日志文件
|
||||||
|
-->
|
||||||
|
|
||||||
<!-- 控制台输出 -->
|
<!-- 控制台输出 -->
|
||||||
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
|
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
|
||||||
<encoder>
|
<encoder>
|
||||||
|
|||||||
@@ -130,9 +130,8 @@ public class ClientAccountController extends BaseController {
|
|||||||
public AjaxResult login(@RequestBody Map<String, String> loginData) {
|
public AjaxResult login(@RequestBody Map<String, String> loginData) {
|
||||||
String username = loginData.get("username");
|
String username = loginData.get("username");
|
||||||
String password = loginData.get("password");
|
String password = loginData.get("password");
|
||||||
if (StringUtils.isEmpty(username) || StringUtils.isEmpty(password)) {
|
String clientId = loginData.get("clientId");
|
||||||
return AjaxResult.error("用户名和密码不能为空");
|
|
||||||
}
|
|
||||||
ClientAccount account = clientAccountService.selectClientAccountByUsername(username);
|
ClientAccount account = clientAccountService.selectClientAccountByUsername(username);
|
||||||
if (account == null || !passwordEncoder.matches(password, account.getPassword())) {
|
if (account == null || !passwordEncoder.matches(password, account.getPassword())) {
|
||||||
return AjaxResult.error("用户名或密码错误");
|
return AjaxResult.error("用户名或密码错误");
|
||||||
@@ -142,24 +141,32 @@ public class ClientAccountController extends BaseController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 检查设备数量限制
|
// 检查设备数量限制
|
||||||
String clientId = loginData.get("clientId");
|
|
||||||
int deviceLimit = account.getDeviceLimit();
|
int deviceLimit = account.getDeviceLimit();
|
||||||
List<ClientDevice> userDevices = clientDeviceMapper.selectByUsername(username);
|
List<ClientDevice> userDevices = clientDeviceMapper.selectByUsername(username);
|
||||||
int userDevice = userDevices.size();
|
int userDevice = userDevices.size();
|
||||||
boolean exists = userDevices.stream()
|
boolean exists = userDevices.stream().anyMatch(d -> clientId.equals(d.getDeviceId()));
|
||||||
.anyMatch(d -> clientId.equals(d.getDeviceId()));
|
if (exists) userDevice--;
|
||||||
if(exists)userDevice--;
|
|
||||||
if (userDevice >= deviceLimit) {
|
if (userDevice >= deviceLimit) {
|
||||||
return AjaxResult.error("设备数量已达上限(" + deviceLimit + "个),请先移除其他设备");
|
return AjaxResult.error("设备数量已达上限(" + deviceLimit + "个),请先移除其他设备");
|
||||||
}
|
}
|
||||||
|
|
||||||
String accessToken = Jwts.builder().setHeaderParam("kid", jwtRsaKeyService.getKeyId()).setSubject(username).setIssuedAt(new Date()).setExpiration(new Date(System.currentTimeMillis() + JWT_EXPIRATION)).claim("accountId", account.getId()).claim("username", username).claim("clientId", clientId).signWith(SignatureAlgorithm.RS256, jwtRsaKeyService.getPrivateKey()).compact();
|
String token = Jwts.builder()
|
||||||
Map<String, Object> result = new HashMap<>();
|
.setHeaderParam("kid", jwtRsaKeyService.getKeyId())
|
||||||
result.put("accessToken", accessToken);
|
.setSubject(username)
|
||||||
result.put("permissions", account.getPermissions());
|
.setIssuedAt(new Date())
|
||||||
result.put("accountName", account.getAccountName());
|
.setExpiration(new Date(System.currentTimeMillis() + JWT_EXPIRATION))
|
||||||
result.put("expireTime", account.getExpireTime());
|
.claim("accountId", account.getId())
|
||||||
return AjaxResult.success("登录成功", result);
|
.claim("username", username)
|
||||||
|
.claim("clientId", clientId)
|
||||||
|
.signWith(SignatureAlgorithm.RS256, jwtRsaKeyService.getPrivateKey())
|
||||||
|
.compact();
|
||||||
|
|
||||||
|
Map<String, Object> data = new HashMap<>();
|
||||||
|
data.put("token", token);
|
||||||
|
data.put("permissions", account.getPermissions());
|
||||||
|
data.put("accountName", account.getAccountName());
|
||||||
|
data.put("expireTime", account.getExpireTime());
|
||||||
|
return AjaxResult.success(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -169,29 +176,23 @@ public class ClientAccountController extends BaseController {
|
|||||||
@PostMapping("/verify")
|
@PostMapping("/verify")
|
||||||
public AjaxResult verifyToken(@RequestBody Map<String, String> data) {
|
public AjaxResult verifyToken(@RequestBody Map<String, String> data) {
|
||||||
String token = data.get("token");
|
String token = data.get("token");
|
||||||
if (StringUtils.isEmpty(token)) {
|
Map<String, Object> claims = Jwts.parser()
|
||||||
return AjaxResult.error("token不能为空");
|
.setSigningKey(jwtRsaKeyService.getPublicKey())
|
||||||
}
|
.parseClaimsJws(token)
|
||||||
Map<String, Object> claims = Jwts.parser().setSigningKey(jwtRsaKeyService.getPublicKey()).parseClaimsJws(token).getBody();
|
.getBody();
|
||||||
String username = (String) claims.get("sub");
|
String username = (String) claims.get("sub");
|
||||||
|
|
||||||
ClientAccount account = clientAccountService.selectClientAccountByUsername(username);
|
ClientAccount account = clientAccountService.selectClientAccountByUsername(username);
|
||||||
if (account == null || !"0".equals(account.getStatus())) {
|
if (account == null || !"0".equals(account.getStatus())) {
|
||||||
return AjaxResult.error("token无效");
|
return AjaxResult.error("token无效");
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<String, Object> result = new HashMap<>();
|
Map<String, Object> result = new HashMap<>();
|
||||||
result.put("username", username);
|
result.put("username", username);
|
||||||
result.put("permissions", account.getPermissions());
|
result.put("permissions", account.getPermissions());
|
||||||
result.put("accountName", account.getAccountName());
|
result.put("accountName", account.getAccountName());
|
||||||
result.put("expireTime", account.getExpireTime());
|
result.put("expireTime", account.getExpireTime());
|
||||||
// 计算VIP状态
|
return AjaxResult.success(result);
|
||||||
if (account.getExpireTime() != null) {
|
|
||||||
boolean isExpired = account.getExpireTime().before(new Date());
|
|
||||||
result.put("isVip", !isExpired);
|
|
||||||
} else {
|
|
||||||
result.put("isVip", false);
|
|
||||||
}
|
|
||||||
|
|
||||||
return AjaxResult.success("验证成功", result);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -223,6 +224,7 @@ public class ClientAccountController extends BaseController {
|
|||||||
String username = registerData.get("username");
|
String username = registerData.get("username");
|
||||||
String password = registerData.get("password");
|
String password = registerData.get("password");
|
||||||
String deviceId = registerData.get("deviceId");
|
String deviceId = registerData.get("deviceId");
|
||||||
|
|
||||||
ClientAccount clientAccount = new ClientAccount();
|
ClientAccount clientAccount = new ClientAccount();
|
||||||
clientAccount.setUsername(username);
|
clientAccount.setUsername(username);
|
||||||
clientAccount.setAccountName(username);
|
clientAccount.setAccountName(username);
|
||||||
@@ -231,22 +233,12 @@ public class ClientAccountController extends BaseController {
|
|||||||
clientAccount.setPermissions("{\"amazon\":true,\"rakuten\":true,\"zebra\":true}");
|
clientAccount.setPermissions("{\"amazon\":true,\"rakuten\":true,\"zebra\":true}");
|
||||||
clientAccount.setPassword(passwordEncoder.encode(password));
|
clientAccount.setPassword(passwordEncoder.encode(password));
|
||||||
|
|
||||||
// 检查设备ID是否已注册过(赠送VIP逻辑)
|
// 新设备赠送3天VIP
|
||||||
boolean isNewDevice = true;
|
ClientDevice existingDevice = clientDeviceMapper.selectByDeviceId(deviceId);
|
||||||
if (!StringUtils.isEmpty(deviceId)) {
|
int vipDays = (existingDevice == null) ? 3 : 0;
|
||||||
ClientDevice existingDevice = clientDeviceMapper.selectByDeviceId(deviceId);
|
|
||||||
isNewDevice = (existingDevice == null);
|
|
||||||
}
|
|
||||||
int vipDays;
|
|
||||||
if (isNewDevice) {
|
|
||||||
vipDays = 3;
|
|
||||||
} else {
|
|
||||||
vipDays = 0; // 立即过期,需要续费
|
|
||||||
}
|
|
||||||
|
|
||||||
if (vipDays > 0) {
|
if (vipDays > 0) {
|
||||||
Date expireDate = new Date(System.currentTimeMillis() + vipDays * 24L * 60 * 60 * 1000);
|
clientAccount.setExpireTime(new Date(System.currentTimeMillis() + vipDays * 24L * 60 * 60 * 1000));
|
||||||
clientAccount.setExpireTime(expireDate);
|
|
||||||
} else {
|
} else {
|
||||||
clientAccount.setExpireTime(new Date());
|
clientAccount.setExpireTime(new Date());
|
||||||
}
|
}
|
||||||
@@ -256,14 +248,22 @@ public class ClientAccountController extends BaseController {
|
|||||||
return AjaxResult.error("注册失败");
|
return AjaxResult.error("注册失败");
|
||||||
}
|
}
|
||||||
|
|
||||||
String accessToken = Jwts.builder().setHeaderParam("kid", jwtRsaKeyService.getKeyId()).setSubject(clientAccount.getUsername()).setIssuedAt(new Date()).setExpiration(new Date(System.currentTimeMillis() + JWT_EXPIRATION)).claim("accountId", clientAccount.getId()).claim("clientId", deviceId).signWith(SignatureAlgorithm.RS256, jwtRsaKeyService.getPrivateKey()).compact();
|
String token = Jwts.builder()
|
||||||
|
.setHeaderParam("kid", jwtRsaKeyService.getKeyId())
|
||||||
|
.setSubject(clientAccount.getUsername())
|
||||||
|
.setIssuedAt(new Date())
|
||||||
|
.setExpiration(new Date(System.currentTimeMillis() + JWT_EXPIRATION))
|
||||||
|
.claim("accountId", clientAccount.getId())
|
||||||
|
.claim("clientId", deviceId)
|
||||||
|
.signWith(SignatureAlgorithm.RS256, jwtRsaKeyService.getPrivateKey())
|
||||||
|
.compact();
|
||||||
|
|
||||||
Map<String, Object> dataMap = new HashMap<>();
|
Map<String, Object> data = new HashMap<>();
|
||||||
dataMap.put("accessToken", accessToken);
|
data.put("token", token);
|
||||||
dataMap.put("permissions", clientAccount.getPermissions());
|
data.put("permissions", clientAccount.getPermissions());
|
||||||
dataMap.put("accountName", clientAccount.getAccountName());
|
data.put("accountName", clientAccount.getAccountName());
|
||||||
dataMap.put("expireTime", clientAccount.getExpireTime());
|
data.put("expireTime", clientAccount.getExpireTime());
|
||||||
return AjaxResult.success("注册成功", dataMap);
|
return AjaxResult.success(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -271,9 +271,6 @@ public class ClientAccountController extends BaseController {
|
|||||||
*/
|
*/
|
||||||
@GetMapping("/check-username")
|
@GetMapping("/check-username")
|
||||||
public AjaxResult checkUsername(@RequestParam("username") String username) {
|
public AjaxResult checkUsername(@RequestParam("username") String username) {
|
||||||
if (StringUtils.isEmpty(username)) {
|
|
||||||
return AjaxResult.error("用户名不能为空");
|
|
||||||
}
|
|
||||||
ClientAccount account = clientAccountService.selectClientAccountByUsername(username);
|
ClientAccount account = clientAccountService.selectClientAccountByUsername(username);
|
||||||
return AjaxResult.success(account == null);
|
return AjaxResult.success(account == null);
|
||||||
}
|
}
|
||||||
@@ -306,14 +303,13 @@ public class ClientAccountController extends BaseController {
|
|||||||
account.setUpdateBy(getUsername());
|
account.setUpdateBy(getUsername());
|
||||||
clientAccountService.updateClientAccount(account);
|
clientAccountService.updateClientAccount(account);
|
||||||
|
|
||||||
// 通过SSE推送续费通知给该账号的所有在线设备
|
// 推送续费通知
|
||||||
try {
|
sseHubService.sendEventToAllDevices(account.getUsername(), "VIP_RENEWED",
|
||||||
sseHubService.sendEventToAllDevices(account.getUsername(), "VIP_RENEWED", "{\"expireTime\":\"" + newExpireTime + "\"}");
|
"{\"expireTime\":\"" + newExpireTime + "\"}");
|
||||||
} catch (Exception e) {
|
|
||||||
// SSE推送失败不影响续费操作
|
|
||||||
}
|
|
||||||
|
|
||||||
return AjaxResult.success("续费成功,新的过期时间:" + newExpireTime);
|
Map<String, Object> result = new HashMap<>();
|
||||||
|
result.put("expireTime", newExpireTime);
|
||||||
|
return AjaxResult.success(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,278 @@
|
|||||||
|
package com.ruoyi.web.controller.monitor;
|
||||||
|
|
||||||
|
import com.ruoyi.common.annotation.Anonymous;
|
||||||
|
import com.ruoyi.common.annotation.Log;
|
||||||
|
import com.ruoyi.common.core.controller.BaseController;
|
||||||
|
import com.ruoyi.common.core.domain.AjaxResult;
|
||||||
|
import com.ruoyi.common.core.page.TableDataInfo;
|
||||||
|
import com.ruoyi.common.enums.BusinessType;
|
||||||
|
import com.ruoyi.system.domain.ClientFeedback;
|
||||||
|
import com.ruoyi.web.service.IClientFeedbackService;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.security.access.prepost.PreAuthorize;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
import javax.servlet.http.HttpServletResponse;
|
||||||
|
import java.io.*;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
import java.text.SimpleDateFormat;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 客户端反馈控制器
|
||||||
|
*
|
||||||
|
* @author ruoyi
|
||||||
|
*/
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/monitor/feedback")
|
||||||
|
public class ClientFeedbackController extends BaseController {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private IClientFeedbackService feedbackService;
|
||||||
|
|
||||||
|
@Value("${feedback.log.path:C:/ProgramData/erp-logs/feedback/}")
|
||||||
|
private String feedbackLogPath;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 提交用户反馈(客户端调用)
|
||||||
|
*/
|
||||||
|
@Anonymous
|
||||||
|
@PostMapping("/submit")
|
||||||
|
public AjaxResult submitFeedback(
|
||||||
|
@RequestParam("username") String username,
|
||||||
|
@RequestParam("deviceId") String deviceId,
|
||||||
|
@RequestParam("feedbackContent") String feedbackContent,
|
||||||
|
@RequestParam(value = "logDate", required = false) String logDate,
|
||||||
|
@RequestParam(value = "logFile", required = false) MultipartFile logFile) {
|
||||||
|
|
||||||
|
try {
|
||||||
|
ClientFeedback feedback = new ClientFeedback();
|
||||||
|
feedback.setUsername(username);
|
||||||
|
feedback.setDeviceId(deviceId);
|
||||||
|
feedback.setFeedbackContent(feedbackContent);
|
||||||
|
feedback.setStatus("pending");
|
||||||
|
|
||||||
|
// 处理日志文件上传
|
||||||
|
if (logFile != null && !logFile.isEmpty()) {
|
||||||
|
String logFilePath = saveLogFile(username, deviceId, logFile);
|
||||||
|
feedback.setLogFilePath(logFilePath);
|
||||||
|
|
||||||
|
// 解析日志日期
|
||||||
|
if (logDate != null && !logDate.isEmpty()) {
|
||||||
|
try {
|
||||||
|
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
|
||||||
|
feedback.setLogDate(sdf.parse(logDate));
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.warn("解析日志日期失败: {}", logDate, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
feedbackService.insertFeedback(feedback);
|
||||||
|
return AjaxResult.success("success");
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("提交反馈失败", e);
|
||||||
|
return AjaxResult.error("反馈提交失败: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询反馈列表(管理端调用)
|
||||||
|
*/
|
||||||
|
@PreAuthorize("@ss.hasPermi('monitor:feedback:list')")
|
||||||
|
@GetMapping("/list")
|
||||||
|
public TableDataInfo list(ClientFeedback feedback) {
|
||||||
|
startPage();
|
||||||
|
List<ClientFeedback> list = feedbackService.selectFeedbackList(feedback);
|
||||||
|
return getDataTable(list);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取反馈详情
|
||||||
|
*/
|
||||||
|
@PreAuthorize("@ss.hasPermi('monitor:feedback:query')")
|
||||||
|
@GetMapping("/{id}")
|
||||||
|
public AjaxResult getInfo(@PathVariable("id") Long id) {
|
||||||
|
return AjaxResult.success(feedbackService.selectFeedbackById(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取反馈附带的日志内容(用于在线查看)
|
||||||
|
*/
|
||||||
|
@PreAuthorize("@ss.hasPermi('monitor:feedback:query')")
|
||||||
|
@GetMapping("/log/content/{id}")
|
||||||
|
public AjaxResult getLogContent(@PathVariable("id") Long id) {
|
||||||
|
try {
|
||||||
|
ClientFeedback feedback = feedbackService.selectFeedbackById(id);
|
||||||
|
if (feedback == null) {
|
||||||
|
return AjaxResult.error("反馈记录不存在");
|
||||||
|
}
|
||||||
|
|
||||||
|
String logFilePath = feedback.getLogFilePath();
|
||||||
|
if (logFilePath == null || logFilePath.isEmpty()) {
|
||||||
|
return AjaxResult.error("该反馈未附带日志文件");
|
||||||
|
}
|
||||||
|
|
||||||
|
File logFile = new File(logFilePath);
|
||||||
|
if (!logFile.exists()) {
|
||||||
|
return AjaxResult.error("日志文件不存在");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 读取日志文件内容
|
||||||
|
StringBuilder content = new StringBuilder();
|
||||||
|
try (BufferedReader reader = new BufferedReader(new FileReader(logFile))) {
|
||||||
|
String line;
|
||||||
|
while ((line = reader.readLine()) != null) {
|
||||||
|
content.append(line).append("\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, Object> result = new HashMap<>();
|
||||||
|
result.put("content", content.toString());
|
||||||
|
result.put("fileName", logFile.getName());
|
||||||
|
result.put("fileSize", logFile.length());
|
||||||
|
|
||||||
|
return AjaxResult.success(result);
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("读取日志文件失败", e);
|
||||||
|
return AjaxResult.error("读取失败: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 下载反馈附带的日志文件
|
||||||
|
*/
|
||||||
|
@PreAuthorize("@ss.hasPermi('monitor:feedback:query')")
|
||||||
|
@GetMapping("/log/download/{id}")
|
||||||
|
public void downloadLog(@PathVariable("id") Long id, HttpServletResponse response) {
|
||||||
|
try {
|
||||||
|
ClientFeedback feedback = feedbackService.selectFeedbackById(id);
|
||||||
|
if (feedback == null) {
|
||||||
|
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
|
||||||
|
response.getWriter().write("反馈记录不存在");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String logFilePath = feedback.getLogFilePath();
|
||||||
|
if (logFilePath == null || logFilePath.isEmpty()) {
|
||||||
|
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
|
||||||
|
response.getWriter().write("该反馈未附带日志文件");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
File logFile = new File(logFilePath);
|
||||||
|
if (!logFile.exists()) {
|
||||||
|
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
|
||||||
|
response.getWriter().write("日志文件不存在");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置响应头
|
||||||
|
response.setContentType("application/octet-stream");
|
||||||
|
response.setHeader("Content-Disposition",
|
||||||
|
"attachment; filename=" + logFile.getName());
|
||||||
|
response.setContentLengthLong(logFile.length());
|
||||||
|
|
||||||
|
// 读取文件并写入响应
|
||||||
|
try (FileInputStream fis = new FileInputStream(logFile);
|
||||||
|
OutputStream os = response.getOutputStream()) {
|
||||||
|
byte[] buffer = new byte[8192];
|
||||||
|
int bytesRead;
|
||||||
|
while ((bytesRead = fis.read(buffer)) != -1) {
|
||||||
|
os.write(buffer, 0, bytesRead);
|
||||||
|
}
|
||||||
|
os.flush();
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("下载日志文件失败", e);
|
||||||
|
try {
|
||||||
|
response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
|
||||||
|
response.getWriter().write("下载失败: " + e.getMessage());
|
||||||
|
} catch (IOException ignored) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新反馈状态
|
||||||
|
*/
|
||||||
|
@PreAuthorize("@ss.hasPermi('monitor:feedback:edit')")
|
||||||
|
@Log(title = "客户端反馈", businessType = BusinessType.UPDATE)
|
||||||
|
@PutMapping("/status/{id}")
|
||||||
|
public AjaxResult updateStatus(@PathVariable("id") Long id, @RequestBody Map<String, String> params) {
|
||||||
|
String status = params.get("status");
|
||||||
|
String remark = params.get("remark");
|
||||||
|
|
||||||
|
ClientFeedback feedback = new ClientFeedback();
|
||||||
|
feedback.setId(id);
|
||||||
|
feedback.setStatus(status);
|
||||||
|
feedback.setRemark(remark);
|
||||||
|
|
||||||
|
int result = feedbackService.updateFeedback(feedback);
|
||||||
|
return toAjax(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除反馈
|
||||||
|
*/
|
||||||
|
@PreAuthorize("@ss.hasPermi('monitor:feedback:remove')")
|
||||||
|
@Log(title = "客户端反馈", businessType = BusinessType.DELETE)
|
||||||
|
@DeleteMapping("/{id}")
|
||||||
|
public AjaxResult remove(@PathVariable("id") Long id) {
|
||||||
|
// 删除前先删除日志文件
|
||||||
|
ClientFeedback feedback = feedbackService.selectFeedbackById(id);
|
||||||
|
if (feedback != null && feedback.getLogFilePath() != null) {
|
||||||
|
try {
|
||||||
|
File logFile = new File(feedback.getLogFilePath());
|
||||||
|
if (logFile.exists()) {
|
||||||
|
logFile.delete();
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.warn("删除日志文件失败: {}", feedback.getLogFilePath(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return toAjax(feedbackService.deleteFeedbackById(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取反馈统计信息
|
||||||
|
*/
|
||||||
|
@PreAuthorize("@ss.hasPermi('monitor:feedback:list')")
|
||||||
|
@GetMapping("/statistics")
|
||||||
|
public AjaxResult getStatistics() {
|
||||||
|
Map<String, Object> stats = new HashMap<>();
|
||||||
|
stats.put("pendingCount", feedbackService.countPendingFeedback());
|
||||||
|
stats.put("todayCount", feedbackService.countTodayFeedback());
|
||||||
|
return AjaxResult.success(stats);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存日志文件
|
||||||
|
*/
|
||||||
|
private String saveLogFile(String username, String deviceId, MultipartFile file) throws IOException {
|
||||||
|
// 确保目录存在
|
||||||
|
Path uploadPath = Paths.get(feedbackLogPath);
|
||||||
|
if (!Files.exists(uploadPath)) {
|
||||||
|
Files.createDirectories(uploadPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成文件名: username_deviceId_timestamp.log
|
||||||
|
String timestamp = new SimpleDateFormat("yyyyMMddHHmmss").format(new Date());
|
||||||
|
String fileName = String.format("%s_%s_%s.log", username, deviceId, timestamp);
|
||||||
|
Path filePath = uploadPath.resolve(fileName);
|
||||||
|
|
||||||
|
// 保存文件
|
||||||
|
file.transferTo(filePath.toFile());
|
||||||
|
|
||||||
|
return filePath.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -98,12 +98,8 @@ public class ClientMonitorController extends BaseController {
|
|||||||
*/
|
*/
|
||||||
@PostMapping("/api/auth")
|
@PostMapping("/api/auth")
|
||||||
public AjaxResult clientAuth(@RequestBody Map<String, Object> authData) {
|
public AjaxResult clientAuth(@RequestBody Map<String, Object> authData) {
|
||||||
try {
|
String authKey = (String) authData.get("authKey");
|
||||||
String authKey = (String) authData.get("authKey");
|
return AjaxResult.success(clientMonitorService.authenticateClient(authKey, authData));
|
||||||
return AjaxResult.success("认证成功", clientMonitorService.authenticateClient(authKey, authData));
|
|
||||||
} catch (Exception e) {
|
|
||||||
return AjaxResult.error("认证失败:" + e.getMessage());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -112,25 +108,14 @@ public class ClientMonitorController extends BaseController {
|
|||||||
*/
|
*/
|
||||||
@PostMapping("/api/error")
|
@PostMapping("/api/error")
|
||||||
public AjaxResult clientError(@RequestBody Map<String, Object> errorData) {
|
public AjaxResult clientError(@RequestBody Map<String, Object> errorData) {
|
||||||
try {
|
clientMonitorService.recordErrorReport(errorData);
|
||||||
clientMonitorService.recordErrorReport(errorData);
|
return AjaxResult.success();
|
||||||
return AjaxResult.success();
|
|
||||||
} catch (Exception e) {
|
|
||||||
return AjaxResult.error(e.getMessage());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 客户端数据上报API
|
|
||||||
*/
|
|
||||||
@PostMapping("/api/data")
|
@PostMapping("/api/data")
|
||||||
public AjaxResult clientDataReport(@RequestBody Map<String, Object> dataReport) {
|
public AjaxResult clientDataReport(@RequestBody Map<String, Object> dataReport) {
|
||||||
try {
|
clientMonitorService.recordDataReport(dataReport);
|
||||||
clientMonitorService.recordDataReport(dataReport);
|
return AjaxResult.success();
|
||||||
return AjaxResult.success();
|
|
||||||
} catch (Exception e) {
|
|
||||||
return AjaxResult.error(e.getMessage());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* 获取特定客户端的详细信息
|
* 获取特定客户端的详细信息
|
||||||
@@ -155,12 +140,8 @@ public class ClientMonitorController extends BaseController {
|
|||||||
@PreAuthorize("@ss.hasPermi('monitor:client:export')")
|
@PreAuthorize("@ss.hasPermi('monitor:client:export')")
|
||||||
@PostMapping("/cleanup")
|
@PostMapping("/cleanup")
|
||||||
public AjaxResult cleanupExpiredData() {
|
public AjaxResult cleanupExpiredData() {
|
||||||
try {
|
clientMonitorService.cleanExpiredData();
|
||||||
clientMonitorService.cleanExpiredData();
|
return AjaxResult.success();
|
||||||
return AjaxResult.success("过期数据清理完成");
|
|
||||||
} catch (Exception e) {
|
|
||||||
return AjaxResult.error("清理过期数据失败: " + e.getMessage());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -34,28 +34,17 @@ public class VersionController extends BaseController {
|
|||||||
*/
|
*/
|
||||||
@GetMapping("/check")
|
@GetMapping("/check")
|
||||||
public AjaxResult checkVersion(@RequestParam String currentVersion) {
|
public AjaxResult checkVersion(@RequestParam String currentVersion) {
|
||||||
try {
|
String latestVersion = redisTemplate.opsForValue().get(VERSION_REDIS_KEY);
|
||||||
// 从Redis获取最新版本信息
|
boolean needUpdate = compareVersions(currentVersion, latestVersion) < 0;
|
||||||
String latestVersion = redisTemplate.opsForValue().get(VERSION_REDIS_KEY);
|
|
||||||
// 比较版本号
|
|
||||||
boolean needUpdate = compareVersions(currentVersion, latestVersion) < 0;
|
|
||||||
|
|
||||||
Map<String, Object> result = new HashMap<>();
|
Map<String, Object> data = new HashMap<>();
|
||||||
result.put("currentVersion", currentVersion);
|
data.put("currentVersion", currentVersion);
|
||||||
result.put("latestVersion", latestVersion);
|
data.put("latestVersion", latestVersion);
|
||||||
result.put("needUpdate", needUpdate);
|
data.put("needUpdate", needUpdate);
|
||||||
// 从Redis获取下载链接
|
data.put("asarUrl", redisTemplate.opsForValue().get(ASAR_URL_REDIS_KEY));
|
||||||
String asarUrl = redisTemplate.opsForValue().get(ASAR_URL_REDIS_KEY);
|
data.put("jarUrl", redisTemplate.opsForValue().get(JAR_URL_REDIS_KEY));
|
||||||
String jarUrl = redisTemplate.opsForValue().get(JAR_URL_REDIS_KEY);
|
|
||||||
result.put("asarUrl", asarUrl);
|
|
||||||
result.put("jarUrl", jarUrl);
|
|
||||||
// 兼容旧版本,保留downloadUrl字段(指向asar)
|
|
||||||
result.put("downloadUrl", asarUrl);
|
|
||||||
|
|
||||||
return AjaxResult.success(result);
|
return AjaxResult.success(data);
|
||||||
} catch (Exception e) {
|
|
||||||
return AjaxResult.error("版本检查失败: " + e.getMessage());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* 获取当前版本信息
|
* 获取当前版本信息
|
||||||
@@ -63,24 +52,18 @@ public class VersionController extends BaseController {
|
|||||||
@PreAuthorize("@ss.hasPermi('system:version:query')")
|
@PreAuthorize("@ss.hasPermi('system:version:query')")
|
||||||
@GetMapping("/info")
|
@GetMapping("/info")
|
||||||
public AjaxResult getVersionInfo() {
|
public AjaxResult getVersionInfo() {
|
||||||
try {
|
String currentVersion = redisTemplate.opsForValue().get(VERSION_REDIS_KEY);
|
||||||
String currentVersion = redisTemplate.opsForValue().get(VERSION_REDIS_KEY);
|
if (StringUtils.isEmpty(currentVersion)) {
|
||||||
if (StringUtils.isEmpty(currentVersion)) {
|
currentVersion = "2.0.0";
|
||||||
currentVersion = "2.0.0";
|
|
||||||
}
|
|
||||||
String asarUrl = redisTemplate.opsForValue().get(ASAR_URL_REDIS_KEY);
|
|
||||||
String jarUrl = redisTemplate.opsForValue().get(JAR_URL_REDIS_KEY);
|
|
||||||
|
|
||||||
Map<String, Object> result = new HashMap<>();
|
|
||||||
result.put("currentVersion", currentVersion);
|
|
||||||
result.put("asarUrl", asarUrl);
|
|
||||||
result.put("jarUrl", jarUrl);
|
|
||||||
result.put("updateTime", System.currentTimeMillis());
|
|
||||||
|
|
||||||
return AjaxResult.success(result);
|
|
||||||
} catch (Exception e) {
|
|
||||||
return AjaxResult.error("获取版本信息失败: " + e.getMessage());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Map<String, Object> data = new HashMap<>();
|
||||||
|
data.put("currentVersion", currentVersion);
|
||||||
|
data.put("asarUrl", redisTemplate.opsForValue().get(ASAR_URL_REDIS_KEY));
|
||||||
|
data.put("jarUrl", redisTemplate.opsForValue().get(JAR_URL_REDIS_KEY));
|
||||||
|
data.put("updateTime", System.currentTimeMillis());
|
||||||
|
|
||||||
|
return AjaxResult.success(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -92,23 +75,20 @@ public class VersionController extends BaseController {
|
|||||||
public AjaxResult updateVersionInfo(@RequestParam("version") String version,
|
public AjaxResult updateVersionInfo(@RequestParam("version") String version,
|
||||||
@RequestParam(value = "asarUrl", required = false) String asarUrl,
|
@RequestParam(value = "asarUrl", required = false) String asarUrl,
|
||||||
@RequestParam(value = "jarUrl", required = false) String jarUrl) {
|
@RequestParam(value = "jarUrl", required = false) String jarUrl) {
|
||||||
try {
|
redisTemplate.opsForValue().set(VERSION_REDIS_KEY, version);
|
||||||
redisTemplate.opsForValue().set(VERSION_REDIS_KEY, version);
|
if (StringUtils.isNotEmpty(asarUrl)) {
|
||||||
if (StringUtils.isNotEmpty(asarUrl)) {
|
redisTemplate.opsForValue().set(ASAR_URL_REDIS_KEY, asarUrl);
|
||||||
redisTemplate.opsForValue().set(ASAR_URL_REDIS_KEY, asarUrl);
|
|
||||||
}
|
|
||||||
if (StringUtils.isNotEmpty(jarUrl)) {
|
|
||||||
redisTemplate.opsForValue().set(JAR_URL_REDIS_KEY, jarUrl);
|
|
||||||
}
|
|
||||||
Map<String, Object> result = new HashMap<>();
|
|
||||||
result.put("version", version);
|
|
||||||
result.put("asarUrl", asarUrl);
|
|
||||||
result.put("jarUrl", jarUrl);
|
|
||||||
result.put("updateTime", System.currentTimeMillis());
|
|
||||||
return AjaxResult.success("版本信息更新成功", result);
|
|
||||||
} catch (Exception e) {
|
|
||||||
return AjaxResult.error("版本信息更新失败: " + e.getMessage());
|
|
||||||
}
|
}
|
||||||
|
if (StringUtils.isNotEmpty(jarUrl)) {
|
||||||
|
redisTemplate.opsForValue().set(JAR_URL_REDIS_KEY, jarUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, Object> data = new HashMap<>();
|
||||||
|
data.put("version", version);
|
||||||
|
data.put("asarUrl", asarUrl);
|
||||||
|
data.put("jarUrl", jarUrl);
|
||||||
|
data.put("updateTime", System.currentTimeMillis());
|
||||||
|
return AjaxResult.success(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
package com.ruoyi.web.service;
|
||||||
|
|
||||||
|
import com.ruoyi.system.domain.ClientFeedback;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 客户端反馈服务接口
|
||||||
|
*
|
||||||
|
* @author ruoyi
|
||||||
|
*/
|
||||||
|
public interface IClientFeedbackService
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* 查询反馈列表
|
||||||
|
*
|
||||||
|
* @param feedback 反馈信息
|
||||||
|
* @return 反馈集合
|
||||||
|
*/
|
||||||
|
List<ClientFeedback> selectFeedbackList(ClientFeedback feedback);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据ID查询反馈
|
||||||
|
*
|
||||||
|
* @param id 反馈ID
|
||||||
|
* @return 反馈信息
|
||||||
|
*/
|
||||||
|
ClientFeedback selectFeedbackById(Long id);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 新增反馈
|
||||||
|
*
|
||||||
|
* @param feedback 反馈信息
|
||||||
|
* @return 结果
|
||||||
|
*/
|
||||||
|
int insertFeedback(ClientFeedback feedback);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新反馈
|
||||||
|
*
|
||||||
|
* @param feedback 反馈信息
|
||||||
|
* @return 结果
|
||||||
|
*/
|
||||||
|
int updateFeedback(ClientFeedback feedback);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除反馈
|
||||||
|
*
|
||||||
|
* @param id 反馈ID
|
||||||
|
* @return 结果
|
||||||
|
*/
|
||||||
|
int deleteFeedbackById(Long id);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 统计待处理反馈数量
|
||||||
|
*
|
||||||
|
* @return 数量
|
||||||
|
*/
|
||||||
|
int countPendingFeedback();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 统计今日反馈数量
|
||||||
|
*
|
||||||
|
* @return 数量
|
||||||
|
*/
|
||||||
|
int countTodayFeedback();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新反馈状态
|
||||||
|
*
|
||||||
|
* @param id 反馈ID
|
||||||
|
* @param status 状态
|
||||||
|
* @return 结果
|
||||||
|
*/
|
||||||
|
int updateFeedbackStatus(Long id, String status);
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
package com.ruoyi.web.service.impl;
|
||||||
|
|
||||||
|
import com.ruoyi.system.domain.ClientFeedback;
|
||||||
|
import com.ruoyi.system.mapper.ClientFeedbackMapper;
|
||||||
|
import com.ruoyi.web.service.IClientFeedbackService;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 客户端反馈服务实现
|
||||||
|
*
|
||||||
|
* @author ruoyi
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
public class ClientFeedbackServiceImpl implements IClientFeedbackService
|
||||||
|
{
|
||||||
|
@Autowired
|
||||||
|
private ClientFeedbackMapper clientFeedbackMapper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询反馈列表
|
||||||
|
*
|
||||||
|
* @param feedback 反馈信息
|
||||||
|
* @return 反馈集合
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public List<ClientFeedback> selectFeedbackList(ClientFeedback feedback)
|
||||||
|
{
|
||||||
|
return clientFeedbackMapper.selectFeedbackList(feedback);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据ID查询反馈
|
||||||
|
*
|
||||||
|
* @param id 反馈ID
|
||||||
|
* @return 反馈信息
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public ClientFeedback selectFeedbackById(Long id)
|
||||||
|
{
|
||||||
|
return clientFeedbackMapper.selectFeedbackById(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 新增反馈
|
||||||
|
*
|
||||||
|
* @param feedback 反馈信息
|
||||||
|
* @return 结果
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public int insertFeedback(ClientFeedback feedback)
|
||||||
|
{
|
||||||
|
return clientFeedbackMapper.insertFeedback(feedback);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新反馈
|
||||||
|
*
|
||||||
|
* @param feedback 反馈信息
|
||||||
|
* @return 结果
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public int updateFeedback(ClientFeedback feedback)
|
||||||
|
{
|
||||||
|
return clientFeedbackMapper.updateFeedback(feedback);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除反馈
|
||||||
|
*
|
||||||
|
* @param id 反馈ID
|
||||||
|
* @return 结果
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public int deleteFeedbackById(Long id)
|
||||||
|
{
|
||||||
|
return clientFeedbackMapper.deleteFeedbackById(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 统计待处理反馈数量
|
||||||
|
*
|
||||||
|
* @return 数量
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public int countPendingFeedback()
|
||||||
|
{
|
||||||
|
return clientFeedbackMapper.countPendingFeedback();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 统计今日反馈数量
|
||||||
|
*
|
||||||
|
* @return 数量
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public int countTodayFeedback()
|
||||||
|
{
|
||||||
|
return clientFeedbackMapper.countTodayFeedback();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新反馈状态
|
||||||
|
*
|
||||||
|
* @param id 反馈ID
|
||||||
|
* @param status 状态
|
||||||
|
* @return 结果
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public int updateFeedbackStatus(Long id, String status)
|
||||||
|
{
|
||||||
|
return clientFeedbackMapper.updateFeedbackStatus(id, status);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -65,17 +65,6 @@ public class SseHubService {
|
|||||||
String key = buildSessionKey(username, clientId);
|
String key = buildSessionKey(username, clientId);
|
||||||
SseEmitter emitter = sessionEmitters.get(key);
|
SseEmitter emitter = sessionEmitters.get(key);
|
||||||
if (emitter == null) {
|
if (emitter == null) {
|
||||||
try {
|
|
||||||
ClientDevice device = clientDeviceMapper.selectByDeviceId(clientId);
|
|
||||||
// 只有当设备状态不是removed时,才更新为offline
|
|
||||||
if (device != null && !"removed".equals(device.getStatus())) {
|
|
||||||
device.setStatus("offline");
|
|
||||||
device.setLastActiveAt(new Date());
|
|
||||||
clientDeviceMapper.updateByDeviceId(device);
|
|
||||||
}
|
|
||||||
} catch (Exception ignored) {
|
|
||||||
// 静默处理,不影响心跳主流程
|
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -84,7 +73,7 @@ public class SseHubService {
|
|||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
sessionEmitters.remove(key);
|
sessionEmitters.remove(key);
|
||||||
try { emitter.complete(); } catch (Exception ignored) {}
|
try { emitter.complete(); } catch (Exception ignored) {}
|
||||||
// 发送失败也更新为离线
|
// 发送失败说明连接已断开,更新为离线
|
||||||
updateDeviceStatus(clientId, "offline");
|
updateDeviceStatus(clientId, "offline");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -149,9 +149,11 @@ public class TokenService
|
|||||||
{
|
{
|
||||||
loginUser.setLoginTime(System.currentTimeMillis());
|
loginUser.setLoginTime(System.currentTimeMillis());
|
||||||
loginUser.setExpireTime(loginUser.getLoginTime() + expireTime * MILLIS_MINUTE);
|
loginUser.setExpireTime(loginUser.getLoginTime() + expireTime * MILLIS_MINUTE);
|
||||||
// 根据uuid将loginUser缓存
|
|
||||||
String userKey = getTokenKey(loginUser.getToken());
|
String userKey = getTokenKey(loginUser.getToken());
|
||||||
redisCache.setCacheObject(userKey, loginUser, expireTime, TimeUnit.MINUTES);
|
// 根据uuid将loginUser缓存(永不过期)
|
||||||
|
//redisCache.setCacheObject(userKey, loginUser, expireTime, TimeUnit.MINUTES);
|
||||||
|
redisCache.setCacheObject(userKey, loginUser);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -0,0 +1,115 @@
|
|||||||
|
package com.ruoyi.system.domain;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||||
|
import com.ruoyi.common.annotation.Excel;
|
||||||
|
import com.ruoyi.common.core.domain.BaseEntity;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 客户端用户反馈对象 client_feedback
|
||||||
|
*
|
||||||
|
* @author ruoyi
|
||||||
|
*/
|
||||||
|
public class ClientFeedback extends BaseEntity
|
||||||
|
{
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
/** 主键ID */
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
/** 用户名 */
|
||||||
|
@Excel(name = "用户名")
|
||||||
|
private String username;
|
||||||
|
|
||||||
|
/** 设备ID */
|
||||||
|
@Excel(name = "设备ID")
|
||||||
|
private String deviceId;
|
||||||
|
|
||||||
|
/** 反馈内容 */
|
||||||
|
@Excel(name = "反馈内容")
|
||||||
|
private String feedbackContent;
|
||||||
|
|
||||||
|
/** 日志文件路径 */
|
||||||
|
@Excel(name = "日志文件路径")
|
||||||
|
private String logFilePath;
|
||||||
|
|
||||||
|
/** 日志日期 */
|
||||||
|
@JsonFormat(pattern = "yyyy-MM-dd")
|
||||||
|
@Excel(name = "日志日期", width = 30, dateFormat = "yyyy-MM-dd")
|
||||||
|
private Date logDate;
|
||||||
|
|
||||||
|
/** 处理状态 */
|
||||||
|
@Excel(name = "处理状态")
|
||||||
|
private String status;
|
||||||
|
|
||||||
|
public void setId(Long id)
|
||||||
|
{
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getId()
|
||||||
|
{
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUsername(String username)
|
||||||
|
{
|
||||||
|
this.username = username;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getUsername()
|
||||||
|
{
|
||||||
|
return username;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDeviceId(String deviceId)
|
||||||
|
{
|
||||||
|
this.deviceId = deviceId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getDeviceId()
|
||||||
|
{
|
||||||
|
return deviceId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setFeedbackContent(String feedbackContent)
|
||||||
|
{
|
||||||
|
this.feedbackContent = feedbackContent;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getFeedbackContent()
|
||||||
|
{
|
||||||
|
return feedbackContent;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setLogFilePath(String logFilePath)
|
||||||
|
{
|
||||||
|
this.logFilePath = logFilePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getLogFilePath()
|
||||||
|
{
|
||||||
|
return logFilePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setLogDate(Date logDate)
|
||||||
|
{
|
||||||
|
this.logDate = logDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Date getLogDate()
|
||||||
|
{
|
||||||
|
return logDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setStatus(String status)
|
||||||
|
{
|
||||||
|
this.status = status;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getStatus()
|
||||||
|
{
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
package com.ruoyi.system.mapper;
|
||||||
|
|
||||||
|
import com.ruoyi.system.domain.ClientFeedback;
|
||||||
|
import org.apache.ibatis.annotations.Param;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public interface ClientFeedbackMapper {
|
||||||
|
/**
|
||||||
|
* 查询反馈列表
|
||||||
|
*/
|
||||||
|
List<ClientFeedback> selectFeedbackList(ClientFeedback feedback);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据ID查询反馈
|
||||||
|
*/
|
||||||
|
ClientFeedback selectFeedbackById(Long id);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 新增反馈
|
||||||
|
*/
|
||||||
|
int insertFeedback(ClientFeedback feedback);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新反馈
|
||||||
|
*/
|
||||||
|
int updateFeedback(ClientFeedback feedback);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除反馈
|
||||||
|
*/
|
||||||
|
int deleteFeedbackById(Long id);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 统计待处理反馈数量
|
||||||
|
*/
|
||||||
|
int countPendingFeedback();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 统计今日反馈数量
|
||||||
|
*/
|
||||||
|
int countTodayFeedback();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新反馈状态
|
||||||
|
*/
|
||||||
|
int updateFeedbackStatus(@Param("id") Long id, @Param("status") String status);
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
<?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.ClientFeedbackMapper">
|
||||||
|
|
||||||
|
<resultMap type="com.ruoyi.system.domain.ClientFeedback" id="ClientFeedbackResult">
|
||||||
|
<result property="id" column="id" />
|
||||||
|
<result property="username" column="username" />
|
||||||
|
<result property="deviceId" column="device_id" />
|
||||||
|
<result property="feedbackContent" column="feedback_content"/>
|
||||||
|
<result property="logFilePath" column="log_file_path" />
|
||||||
|
<result property="logDate" column="log_date" />
|
||||||
|
<result property="status" column="status" />
|
||||||
|
<result property="createTime" column="create_time" />
|
||||||
|
<result property="updateTime" column="update_time" />
|
||||||
|
<result property="remark" column="remark" />
|
||||||
|
</resultMap>
|
||||||
|
|
||||||
|
<sql id="selectFeedbackVo">
|
||||||
|
select id, username, device_id, feedback_content, log_file_path, log_date,
|
||||||
|
status, create_time, update_time, remark
|
||||||
|
from client_feedback
|
||||||
|
</sql>
|
||||||
|
|
||||||
|
<select id="selectFeedbackList" parameterType="com.ruoyi.system.domain.ClientFeedback" resultMap="ClientFeedbackResult">
|
||||||
|
<include refid="selectFeedbackVo"/>
|
||||||
|
<where>
|
||||||
|
<if test="username != null and username != ''">
|
||||||
|
AND username like concat('%', #{username}, '%')
|
||||||
|
</if>
|
||||||
|
<if test="deviceId != null and deviceId != ''">
|
||||||
|
AND device_id = #{deviceId}
|
||||||
|
</if>
|
||||||
|
<if test="status != null and status != ''">
|
||||||
|
AND status = #{status}
|
||||||
|
</if>
|
||||||
|
<if test="params.beginTime != null and params.beginTime != ''">
|
||||||
|
AND date_format(create_time,'%y%m%d') >= date_format(#{params.beginTime},'%y%m%d')
|
||||||
|
</if>
|
||||||
|
<if test="params.endTime != null and params.endTime != ''">
|
||||||
|
AND date_format(create_time,'%y%m%d') <= date_format(#{params.endTime},'%y%m%d')
|
||||||
|
</if>
|
||||||
|
</where>
|
||||||
|
order by create_time desc
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<select id="selectFeedbackById" parameterType="Long" resultMap="ClientFeedbackResult">
|
||||||
|
<include refid="selectFeedbackVo"/>
|
||||||
|
where id = #{id}
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<insert id="insertFeedback" parameterType="com.ruoyi.system.domain.ClientFeedback" useGeneratedKeys="true" keyProperty="id">
|
||||||
|
insert into client_feedback
|
||||||
|
<trim prefix="(" suffix=")" suffixOverrides=",">
|
||||||
|
<if test="username != null and username != ''">username,</if>
|
||||||
|
<if test="deviceId != null and deviceId != ''">device_id,</if>
|
||||||
|
<if test="feedbackContent != null and feedbackContent != ''">feedback_content,</if>
|
||||||
|
<if test="logFilePath != null and logFilePath != ''">log_file_path,</if>
|
||||||
|
<if test="logDate != null">log_date,</if>
|
||||||
|
<if test="status != null and status != ''">status,</if>
|
||||||
|
<if test="remark != null and remark != ''">remark,</if>
|
||||||
|
create_time
|
||||||
|
</trim>
|
||||||
|
<trim prefix="values (" suffix=")" suffixOverrides=",">
|
||||||
|
<if test="username != null and username != ''">#{username},</if>
|
||||||
|
<if test="deviceId != null and deviceId != ''">#{deviceId},</if>
|
||||||
|
<if test="feedbackContent != null and feedbackContent != ''">#{feedbackContent},</if>
|
||||||
|
<if test="logFilePath != null and logFilePath != ''">#{logFilePath},</if>
|
||||||
|
<if test="logDate != null">#{logDate},</if>
|
||||||
|
<if test="status != null and status != ''">#{status},</if>
|
||||||
|
<if test="remark != null and remark != ''">#{remark},</if>
|
||||||
|
sysdate()
|
||||||
|
</trim>
|
||||||
|
</insert>
|
||||||
|
|
||||||
|
<update id="updateFeedback" parameterType="com.ruoyi.system.domain.ClientFeedback">
|
||||||
|
update client_feedback
|
||||||
|
<trim prefix="SET" suffixOverrides=",">
|
||||||
|
<if test="status != null and status != ''">status = #{status},</if>
|
||||||
|
<if test="remark != null">remark = #{remark},</if>
|
||||||
|
update_time = sysdate()
|
||||||
|
</trim>
|
||||||
|
where id = #{id}
|
||||||
|
</update>
|
||||||
|
|
||||||
|
<delete id="deleteFeedbackById" parameterType="Long">
|
||||||
|
delete from client_feedback where id = #{id}
|
||||||
|
</delete>
|
||||||
|
|
||||||
|
<select id="countPendingFeedback" resultType="int">
|
||||||
|
select count(*) from client_feedback where status = 'pending'
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<select id="countTodayFeedback" resultType="int">
|
||||||
|
select count(*) from client_feedback
|
||||||
|
where date_format(create_time,'%Y-%m-%d') = date_format(sysdate(),'%Y-%m-%d')
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<update id="updateFeedbackStatus">
|
||||||
|
update client_feedback set status = #{status}, update_time = sysdate()
|
||||||
|
where id = #{id}
|
||||||
|
</update>
|
||||||
|
|
||||||
|
</mapper>
|
||||||
|
|
||||||
57
ruoyi-ui/src/api/monitor/feedback.js
Normal file
57
ruoyi-ui/src/api/monitor/feedback.js
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import request from '@/utils/request'
|
||||||
|
|
||||||
|
// 查询反馈列表
|
||||||
|
export function listFeedback(query) {
|
||||||
|
return request({
|
||||||
|
url: '/monitor/feedback/list',
|
||||||
|
method: 'get',
|
||||||
|
params: query
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查询反馈详细
|
||||||
|
export function getFeedback(id) {
|
||||||
|
return request({
|
||||||
|
url: '/monitor/feedback/' + id,
|
||||||
|
method: 'get'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新反馈状态
|
||||||
|
export function updateFeedbackStatus(id, data) {
|
||||||
|
return request({
|
||||||
|
url: '/monitor/feedback/status/' + id,
|
||||||
|
method: 'put',
|
||||||
|
data: data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除反馈
|
||||||
|
export function delFeedback(id) {
|
||||||
|
return request({
|
||||||
|
url: '/monitor/feedback/' + id,
|
||||||
|
method: 'delete'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取反馈统计信息
|
||||||
|
export function getFeedbackStatistics() {
|
||||||
|
return request({
|
||||||
|
url: '/monitor/feedback/statistics',
|
||||||
|
method: 'get'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取反馈日志内容(用于在线查看)
|
||||||
|
export function getFeedbackLogContent(id) {
|
||||||
|
return request({
|
||||||
|
url: '/monitor/feedback/log/content/' + id,
|
||||||
|
method: 'get'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 下载反馈日志文件
|
||||||
|
export function downloadFeedbackLog(id) {
|
||||||
|
return process.env.VUE_APP_BASE_API + '/monitor/feedback/log/download/' + id
|
||||||
|
}
|
||||||
|
|
||||||
@@ -56,17 +56,17 @@
|
|||||||
</el-card>
|
</el-card>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :span="6">
|
<el-col :span="6">
|
||||||
<el-card class="monitor-card">
|
<el-card class="monitor-card clickable-card" @click.native="showFeedbackList">
|
||||||
<template v-slot:header>
|
<template v-slot:header>
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<i class="el-icon-s-data"></i>
|
<i class="el-icon-chat-line-square"></i>
|
||||||
<span>离线设备</span>
|
<span>用户反馈</span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<count-to :start-val="0" :end-val="statisticsData.totalCount - statisticsData.onlineCount" :duration="2500" class="card-value" />
|
<count-to :start-val="0" :end-val="feedbackStats.pendingCount" :duration="2500" class="card-value" />
|
||||||
<div class="card-footer">
|
<div class="card-footer">
|
||||||
<span>离线设备数</span>
|
<span>待处理: {{ feedbackStats.pendingCount }} / 今日: {{ feedbackStats.todayCount }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</el-card>
|
</el-card>
|
||||||
@@ -116,18 +116,18 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<el-table :data="clientList" style="width: 100%" v-loading="loading">
|
<el-table :data="clientList" style="width: 100%" v-loading="loading">
|
||||||
<el-table-column prop="clientId" label="设备ID" width="240" show-overflow-tooltip></el-table-column>
|
<el-table-column prop="clientId" label="设备ID" min-width="240" show-overflow-tooltip></el-table-column>
|
||||||
<el-table-column prop="username" label="账号" width="120"></el-table-column>
|
<el-table-column prop="username" label="账号" min-width="120"></el-table-column>
|
||||||
<el-table-column prop="hostname" label="设备名称" width="150" show-overflow-tooltip></el-table-column>
|
<el-table-column prop="hostname" label="设备名称" min-width="150" show-overflow-tooltip></el-table-column>
|
||||||
<el-table-column prop="osName" label="操作系统" width="120"></el-table-column>
|
<el-table-column prop="osName" label="操作系统" min-width="120"></el-table-column>
|
||||||
<el-table-column prop="ipAddress" label="IP地址" width="150"></el-table-column>
|
<el-table-column prop="ipAddress" label="IP地址" min-width="150"></el-table-column>
|
||||||
<el-table-column prop="lastActiveTime" label="最后活跃时间" width="180"></el-table-column>
|
<el-table-column prop="lastActiveTime" label="最后活跃时间" min-width="180"></el-table-column>
|
||||||
<el-table-column prop="online" label="状态" width="100">
|
<el-table-column prop="online" label="状态" min-width="100">
|
||||||
<template v-slot="scope">
|
<template v-slot="scope">
|
||||||
<el-tag :type="scope.row.online === '1' ? 'success' : 'info'">{{ scope.row.online === '1' ? '在线' : '离线' }}</el-tag>
|
<el-tag :type="scope.row.online === '1' ? 'success' : 'info'">{{ scope.row.online === '1' ? '在线' : '离线' }}</el-tag>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="操作" width="120">
|
<el-table-column label="操作" min-width="120">
|
||||||
<template v-slot="scope">
|
<template v-slot="scope">
|
||||||
<el-button size="mini" type="text" @click="viewClientData(scope.row)">详情</el-button>
|
<el-button size="mini" type="text" @click="viewClientData(scope.row)">详情</el-button>
|
||||||
</template>
|
</template>
|
||||||
@@ -241,6 +241,111 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
|
<!-- 用户反馈列表弹窗 -->
|
||||||
|
<el-dialog title="用户反馈列表" :visible.sync="feedbackDialogVisible" width="80%">
|
||||||
|
<el-table :data="feedbackList" style="width: 100%" v-loading="feedbackLoading">
|
||||||
|
<el-table-column prop="id" label="ID" width="80"></el-table-column>
|
||||||
|
<el-table-column prop="username" label="用户名" width="120"></el-table-column>
|
||||||
|
<el-table-column prop="deviceId" label="设备ID" width="200" show-overflow-tooltip></el-table-column>
|
||||||
|
<el-table-column prop="feedbackContent" label="反馈内容" min-width="250" show-overflow-tooltip></el-table-column>
|
||||||
|
<el-table-column prop="logDate" label="日志日期" width="120">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<span>{{ scope.row.logDate || '-' }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="status" label="状态" width="100">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<el-tag :type="getFeedbackStatusType(scope.row.status)" size="small">
|
||||||
|
{{ getFeedbackStatusText(scope.row.status) }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="createTime" label="提交时间" width="180"></el-table-column>
|
||||||
|
<el-table-column label="操作" width="300" fixed="right">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<el-button size="mini" type="text" @click="viewFeedbackDetail(scope.row)">详情</el-button>
|
||||||
|
<el-button size="mini" type="text" @click="viewFeedbackLog(scope.row)" v-if="scope.row.logFilePath">
|
||||||
|
查看日志
|
||||||
|
</el-button>
|
||||||
|
<el-button size="mini" type="text" @click="handleFeedbackStatus(scope.row, 'processing')"
|
||||||
|
v-if="scope.row.status === 'pending'">
|
||||||
|
处理中
|
||||||
|
</el-button>
|
||||||
|
<el-button size="mini" type="text" @click="handleFeedbackStatus(scope.row, 'completed')"
|
||||||
|
v-if="scope.row.status !== 'completed'">
|
||||||
|
完成
|
||||||
|
</el-button>
|
||||||
|
<el-button size="mini" type="text" style="color: #F56C6C;" @click="deleteFeedback(scope.row)">
|
||||||
|
删除
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
<div class="pagination-container">
|
||||||
|
<el-pagination
|
||||||
|
background
|
||||||
|
@size-change="handleFeedbackSizeChange"
|
||||||
|
@current-change="handleFeedbackCurrentChange"
|
||||||
|
:current-page="feedbackQueryParams.pageNum"
|
||||||
|
:page-sizes="[10, 20, 30, 50]"
|
||||||
|
:page-size="feedbackQueryParams.pageSize"
|
||||||
|
layout="total, sizes, prev, pager, next, jumper"
|
||||||
|
:total="feedbackTotal">
|
||||||
|
</el-pagination>
|
||||||
|
</div>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
<!-- 反馈详情弹窗 -->
|
||||||
|
<el-dialog title="反馈详情" :visible.sync="feedbackDetailDialogVisible" width="60%">
|
||||||
|
<el-descriptions :column="2" border>
|
||||||
|
<el-descriptions-item label="反馈ID">{{ currentFeedback.id }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="用户名">{{ currentFeedback.username }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="设备ID" :span="2">{{ currentFeedback.deviceId }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="状态">
|
||||||
|
<el-tag :type="getFeedbackStatusType(currentFeedback.status)" size="small">
|
||||||
|
{{ getFeedbackStatusText(currentFeedback.status) }}
|
||||||
|
</el-tag>
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="日志日期">
|
||||||
|
{{ currentFeedback.logDate || '未附带日志' }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="提交时间" :span="2">{{ currentFeedback.createTime }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="反馈内容" :span="2">
|
||||||
|
<div style="white-space: pre-wrap; word-wrap: break-word;">{{ currentFeedback.feedbackContent }}</div>
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="处理备注" :span="2" v-if="currentFeedback.remark">
|
||||||
|
<div style="white-space: pre-wrap; word-wrap: break-word;">{{ currentFeedback.remark }}</div>
|
||||||
|
</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
<div slot="footer" class="dialog-footer">
|
||||||
|
<el-button @click="feedbackDetailDialogVisible = false">关闭</el-button>
|
||||||
|
<el-button type="primary" @click="viewFeedbackLog(currentFeedback)" v-if="currentFeedback.logFilePath">
|
||||||
|
查看日志
|
||||||
|
</el-button>
|
||||||
|
<el-button @click="downloadFeedbackLogFile(currentFeedback)" v-if="currentFeedback.logFilePath">
|
||||||
|
下载日志
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
<!-- 反馈日志查看弹窗 -->
|
||||||
|
<el-dialog :title="'反馈日志: ' + currentFeedbackLog.username" :visible.sync="feedbackLogDialogVisible" width="80%" top="5vh">
|
||||||
|
<div class="log-container">
|
||||||
|
<div class="log-header">
|
||||||
|
<el-button type="primary" size="small" @click="loadFeedbackLog(currentFeedbackLog.id)" :loading="feedbackLogLoading">
|
||||||
|
<i class="el-icon-refresh"></i> 刷新
|
||||||
|
</el-button>
|
||||||
|
<el-button type="success" size="small" @click="downloadFeedbackLogFile(currentFeedbackLog)">
|
||||||
|
<i class="el-icon-download"></i> 下载日志
|
||||||
|
</el-button>
|
||||||
|
<span class="log-info">日志日期: {{ currentFeedbackLog.logDate || '-' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="log-content" v-loading="feedbackLogLoading">
|
||||||
|
<pre class="log-text">{{ feedbackLogContent }}</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-dialog>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -250,6 +355,7 @@ import LineChart from '@/components/Charts/LineChart'
|
|||||||
import PieChart from '@/components/Charts/PieChart'
|
import PieChart from '@/components/Charts/PieChart'
|
||||||
import { listInfo, listError, listData } from '@/api/monitor/client'
|
import { listInfo, listError, listData } from '@/api/monitor/client'
|
||||||
import { getVersionInfo } from '@/api/monitor/version'
|
import { getVersionInfo } from '@/api/monitor/version'
|
||||||
|
import { listFeedback, getFeedback, updateFeedbackStatus, delFeedback, getFeedbackLogContent, downloadFeedbackLog } from '@/api/monitor/feedback'
|
||||||
import request from '@/utils/request'
|
import request from '@/utils/request'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@@ -321,7 +427,27 @@ export default {
|
|||||||
logLoading: false,
|
logLoading: false,
|
||||||
currentLogClient: {},
|
currentLogClient: {},
|
||||||
logContent: '',
|
logContent: '',
|
||||||
lastLogUpdate: ''
|
lastLogUpdate: '',
|
||||||
|
// 用户反馈
|
||||||
|
feedbackDialogVisible: false,
|
||||||
|
feedbackLoading: false,
|
||||||
|
feedbackList: [],
|
||||||
|
feedbackQueryParams: {
|
||||||
|
pageNum: 1,
|
||||||
|
pageSize: 10
|
||||||
|
},
|
||||||
|
feedbackTotal: 0,
|
||||||
|
feedbackDetailDialogVisible: false,
|
||||||
|
currentFeedback: {},
|
||||||
|
feedbackStats: {
|
||||||
|
pendingCount: 0,
|
||||||
|
todayCount: 0
|
||||||
|
},
|
||||||
|
// 反馈日志查看
|
||||||
|
feedbackLogDialogVisible: false,
|
||||||
|
feedbackLogLoading: false,
|
||||||
|
feedbackLogContent: '',
|
||||||
|
currentFeedbackLog: {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
@@ -329,6 +455,7 @@ export default {
|
|||||||
this.getOnlineClientTrend()
|
this.getOnlineClientTrend()
|
||||||
this.getDataTypeDistribution()
|
this.getDataTypeDistribution()
|
||||||
this.getClientList()
|
this.getClientList()
|
||||||
|
this.getFeedbackStatistics()
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
// 获取统计数据
|
// 获取统计数据
|
||||||
@@ -558,6 +685,186 @@ export default {
|
|||||||
console.error('获取版本信息失败:', error)
|
console.error('获取版本信息失败:', error)
|
||||||
this.$message.error('获取下载链接失败: ' + (error.message || '网络错误'))
|
this.$message.error('获取下载链接失败: ' + (error.message || '网络错误'))
|
||||||
})
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
// 获取反馈统计信息
|
||||||
|
getFeedbackStatistics() {
|
||||||
|
request.get('/monitor/feedback/statistics').then(res => {
|
||||||
|
if (res.code === 200) {
|
||||||
|
this.feedbackStats = {
|
||||||
|
pendingCount: res.data.pendingCount || 0,
|
||||||
|
todayCount: res.data.todayCount || 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}).catch(error => {
|
||||||
|
console.error('获取反馈统计失败:', error)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
// 显示反馈列表
|
||||||
|
showFeedbackList() {
|
||||||
|
this.feedbackDialogVisible = true
|
||||||
|
this.getFeedbackList()
|
||||||
|
},
|
||||||
|
|
||||||
|
// 获取反馈列表
|
||||||
|
getFeedbackList() {
|
||||||
|
this.feedbackLoading = true
|
||||||
|
listFeedback(this.feedbackQueryParams).then(res => {
|
||||||
|
this.feedbackLoading = false
|
||||||
|
if (res.code === 200) {
|
||||||
|
this.feedbackList = res.rows || []
|
||||||
|
this.feedbackTotal = res.total || 0
|
||||||
|
}
|
||||||
|
}).catch(() => {
|
||||||
|
this.feedbackLoading = false
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
// 查看反馈详情
|
||||||
|
viewFeedbackDetail(row) {
|
||||||
|
this.currentFeedback = {...row}
|
||||||
|
this.feedbackDetailDialogVisible = true
|
||||||
|
},
|
||||||
|
|
||||||
|
// 查看反馈日志
|
||||||
|
viewFeedbackLog(row) {
|
||||||
|
if (!row.logFilePath) {
|
||||||
|
this.$message.warning('该反馈未附带日志文件')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.currentFeedbackLog = {...row}
|
||||||
|
this.feedbackLogDialogVisible = true
|
||||||
|
this.loadFeedbackLog(row.id)
|
||||||
|
},
|
||||||
|
|
||||||
|
// 加载反馈日志内容
|
||||||
|
loadFeedbackLog(id) {
|
||||||
|
this.feedbackLogLoading = true
|
||||||
|
this.feedbackLogContent = ''
|
||||||
|
getFeedbackLogContent(id).then(res => {
|
||||||
|
this.feedbackLogLoading = false
|
||||||
|
if (res.code === 200) {
|
||||||
|
this.feedbackLogContent = res.data.content || '日志内容为空'
|
||||||
|
} else {
|
||||||
|
this.$message.error(res.msg || '获取日志失败')
|
||||||
|
this.feedbackLogContent = '获取日志失败'
|
||||||
|
}
|
||||||
|
}).catch(error => {
|
||||||
|
this.feedbackLogLoading = false
|
||||||
|
this.$message.error('获取日志失败: ' + (error.message || '未知错误'))
|
||||||
|
this.feedbackLogContent = '获取日志失败: ' + (error.message || '未知错误')
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
// 下载反馈日志
|
||||||
|
downloadFeedbackLogFile(row) {
|
||||||
|
if (!row.logFilePath) {
|
||||||
|
this.$message.warning('该反馈未附带日志文件')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// 使用request下载文件(带token)
|
||||||
|
const url = '/monitor/feedback/log/download/' + row.id
|
||||||
|
const loading = this.$loading({
|
||||||
|
lock: true,
|
||||||
|
text: '正在下载日志文件...',
|
||||||
|
spinner: 'el-icon-loading',
|
||||||
|
background: 'rgba(0, 0, 0, 0.7)'
|
||||||
|
})
|
||||||
|
|
||||||
|
request({
|
||||||
|
url: url,
|
||||||
|
method: 'get',
|
||||||
|
responseType: 'blob'
|
||||||
|
}).then(response => {
|
||||||
|
const blob = new Blob([response])
|
||||||
|
const link = document.createElement('a')
|
||||||
|
link.href = window.URL.createObjectURL(blob)
|
||||||
|
link.download = `feedback_${row.id}_log.txt`
|
||||||
|
link.click()
|
||||||
|
window.URL.revokeObjectURL(link.href)
|
||||||
|
loading.close()
|
||||||
|
this.$message.success('下载成功')
|
||||||
|
}).catch(error => {
|
||||||
|
loading.close()
|
||||||
|
console.error('下载失败:', error)
|
||||||
|
this.$message.error('下载失败: ' + (error.message || '请稍后重试'))
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
// 更新反馈状态
|
||||||
|
handleFeedbackStatus(row, status) {
|
||||||
|
this.$prompt('请输入处理备注(可选)', '更新状态', {
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
inputType: 'textarea'
|
||||||
|
}).then(({ value }) => {
|
||||||
|
updateFeedbackStatus(row.id, {
|
||||||
|
status: status,
|
||||||
|
remark: value || ''
|
||||||
|
}).then(res => {
|
||||||
|
if (res.code === 200) {
|
||||||
|
this.$message.success('状态更新成功')
|
||||||
|
this.getFeedbackList()
|
||||||
|
this.getFeedbackStatistics()
|
||||||
|
} else {
|
||||||
|
this.$message.error(res.msg || '更新失败')
|
||||||
|
}
|
||||||
|
}).catch(error => {
|
||||||
|
this.$message.error('更新失败: ' + (error.message || '未知错误'))
|
||||||
|
})
|
||||||
|
}).catch(() => {})
|
||||||
|
},
|
||||||
|
|
||||||
|
// 删除反馈
|
||||||
|
deleteFeedback(row) {
|
||||||
|
this.$confirm('确定要删除该反馈吗?', '提示', {
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning'
|
||||||
|
}).then(() => {
|
||||||
|
delFeedback(row.id).then(res => {
|
||||||
|
if (res.code === 200) {
|
||||||
|
this.$message.success('删除成功')
|
||||||
|
this.getFeedbackList()
|
||||||
|
this.getFeedbackStatistics()
|
||||||
|
} else {
|
||||||
|
this.$message.error(res.msg || '删除失败')
|
||||||
|
}
|
||||||
|
}).catch(error => {
|
||||||
|
this.$message.error('删除失败: ' + (error.message || '未知错误'))
|
||||||
|
})
|
||||||
|
}).catch(() => {})
|
||||||
|
},
|
||||||
|
|
||||||
|
// 反馈分页
|
||||||
|
handleFeedbackSizeChange(val) {
|
||||||
|
this.feedbackQueryParams.pageSize = val
|
||||||
|
this.getFeedbackList()
|
||||||
|
},
|
||||||
|
handleFeedbackCurrentChange(val) {
|
||||||
|
this.feedbackQueryParams.pageNum = val
|
||||||
|
this.getFeedbackList()
|
||||||
|
},
|
||||||
|
|
||||||
|
// 获取状态标签类型
|
||||||
|
getFeedbackStatusType(status) {
|
||||||
|
const typeMap = {
|
||||||
|
'pending': 'warning',
|
||||||
|
'processing': 'primary',
|
||||||
|
'completed': 'success'
|
||||||
|
}
|
||||||
|
return typeMap[status] || 'info'
|
||||||
|
},
|
||||||
|
|
||||||
|
// 获取状态文本
|
||||||
|
getFeedbackStatusText(status) {
|
||||||
|
const textMap = {
|
||||||
|
'pending': '待处理',
|
||||||
|
'processing': '处理中',
|
||||||
|
'completed': '已完成'
|
||||||
|
}
|
||||||
|
return textMap[status] || status
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user