Compare commits

...

2 Commits

Author SHA1 Message Date
cce281497b feat(client): 添加品牌logo功能支持
- 在客户端账户实体中新增brandLogo字段用于存储品牌logo URL
- 实现品牌logo的上传、获取和删除接口
- 在Vue前端中集成品牌logo的展示和管理功能- 添加品牌logo的缓存机制提升访问性能
- 在设置对话框中增加品牌logo配置界面
- 实现品牌logo的预览、上传和删除操作
- 添加VIP权限控制确保只有VIP用户可使用该功能
- 增加品牌logo变更事件监听以实时更新界面显示- 更新数据库映射文件以支持brand_logo字段的读写- 在登录成功后异步加载品牌logo配置信息- 调整UI布局以适配品牌logo展示区域- 添加品牌logo相关的样式定义和响应式处理
- 实现品牌logo上传的文件类型和大小校验- 增加品牌logo删除确认提示增强用户体验
- 在App.vue中添加品牌logo的全局状态管理和展示逻辑- 优化品牌logo加载失败时的容错处理
- 完善品牌logo功能的相关错误处理和日志记录
2025-11-10 15:18:38 +08:00
92ab782943 refactor(utils):优化商标查询重试机制- 将初始化失败重试次数从3次增加到5次
- 改进批量查询逻辑,支持单个品牌独立重试- 添加针对HTTP 403、网络错误等异常的代理切换机制-优化查询脚本构造方式,提高执行稳定性
- 增强错误处理和日志输出信息
- 移除控制器中冗余的注释描述
2025-11-10 11:21:04 +08:00
11 changed files with 573 additions and 219 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@@ -5,6 +5,7 @@ import zhCn from 'element-plus/es/locale/lang/zh-cn'
import 'element-plus/dist/index.css' import 'element-plus/dist/index.css'
import {authApi} 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 {splashApi} from './api/splash'
import {getOrCreateDeviceId} from './utils/deviceId' import {getOrCreateDeviceId} from './utils/deviceId'
import {getToken, setToken, removeToken, getUsernameFromToken, getClientIdFromToken} from './utils/token' import {getToken, setToken, removeToken, getUsernameFromToken, getClientIdFromToken} from './utils/token'
import {CONFIG} from './api/http' import {CONFIG} from './api/http'
@@ -61,12 +62,12 @@ const vipExpireTime = ref<Date | null>(null)
const deviceTrialExpired = ref(false) const deviceTrialExpired = ref(false)
const accountType = ref<string>('trial') const accountType = ref<string>('trial')
const vipStatus = computed(() => { const vipStatus = computed(() => {
if (!vipExpireTime.value) return { isVip: false, daysLeft: 0, hoursLeft: 0, status: 'expired', expiredType: 'account' } if (!vipExpireTime.value) return {isVip: false, daysLeft: 0, hoursLeft: 0, status: 'expired', expiredType: 'account'}
const now = new Date() const now = new Date()
const expire = new Date(vipExpireTime.value) const expire = new Date(vipExpireTime.value)
const msLeft = expire.getTime() - now.getTime() const msLeft = expire.getTime() - now.getTime()
// 精确判断:当前时间 >= 过期时间,则已过期(与后端逻辑一致) // 精确判断:当前时间 >= 过期时间,则已过期(与后端逻辑一致)
if (msLeft <= 0) { if (msLeft <= 0) {
const accountExpired = true const accountExpired = true
@@ -75,22 +76,22 @@ const vipStatus = computed(() => {
if (deviceExpired && accountExpired) expiredType = 'both' if (deviceExpired && accountExpired) expiredType = 'both'
else if (accountExpired) expiredType = 'account' else if (accountExpired) expiredType = 'account'
else if (deviceExpired) expiredType = 'device' else if (deviceExpired) expiredType = 'device'
return { isVip: false, daysLeft: 0, hoursLeft: 0, status: 'expired', expiredType } return {isVip: false, daysLeft: 0, hoursLeft: 0, status: 'expired', expiredType}
} }
const hoursLeft = Math.floor(msLeft / (1000 * 60 * 60)) const hoursLeft = Math.floor(msLeft / (1000 * 60 * 60))
const daysLeft = Math.floor(msLeft / (1000 * 60 * 60 * 24)) const daysLeft = Math.floor(msLeft / (1000 * 60 * 60 * 24))
let expiredType: 'device' | 'account' | 'both' | 'subscribe' = 'subscribe' let expiredType: 'device' | 'account' | 'both' | 'subscribe' = 'subscribe'
if (accountType.value === 'trial' && deviceTrialExpired.value) { if (accountType.value === 'trial' && deviceTrialExpired.value) {
expiredType = 'device' // 试用账号且设备过期 expiredType = 'device' // 试用账号且设备过期
} }
if (daysLeft === 0) return { isVip: true, daysLeft, hoursLeft, status: 'warning', expiredType } if (daysLeft === 0) return {isVip: true, daysLeft, hoursLeft, status: 'warning', expiredType}
if (daysLeft <= 7) return { isVip: true, daysLeft, hoursLeft, status: 'warning', expiredType } if (daysLeft <= 7) return {isVip: true, daysLeft, hoursLeft, status: 'warning', expiredType}
if (daysLeft <= 30) return { isVip: true, daysLeft, hoursLeft, status: 'normal', expiredType } if (daysLeft <= 30) return {isVip: true, daysLeft, hoursLeft, status: 'normal', expiredType}
return { isVip: true, daysLeft, hoursLeft, status: 'active', expiredType } return {isVip: true, daysLeft, hoursLeft, status: 'active', expiredType}
}) })
// 功能可用性账号VIP + 设备试用期) // 功能可用性账号VIP + 设备试用期)
@@ -118,6 +119,9 @@ const showAccountManager = ref(false)
// 当前版本 // 当前版本
const currentVersion = ref('') const currentVersion = ref('')
// 品牌logo
const brandLogoUrl = ref('')
// 菜单配置 - 复刻ERP客户端格式 // 菜单配置 - 复刻ERP客户端格式
const menuConfig = [ const menuConfig = [
{key: 'rakuten', name: 'Rakuten', index: 'rakuten', icon: 'R', iconImage: rakutenIcon}, {key: 'rakuten', name: 'Rakuten', index: 'rakuten', icon: 'R', iconImage: rakutenIcon},
@@ -198,19 +202,25 @@ function handleMenuSelect(key: string) {
addToHistory(key) addToHistory(key)
} }
async function handleLoginSuccess(data: { token: string; permissions?: string; expireTime?: string; accountType?: string; deviceTrialExpired?: boolean }) { async function handleLoginSuccess(data: {
token: string;
permissions?: string;
expireTime?: string;
accountType?: string;
deviceTrialExpired?: boolean
}) {
try { try {
setToken(data.token) setToken(data.token)
isAuthenticated.value = true isAuthenticated.value = true
showAuthDialog.value = false showAuthDialog.value = false
showRegDialog.value = false showRegDialog.value = false
currentUsername.value = getUsernameFromToken(data.token) currentUsername.value = getUsernameFromToken(data.token)
userPermissions.value = data.permissions || '' userPermissions.value = data.permissions || ''
vipExpireTime.value = data.expireTime ? new Date(data.expireTime) : null vipExpireTime.value = data.expireTime ? new Date(data.expireTime) : null
accountType.value = data.accountType || 'trial' accountType.value = data.accountType || 'trial'
deviceTrialExpired.value = data.deviceTrialExpired || false deviceTrialExpired.value = data.deviceTrialExpired || false
const deviceId = await getOrCreateDeviceId() const deviceId = await getOrCreateDeviceId()
await deviceApi.register({ await deviceApi.register({
username: currentUsername.value, username: currentUsername.value,
@@ -218,7 +228,7 @@ async function handleLoginSuccess(data: { token: string; permissions?: string; e
os: navigator.platform os: navigator.platform
}) })
SSEManager.connect() SSEManager.connect()
// 同步当前账号的设置到 Electron 主进程 // 同步当前账号的设置到 Electron 主进程
syncSettingsToElectron() syncSettingsToElectron()
@@ -231,7 +241,7 @@ async function handleLoginSuccess(data: { token: string; permissions?: string; e
isAuthenticated.value = false isAuthenticated.value = false
showAuthDialog.value = true showAuthDialog.value = true
removeToken() removeToken()
} }
} }
@@ -251,7 +261,7 @@ function clearLocalAuth() {
async function logout() { async function logout() {
try { try {
const deviceId = getClientIdFromToken() const deviceId = getClientIdFromToken()
if (deviceId) await deviceApi.offline({ deviceId, username: currentUsername.value }) if (deviceId) await deviceApi.offline({deviceId, username: currentUsername.value})
} catch (error) { } catch (error) {
console.warn('离线通知失败:', error) console.warn('离线通知失败:', error)
} }
@@ -270,7 +280,8 @@ async function handleUserClick() {
cancelButtonText: '取消' cancelButtonText: '取消'
}) })
await logout() await logout()
} catch {} } catch {
}
} }
function showRegisterDialog() { function showRegisterDialog() {
@@ -280,7 +291,7 @@ function showRegisterDialog() {
function backToLogin() { function backToLogin() {
showRegDialog.value = false showRegDialog.value = false
showAuthDialog.value = true showAuthDialog.value = true
} }
@@ -300,13 +311,13 @@ async function checkAuth() {
userPermissions.value = res.data.permissions || '' userPermissions.value = res.data.permissions || ''
deviceTrialExpired.value = res.data.deviceTrialExpired || false deviceTrialExpired.value = res.data.deviceTrialExpired || false
accountType.value = res.data.accountType || 'trial' accountType.value = res.data.accountType || 'trial'
if (res.data.expireTime) { if (res.data.expireTime) {
vipExpireTime.value = new Date(res.data.expireTime) vipExpireTime.value = new Date(res.data.expireTime)
} }
SSEManager.connect() SSEManager.connect()
// 同步当前账号的设置到 Electron 主进程 // 同步当前账号的设置到 Electron 主进程
syncSettingsToElectron() syncSettingsToElectron()
} catch { } catch {
@@ -322,7 +333,7 @@ async function refreshVipStatus() {
try { try {
const token = getToken() const token = getToken()
if (!token) return false if (!token) return false
const res = await authApi.verifyToken(token) const res = await authApi.verifyToken(token)
deviceTrialExpired.value = res.data.deviceTrialExpired || false deviceTrialExpired.value = res.data.deviceTrialExpired || false
accountType.value = res.data.accountType || 'trial' accountType.value = res.data.accountType || 'trial'
@@ -346,10 +357,10 @@ async function syncSettingsToElectron() {
try { try {
const username = getUsernameFromToken() const username = getUsernameFromToken()
const settings = getSettings(username) const settings = getSettings(username)
// 同步关闭行为 // 同步关闭行为
await (window as any).electronAPI.setCloseAction(settings.closeAction || 'quit') await (window as any).electronAPI.setCloseAction(settings.closeAction || 'quit')
// 同步启动配置 // 同步启动配置
await (window as any).electronAPI.setLaunchConfig({ await (window as any).electronAPI.setLaunchConfig({
autoLaunch: settings.autoLaunch || false, autoLaunch: settings.autoLaunch || false,
@@ -360,6 +371,19 @@ async function syncSettingsToElectron() {
} }
} }
// 加载品牌logo
async function loadBrandLogo() {
try {
const username = getUsernameFromToken()
if (!username) return
const res = await splashApi.getBrandLogo(username)
brandLogoUrl.value = res.data.url
} catch (error) {
brandLogoUrl.value = ''
}
}
// 提供给子组件使用 // 提供给子组件使用
provide('refreshVipStatus', refreshVipStatus) provide('refreshVipStatus', refreshVipStatus)
provide('vipStatus', vipStatus) provide('vipStatus', vipStatus)
@@ -518,24 +542,32 @@ async function confirmRemoveDevice(row: DeviceItem) {
onMounted(async () => { onMounted(async () => {
showContent() showContent()
await checkAuth() await checkAuth()
// 检查是否有待安装的更新 // 检查是否有待安装的更新
await checkPendingUpdate() await checkPendingUpdate()
// 加载当前版本 // 加载当前版本
try { try {
currentVersion.value = await (window as any).electronAPI.getJarVersion() currentVersion.value = await (window as any).electronAPI.getJarVersion()
} catch (error) { } catch (error) {
console.warn('获取当前版本失败:', error) console.warn('获取当前版本失败:', error)
} }
// 加载品牌logo
loadBrandLogo()
// 监听品牌logo变化
window.addEventListener('brandLogoChanged', (e: any) => {
brandLogoUrl.value = e.detail
})
// 全局阻止文件拖拽到窗口(避免意外打开文件) // 全局阻止文件拖拽到窗口(避免意外打开文件)
// 只在指定的 dropzone 区域处理拖拽上传 // 只在指定的 dropzone 区域处理拖拽上传
document.addEventListener('dragover', (e) => { document.addEventListener('dragover', (e) => {
e.preventDefault() e.preventDefault()
e.stopPropagation() e.stopPropagation()
}, false) }, false)
document.addEventListener('drop', (e) => { document.addEventListener('drop', (e) => {
e.preventDefault() e.preventDefault()
e.stopPropagation() e.stopPropagation()
@@ -584,6 +616,12 @@ onUnmounted(() => {
<div class="user-avatar"> <div class="user-avatar">
<img src="/icon/icon.png" alt="logo"/> <img src="/icon/icon.png" alt="logo"/>
</div> </div>
<!-- 品牌logo区域有logo时显示 -->
<div v-if="brandLogoUrl" class="brand-logo-section">
<img :src="brandLogoUrl" alt="品牌logo" class="brand-logo"/>
</div>
<div class="menu-group-title">电商平台</div> <div class="menu-group-title">电商平台</div>
<ul class="menu"> <ul class="menu">
<li <li
@@ -595,7 +633,7 @@ onUnmounted(() => {
> >
<span class="menu-text"> <span class="menu-text">
<span class="menu-icon" :data-k="item.key"> <span class="menu-icon" :data-k="item.key">
<img v-if="item.iconImage" :src="item.iconImage" :alt="item.name" class="menu-icon-img" /> <img v-if="item.iconImage" :src="item.iconImage" :alt="item.name" class="menu-icon-img"/>
<template v-else>{{ item.icon }}</template> <template v-else>{{ item.icon }}</template>
</span> </span>
{{ item.name }} {{ item.name }}
@@ -604,7 +642,8 @@ onUnmounted(() => {
</ul> </ul>
<!-- VIP状态卡片 --> <!-- VIP状态卡片 -->
<div v-if="isAuthenticated" class="vip-status-card" :class="'vip-' + vipStatus.status" @click="openSubscriptionDialog"> <div v-if="isAuthenticated" class="vip-status-card" :class="'vip-' + vipStatus.status"
@click="openSubscriptionDialog">
<div class="vip-info"> <div class="vip-info">
<div class="vip-status-text"> <div class="vip-status-text">
<template v-if="vipStatus.isVip"> <template v-if="vipStatus.isVip">
@@ -616,7 +655,13 @@ onUnmounted(() => {
</template> </template>
</div> </div>
<div class="vip-expire-date" v-if="vipExpireTime"> <div class="vip-expire-date" v-if="vipExpireTime">
有效期至{{ new Date(vipExpireTime).toLocaleDateString('zh-CN', { year: 'numeric', month: 'long', day: 'numeric' }) }} 有效期至{{
new Date(vipExpireTime).toLocaleDateString('zh-CN', {
year: 'numeric',
month: 'long',
day: 'numeric'
})
}}
</div> </div>
</div> </div>
</div> </div>
@@ -672,16 +717,20 @@ onUnmounted(() => {
@back-to-login="backToLogin"/> @back-to-login="backToLogin"/>
<!-- 更新对话框 --> <!-- 更新对话框 -->
<UpdateDialog ref="updateDialogRef" v-model="showUpdateDialog" /> <UpdateDialog ref="updateDialogRef" v-model="showUpdateDialog"/>
<!-- 设置对话框 --> <!-- 设置对话框 -->
<SettingsDialog v-model="showSettingsDialog" @auto-update-changed="handleAutoUpdateChanged" @open-update-dialog="handleOpenUpdateDialog" /> <SettingsDialog
v-model="showSettingsDialog"
:is-vip="canUseFunctions"
@auto-update-changed="handleAutoUpdateChanged"
@open-update-dialog="handleOpenUpdateDialog" />
<!-- 试用期过期弹框 --> <!-- 试用期过期弹框 -->
<TrialExpiredDialog v-model="showTrialExpiredDialog" :expired-type="trialExpiredType" /> <TrialExpiredDialog v-model="showTrialExpiredDialog" :expired-type="trialExpiredType"/>
<!-- 账号管理弹框 --> <!-- 账号管理弹框 -->
<AccountManager v-model="showAccountManager" platform="zebra" /> <AccountManager v-model="showAccountManager" platform="zebra"/>
<!-- 设备管理弹框 --> <!-- 设备管理弹框 -->
<el-dialog <el-dialog
@@ -693,7 +742,9 @@ onUnmounted(() => {
<template #header> <template #header>
<div class="device-dialog-header"> <div class="device-dialog-header">
<img src="/icon/img.png" alt="devices" class="device-illustration"/> <img src="/icon/img.png" alt="devices" class="device-illustration"/>
<div class="device-title">设备管理 <span class="device-count">({{ deviceQuota.used || 0 }}/{{ deviceQuota.limit || 0 }})</span></div> <div class="device-title">设备管理 <span class="device-count">({{
deviceQuota.used || 0
}}/{{ deviceQuota.limit || 0 }})</span></div>
<div class="device-subtitle">当前账号可以授权绑定 {{ deviceQuota.limit }} 台设备</div> <div class="device-subtitle">当前账号可以授权绑定 {{ deviceQuota.limit }} 台设备</div>
</div> </div>
</template> </template>
@@ -829,6 +880,24 @@ onUnmounted(() => {
text-align: left; text-align: left;
} }
/* 品牌logo区域 */
.brand-logo-section {
display: flex;
justify-content: center;
align-items: center;
width: 100%;
height: 48px;
margin-bottom: 12px;
padding: 0 4px;
box-sizing: border-box;
}
.brand-logo {
max-width: 100%;
max-height: 100%;
object-fit: contain;
}
.menu { .menu {
list-style: none; list-style: none;
padding: 0; padding: 0;
@@ -952,6 +1021,7 @@ onUnmounted(() => {
font-size: 13px; font-size: 13px;
color: #606266; color: #606266;
} }
.device-dialog-header { .device-dialog-header {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -959,31 +1029,45 @@ onUnmounted(() => {
padding: 12px 0 4px 0; padding: 12px 0 4px 0;
margin-left: 40px; margin-left: 40px;
} }
.device-dialog :deep(.el-dialog__header) { .device-dialog :deep(.el-dialog__header) {
text-align: center; text-align: center;
} }
.device-dialog :deep(.el-dialog__body) { padding-top: 0; }
.device-dialog :deep(.el-dialog__body) {
padding-top: 0;
}
.device-illustration { .device-illustration {
width: 180px; width: 180px;
height: auto; height: auto;
object-fit: contain; object-fit: contain;
margin-bottom: 8px; margin-bottom: 8px;
} }
.device-title { .device-title {
font-size: 18px; font-size: 18px;
font-weight: 600; font-weight: 600;
color: #303133; color: #303133;
margin-bottom: 6px; margin-bottom: 6px;
} }
.device-count { color: #909399; font-weight: 500; }
.device-subtitle { font-size: 12px; color: #909399; } .device-count {
color: #909399;
font-weight: 500;
}
.device-subtitle {
font-size: 12px;
color: #909399;
}
/* 浮动版本信息 */ /* 浮动版本信息 */
.version-info { .version-info {
position: fixed; position: fixed;
right: 10px; right: 10px;
bottom: 10px; bottom: 10px;
background: rgba(255,255,255,0.9); background: rgba(255, 255, 255, 0.9);
padding: 5px 10px; padding: 5px 10px;
border-radius: 4px; border-radius: 4px;
font-size: 12px; font-size: 12px;

View File

@@ -27,6 +27,24 @@ export const splashApi = {
// 删除自定义开屏图片(恢复默认) // 删除自定义开屏图片(恢复默认)
async deleteSplashImage(username: string) { async deleteSplashImage(username: string) {
return http.post<{ data: string }>(`/monitor/account/splash-image/delete?username=${username}`) return http.post<{ data: string }>(`/monitor/account/splash-image/delete?username=${username}`)
},
// 上传品牌logo
async uploadBrandLogo(file: File, username: string) {
const formData = new FormData()
formData.append('file', file)
formData.append('username', username)
return http.upload<{ data: { url: string; fileName: string } }>('/monitor/account/brand-logo/upload', formData)
},
// 获取当前用户的品牌logo
async getBrandLogo(username: string) {
return http.get<{ data: { url: string } }>('/monitor/account/brand-logo', { username })
},
// 删除品牌logo
async deleteBrandLogo(username: string) {
return http.post<{ data: string }>(`/monitor/account/brand-logo/delete?username=${username}`)
} }
} }

View File

@@ -35,16 +35,17 @@ async function handleAuth() {
try { try {
// 获取或生成设备ID // 获取或生成设备ID
const deviceId = await getOrCreateDeviceId() const deviceId = await getOrCreateDeviceId()
// 登录 // 登录
const loginRes: any = await authApi.login({ const loginRes: any = await authApi.login({
...authForm.value, ...authForm.value,
clientId: deviceId clientId: deviceId
}) })
// 保存开屏图片配置(不阻塞登录) // 保存开屏图片配置和品牌logo(不阻塞登录)
saveSplashConfigInBackground(authForm.value.username) saveSplashConfigInBackground(authForm.value.username)
saveBrandLogoInBackground(authForm.value.username)
emit('loginSuccess', { emit('loginSuccess', {
token: loginRes.data.accessToken || loginRes.data.token, token: loginRes.data.accessToken || loginRes.data.token,
permissions: loginRes.data.permissions, permissions: loginRes.data.permissions,
@@ -90,6 +91,18 @@ async function saveSplashConfigInBackground(username: string) {
console.error('[开屏图片] 保存配置失败:', error) console.error('[开屏图片] 保存配置失败:', error)
} }
} }
// 保存品牌logo配置
async function saveBrandLogoInBackground(username: string) {
try {
const res = await splashApi.getBrandLogo(username)
const url = res?.data?.url || ''
// 触发App.vue加载品牌logo
window.dispatchEvent(new CustomEvent('brandLogoChanged', { detail: url }))
} catch (error) {
console.error('[品牌logo] 加载配置失败:', error)
}
}
</script> </script>
<template> <template>

View File

@@ -1,12 +1,12 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, onMounted, watch, nextTick } from 'vue' import { ref, computed, onMounted, watch, nextTick, inject, defineAsyncComponent } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus' import { ElMessage, ElMessageBox } from 'element-plus'
import { import {
getSettings, getSettings,
saveSettings, saveSettings,
savePlatformSettings, savePlatformSettings,
type Platform, type Platform,
type PlatformExportSettings type PlatformExportSettings
} from '../../utils/settings' } from '../../utils/settings'
import { feedbackApi } from '../../api/feedback' import { feedbackApi } from '../../api/feedback'
import { getToken, getUsernameFromToken } from '../../utils/token' import { getToken, getUsernameFromToken } from '../../utils/token'
@@ -14,8 +14,13 @@ import { getOrCreateDeviceId } from '../../utils/deviceId'
import { updateApi } from '../../api/update' import { updateApi } from '../../api/update'
import { splashApi } from '../../api/splash' import { splashApi } from '../../api/splash'
const TrialExpiredDialog = defineAsyncComponent(() => import('./TrialExpiredDialog.vue'))
const refreshVipStatus = inject<() => Promise<boolean>>('refreshVipStatus')
const vipStatus = inject<any>('vipStatus')
interface Props { interface Props {
modelValue: boolean modelValue: boolean
isVip?: boolean
} }
interface Emits { interface Emits {
@@ -75,6 +80,16 @@ const uploadingSplashImage = ref(false)
const deletingSplashImage = ref(false) const deletingSplashImage = ref(false)
const fileInputRef = ref<HTMLInputElement | null>(null) const fileInputRef = ref<HTMLInputElement | null>(null)
// 品牌logo相关
const brandLogoUrl = ref('')
const uploadingBrandLogo = ref(false)
const deletingBrandLogo = ref(false)
const brandLogoInputRef = ref<HTMLInputElement | null>(null)
// VIP过期弹窗
const showTrialExpiredDialog = ref(false)
const trialExpiredType = ref<'device' | 'account' | 'both' | 'subscribe'>('account')
const show = computed({ const show = computed({
get: () => props.modelValue, get: () => props.modelValue,
set: (value) => emit('update:modelValue', value) set: (value) => emit('update:modelValue', value)
@@ -86,6 +101,7 @@ watch(() => props.modelValue, (newVal) => {
loadAllSettings() loadAllSettings()
loadCurrentVersion() loadCurrentVersion()
loadSplashImage() loadSplashImage()
loadBrandLogo()
} }
}) })
@@ -379,9 +395,9 @@ async function loadSplashImage() {
try { try {
const username = getUsernameFromToken() const username = getUsernameFromToken()
if (!username) return if (!username) return
const res = await splashApi.getSplashImage(username) const res = await splashApi.getSplashImage(username)
splashImageUrl.value = res?.data?.data?.url || res?.data?.url || '' splashImageUrl.value = res.data.url
} catch (error) { } catch (error) {
splashImageUrl.value = '' splashImageUrl.value = ''
} }
@@ -391,22 +407,28 @@ async function loadSplashImage() {
async function handleSplashImageUpload(event: Event) { async function handleSplashImageUpload(event: Event) {
const input = event.target as HTMLInputElement const input = event.target as HTMLInputElement
if (!input.files || input.files.length === 0) return if (!input.files || input.files.length === 0) return
// VIP验证
if (refreshVipStatus) await refreshVipStatus()
if (!props.isVip) {
if (vipStatus) trialExpiredType.value = vipStatus.value.expiredType
showTrialExpiredDialog.value = true
input.value = ''
return
}
const file = input.files[0] const file = input.files[0]
const username = getUsernameFromToken() const username = getUsernameFromToken()
if (!username) return ElMessage.warning('请先登录') if (!username) return ElMessage.warning('请先登录')
if (!file.type.startsWith('image/')) return ElMessage.error('只支持图片文件') if (!file.type.startsWith('image/')) return ElMessage.error('只支持图片文件')
if (file.size > 5 * 1024 * 1024) return ElMessage.error('图片大小不能超过5MB') if (file.size > 5 * 1024 * 1024) return ElMessage.error('图片大小不能超过5MB')
try { try {
uploadingSplashImage.value = true uploadingSplashImage.value = true
const res = await splashApi.uploadSplashImage(file, username) const res = await splashApi.uploadSplashImage(file, username)
const url = res?.data?.data?.url || res?.data?.url splashImageUrl.value = res.url
if (url) { await (window as any).electronAPI.saveSplashConfig(username, res.url)
splashImageUrl.value = url ElMessage.success('开屏图片设置成功,重启应用后生效')
await (window as any).electronAPI.saveSplashConfig(username, url)
ElMessage.success('开屏图片设置成功,重启应用后生效')
}
} catch (error: any) { } catch (error: any) {
ElMessage.error(error?.message || '上传失败') ElMessage.error(error?.message || '上传失败')
} finally { } finally {
@@ -423,11 +445,11 @@ async function handleDeleteSplashImage() {
'确认删除', '确认删除',
{ confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' } { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' }
) )
deletingSplashImage.value = true deletingSplashImage.value = true
const username = getUsernameFromToken() const username = getUsernameFromToken()
if (!username) return ElMessage.warning('请先登录') if (!username) return ElMessage.warning('请先登录')
await splashApi.deleteSplashImage(username) await splashApi.deleteSplashImage(username)
await (window as any).electronAPI.saveSplashConfig(username, '') await (window as any).electronAPI.saveSplashConfig(username, '')
splashImageUrl.value = '' splashImageUrl.value = ''
@@ -439,11 +461,87 @@ async function handleDeleteSplashImage() {
} }
} }
// 加载品牌logo
async function loadBrandLogo() {
try {
const username = getUsernameFromToken()
if (!username) return
const res = await splashApi.getBrandLogo(username)
brandLogoUrl.value = res.data.url
} catch (error) {
brandLogoUrl.value = ''
}
}
// 上传品牌logo
async function handleBrandLogoUpload(event: Event) {
const input = event.target as HTMLInputElement
if (!input.files || input.files.length === 0) return
// VIP验证
if (refreshVipStatus) await refreshVipStatus()
if (!props.isVip) {
if (vipStatus) trialExpiredType.value = vipStatus.value.expiredType
showTrialExpiredDialog.value = true
input.value = ''
return
}
const file = input.files[0]
const username = getUsernameFromToken()
if (!username) return ElMessage.warning('请先登录')
if (!file.type.startsWith('image/')) return ElMessage.error('只支持图片文件')
if (file.size > 5 * 1024 * 1024) return ElMessage.error('图片大小不能超过5MB')
try {
uploadingBrandLogo.value = true
const res = await splashApi.uploadBrandLogo(file, username)
brandLogoUrl.value = res.url
ElMessage.success('品牌logo设置成功')
window.dispatchEvent(new CustomEvent('brandLogoChanged', { detail: res.url }))
} catch (error: any) {
ElMessage.error(error?.message || '上传失败')
} finally {
uploadingBrandLogo.value = false
input.value = ''
}
}
// 删除品牌logo
async function handleDeleteBrandLogo() {
try {
await ElMessageBox.confirm(
'确定要删除品牌logo吗删除后将隐藏logo区域。',
'确认删除',
{ confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' }
)
deletingBrandLogo.value = true
const username = getUsernameFromToken()
if (!username) return ElMessage.warning('请先登录')
await splashApi.deleteBrandLogo(username)
// 立即清空本地状态
brandLogoUrl.value = ''
ElMessage.success('品牌logo已删除')
// 立即触发App.vue清空logo
window.dispatchEvent(new CustomEvent('brandLogoChanged', { detail: '' }))
} catch (error: any) {
if (error !== 'cancel') ElMessage.error(error?.message || '删除失败')
} finally {
deletingBrandLogo.value = false
}
}
onMounted(() => { onMounted(() => {
loadAllSettings() loadAllSettings()
loadLogDates() loadLogDates()
loadCurrentVersion() loadCurrentVersion()
loadSplashImage() loadSplashImage()
loadBrandLogo()
}) })
</script> </script>
@@ -480,12 +578,18 @@ onMounted(() => {
<span class="sidebar-icon">🚀</span> <span class="sidebar-icon">🚀</span>
<span class="sidebar-text">启动</span> <span class="sidebar-text">启动</span>
</div> </div>
<div <div
:class="['sidebar-item', { active: activeTab === 'splash' }]" :class="['sidebar-item', { active: activeTab === 'splash' }]"
@click="scrollToSection('splash')"> @click="scrollToSection('splash')">
<span class="sidebar-icon">🖼</span> <span class="sidebar-icon">🖼</span>
<span class="sidebar-text">开屏图片</span> <span class="sidebar-text">开屏图片</span>
</div> </div>
<div
:class="['sidebar-item', { active: activeTab === 'brand' }]"
@click="scrollToSection('brand')">
<span class="sidebar-icon">🏷</span>
<span class="sidebar-text">品牌logo</span>
</div>
<div <div
:class="['sidebar-item', { active: activeTab === 'feedback' }]" :class="['sidebar-item', { active: activeTab === 'feedback' }]"
@click="scrollToSection('feedback')"> @click="scrollToSection('feedback')">
@@ -624,48 +728,67 @@ onMounted(() => {
<!-- 开屏图片设置 --> <!-- 开屏图片设置 -->
<div id="section-splash" class="setting-section" @mouseenter="activeTab = 'splash'"> <div id="section-splash" class="setting-section" @mouseenter="activeTab = 'splash'">
<div class="section-title">开屏图片</div> <div class="section-title-row">
<div class="section-subtitle-text">自定义应用启动时的开屏图片</div> <div class="section-title">开屏图片</div>
<img src="/icon/vipExclusive.png" alt="VIP专享" class="vip-exclusive-logo" />
</div>
<div class="section-subtitle-text">支持.jpg .png格式最佳显示尺寸/比例 1200*736</div>
<div class="setting-item"> <div class="setting-item">
<div class="splash-preview-container" v-if="splashImageUrl"> <div class="image-preview-wrapper" v-if="splashImageUrl">
<img :src="splashImageUrl" alt="开屏图片预览" class="splash-preview-image" /> <img :src="splashImageUrl" alt="开屏图片预览" class="preview-image" />
<div class="image-buttons">
<input ref="fileInputRef" type="file" accept="image/*" @change="handleSplashImageUpload" style="display: none;" />
<button class="img-btn" :disabled="uploadingSplashImage" @click="triggerFileSelect">
{{ uploadingSplashImage ? '上传中' : '更换' }}
</button>
<button class="img-btn" :disabled="deletingSplashImage" @click="handleDeleteSplashImage">
{{ deletingSplashImage ? '删除中' : '删除' }}
</button>
</div>
</div> </div>
<div class="splash-placeholder" v-else> <div class="splash-placeholder" v-else>
<span class="placeholder-icon">🖼</span> <span class="placeholder-icon">🖼</span>
<span class="placeholder-text">未设置自定义开屏图片</span> <span class="placeholder-text">未设置自定义开屏图片</span>
<input ref="fileInputRef" type="file" accept="image/*" @change="handleSplashImageUpload" style="display: none;" />
<el-button type="primary" size="small" style="margin-top: 8px;" :loading="uploadingSplashImage" @click="triggerFileSelect">
{{ uploadingSplashImage ? '上传中' : '选择图片' }}
</el-button>
</div> </div>
</div>
<div class="setting-item" style="margin-top: 10px;"> </div>
<div class="splash-actions"> </div>
<input
ref="fileInputRef" <!-- 品牌logo页面 -->
type="file" <div id="section-brand" class="setting-section" @mouseenter="activeTab = 'brand'">
accept="image/*" <div class="section-title-row">
@change="handleSplashImageUpload" <div class="section-title">品牌logo</div>
style="display: none;" <img src="/icon/vipExclusive.png" alt="VIP专享" class="vip-exclusive-logo" />
/> </div>
<el-button <div class="section-subtitle-text"> 支持 JPGPNG 格式最佳显示尺寸/比例 1200*736</div>
type="primary"
size="small" <div class="setting-item">
:loading="uploadingSplashImage" <div class="image-preview-wrapper" v-if="brandLogoUrl">
:disabled="uploadingSplashImage" <img :src="brandLogoUrl" alt="品牌logo" class="preview-image" />
@click="triggerFileSelect"> <div class="image-buttons">
{{ uploadingSplashImage ? '上传中...' : '选择图片' }} <input ref="brandLogoInputRef" type="file" accept="image/*" @change="handleBrandLogoUpload" style="display: none;" />
</el-button> <button class="img-btn" :disabled="uploadingBrandLogo" @click="brandLogoInputRef?.click()">
<el-button {{ uploadingBrandLogo ? '上传中' : '更换' }}
v-if="splashImageUrl" </button>
size="small" <button class="img-btn" :disabled="deletingBrandLogo" @click="handleDeleteBrandLogo">
:loading="deletingSplashImage" {{ deletingBrandLogo ? '删除中' : '删除' }}
:disabled="deletingSplashImage" </button>
@click="handleDeleteSplashImage"> </div>
{{ deletingSplashImage ? '删除中...' : '删除' }} </div>
<div class="splash-placeholder" v-else>
<span class="placeholder-icon">🏷</span>
<span class="placeholder-text">未设置品牌logo</span>
<input ref="brandLogoInputRef" type="file" accept="image/*" @change="handleBrandLogoUpload" style="display: none;" />
<el-button type="primary" size="small" style="margin-top: 8px;" :loading="uploadingBrandLogo" @click="brandLogoInputRef?.click()">
{{ uploadingBrandLogo ? '上传中' : '选择图片' }}
</el-button> </el-button>
</div> </div>
<div class="setting-desc" style="margin-top: 6px;">
支持 JPGPNG 格式大小不超过 5MB建议尺寸 1200x675
</div>
</div> </div>
</div> </div>
@@ -737,6 +860,9 @@ onMounted(() => {
</div> </div>
</div> </div>
</Transition> </Transition>
<!-- VIP过期提示弹窗 -->
<TrialExpiredDialog v-model="showTrialExpiredDialog" :expired-type="trialExpiredType" />
</Teleport> </Teleport>
</template> </template>
@@ -940,9 +1066,21 @@ onMounted(() => {
font-weight: 600; font-weight: 600;
color: #1F2937; color: #1F2937;
text-align: left; text-align: left;
}
.section-title-row {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 8px; margin-bottom: 8px;
} }
.vip-exclusive-logo {
width: 60px;
height: auto;
vertical-align: middle;
}
.section-header .section-title { .section-header .section-title {
margin-bottom: 0; margin-bottom: 0;
} }
@@ -1100,6 +1238,53 @@ onMounted(() => {
margin-top: 8px; margin-top: 8px;
} }
/* 图片预览容器 */
.image-preview-wrapper {
position: relative;
width: 75%;
}
.preview-image {
width: 100%;
height: 100%;
object-fit: contain;
}
.image-buttons {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
display: flex;
gap: 8px;
opacity: 0;
transition: opacity 0.2s ease;
}
.image-preview-wrapper:hover .image-buttons {
opacity: 1;
}
.img-btn {
padding: 6px 14px;
background: rgba(0, 0, 0, 0.6);
border: none;
border-radius: 4px;
color: #fff;
font-size: 12px;
cursor: pointer;
transition: background 0.2s ease;
}
.img-btn:hover:not(:disabled) {
background: rgba(0, 0, 0, 0.8);
}
.img-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.feedback-form :deep(.el-textarea__inner) { .feedback-form :deep(.el-textarea__inner) {
border-color: #E5E6EB; border-color: #E5E6EB;
border-radius: 6px; border-radius: 6px;
@@ -1312,22 +1497,7 @@ onMounted(() => {
color: #1F2937; color: #1F2937;
} }
/* 开屏图片样式 */ /* 占位符 */
.splash-preview-container {
width: 100%;
height: 180px;
border-radius: 6px;
overflow: hidden;
border: 1px solid #E5E6EB;
background: #F8F9FA;
}
.splash-preview-image {
width: 100%;
height: 100%;
object-fit: contain;
}
.splash-placeholder { .splash-placeholder {
width: 100%; width: 100%;
height: 180px; height: 180px;
@@ -1350,12 +1520,6 @@ onMounted(() => {
font-size: 13px; font-size: 13px;
color: #86909C; color: #86909C;
} }
.splash-actions {
display: flex;
align-items: center;
gap: 8px;
}
</style> </style>
<script lang="ts"> <script lang="ts">

View File

@@ -21,7 +21,7 @@ import java.time.LocalDateTime;
import java.util.*; import java.util.*;
import java.util.stream.Collectors; import java.util.stream.Collectors;
/** /**
* 商标检查控制器 - 极速版(浏览器内并发) * 商标检查控制器
*/ */
@RestController @RestController
@RequestMapping("/api/trademark") @RequestMapping("/api/trademark")
@@ -63,12 +63,9 @@ public class TrademarkController {
.filter(b -> b != null && !b.trim().isEmpty()) .filter(b -> b != null && !b.trim().isEmpty())
.map(String::trim) .map(String::trim)
.collect(Collectors.toList()); .collect(Collectors.toList());
long start = System.currentTimeMillis(); long start = System.currentTimeMillis();
// 1. 先从全局缓存获取 // 1. 先从全局缓存获取
Map<String, Boolean> cached = cacheService.getCached(list); Map<String, Boolean> cached = cacheService.getCached(list);
// 2. 找出缓存未命中的品牌 // 2. 找出缓存未命中的品牌
List<String> toQuery = list.stream() List<String> toQuery = list.stream()
.filter(b -> !cached.containsKey(b)) .filter(b -> !cached.containsKey(b))

View File

@@ -28,7 +28,7 @@ public class TrademarkCheckUtil {
Thread.sleep(6000); Thread.sleep(6000);
return; // 成功则返回 return; // 成功则返回
} catch (Exception e) { } catch (Exception e) {
System.err.println("初始化失败(尝试" + (i+1) + "/3: " + e.getMessage()); System.err.println("初始化失败(尝试" + (i+1) + "/5: " + e.getMessage());
if (driver != null) { if (driver != null) {
try { driver.quit(); } catch (Exception ex) {} try { driver.quit(); } catch (Exception ex) {}
driver = null; driver = null;
@@ -40,106 +40,99 @@ public class TrademarkCheckUtil {
} }
public synchronized Map<String, Boolean> batchCheck(List<String> brands, Map<String, Boolean> alreadyQueried) { public synchronized Map<String, Boolean> batchCheck(List<String> brands, Map<String, Boolean> alreadyQueried) {
ensureInit(); Map<String, Boolean> resultMap = new HashMap<>();
int maxRetries = 5;
// 构建批量查询脚本(带错误诊断)
String script = """ for (String brand : brands) {
const brands = arguments[0]; int retryCount = 0;
const callback = arguments[arguments.length - 1]; boolean success = false;
Promise.all(brands.map(brand => while (retryCount < maxRetries && !success) {
fetch('https://tmsearch.uspto.gov/prod-stage-v1-0-0/tmsearch', { try {
method: 'POST', ensureInit();
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ String script = "const brands = ['" + brand.replace("'", "\\'") + "'];\n" +
query: {bool: {must: [{bool: {should: [ "const callback = arguments[arguments.length - 1];\n" +
{match_phrase: {WM: {query: brand, boost: 5}}}, "Promise.all(brands.map(b => \n" +
{match: {WM: {query: brand, boost: 2}}}, " fetch('https://tmsearch.uspto.gov/prod-stage-v1-0-0/tmsearch', {\n" +
{match_phrase: {PM: {query: brand, boost: 2}}} " method: 'POST',\n" +
]}}]}}, " headers: {'Content-Type': 'application/json'},\n" +
size: 1, _source: ['alive'] " body: JSON.stringify({\n" +
}) " query: {bool: {must: [{bool: {should: [\n" +
}) " {match_phrase: {WM: {query: b, boost: 5}}},\n" +
.then(r => { " {match: {WM: {query: b, boost: 2}}},\n" +
if (!r.ok) { " {match_phrase: {PM: {query: b, boost: 2}}}\n" +
return {brand, alive: false, error: `HTTP ${r.status}: ${r.statusText}`}; " ]}}]}},\n" +
} " size: 1, _source: ['alive']\n" +
return r.json().then(d => ({ " })\n" +
brand, " })\n" +
alive: d?.hits?.hits?.[0]?.source?.alive || false, " .then(r => {\n" +
error: null " if (!r.ok) {\n" +
})); " return {brand: b, alive: false, error: 'HTTP ' + r.status + ': ' + r.statusText};\n" +
}) " }\n" +
.catch(e => ({ " return r.json().then(d => ({\n" +
brand, " brand: b,\n" +
alive: false, " alive: d?.hits?.hits?.[0]?.source?.alive || false,\n" +
error: e.name + ': ' + e.message " error: null\n" +
})) " }));\n" +
)).then(callback); " })\n" +
"""; " .catch(e => ({\n" +
" brand: b,\n" +
try { " alive: false,\n" +
@SuppressWarnings("unchecked") " error: e.name + ': ' + e.message\n" +
List<Map<String, Object>> results = (List<Map<String, Object>>) " }))\n" +
((JavascriptExecutor) driver).executeAsyncScript(script, brands); ")).then(callback);";
// 检测是否有网络错误包括403、Failed to fetch等 @SuppressWarnings("unchecked")
boolean hasNetworkError = results.stream() List<Map<String, Object>> results = (List<Map<String, Object>>)
.anyMatch(item -> { ((JavascriptExecutor) driver).executeAsyncScript(script);
Map<String, Object> item = results.get(0);
String error = (String) item.get("error"); String error = (String) item.get("error");
return error != null && (
error.contains("HTTP 403") || if (error != null && (
error.contains("HTTP 403") ||
error.contains("Failed to fetch") || error.contains("Failed to fetch") ||
error.contains("NetworkError") || error.contains("NetworkError") ||
error.contains("TypeError") error.contains("TypeError") ||
); error.contains("script timeout"))) {
});
System.err.println(brand + " 查询失败(" + (retryCount + 1) + "/" + maxRetries + "): " + error + ",切换代理...");
// 如果有网络错误,切换代理并重试
if (hasNetworkError) { // 切换代理
System.err.println("检测到网络错误,切换代理并重试..."); try { driver.quit(); } catch (Exception e) {}
driver = null;
// 切换代理前保存已查询的品牌 retryCount++;
if (alreadyQueried != null && !alreadyQueried.isEmpty()) { continue;
try {
cacheService.saveResults(alreadyQueried);
System.out.println("代理切换,已保存 " + alreadyQueried.size() + " 个品牌到缓存");
} catch (Exception e) {
System.err.println("保存缓存失败: " + e.getMessage());
} }
}
// 成功或非网络错误
try { driver.quit(); } catch (Exception e) {} if (error == null) {
driver = null; Boolean alive = (Boolean) item.get("alive");
ensureInit(); resultMap.put(brand, alive);
System.out.println(brand + " -> " + (alive ? "✓ 已注册" : "✗ 未注册"));
// 重新执行查询 success = true;
@SuppressWarnings("unchecked") } else {
List<Map<String, Object>> retryResults = (List<Map<String, Object>>) System.err.println(brand + " -> [查询失败: " + error + "]");
((JavascriptExecutor) driver).executeAsyncScript(script, brands); resultMap.put(brand, false); // 失败也记录为未注册
results = retryResults; success = true;
} }
Map<String, Boolean> resultMap = new HashMap<>(); } catch (Exception e) {
for (Map<String, Object> item : results) { System.err.println(brand + " 查询异常(" + (retryCount + 1) + "/" + maxRetries + "): " + e.getMessage());
String brand = (String) item.get("brand"); try { driver.quit(); } catch (Exception ex) {}
Boolean alive = (Boolean) item.get("alive"); driver = null;
String error = (String) item.get("error"); retryCount++;
if (error != null) {
// 查询失败,不放入结果,只打印错误
System.err.println(brand + " -> [查询失败: " + error + "]");
} else {
// 查询成功,放入结果
resultMap.put(brand, alive);
System.out.println(brand + " -> " + (alive ? "✓ 已注册" : "✗ 未注册"));
} }
} }
return resultMap;
} catch (Exception e) { if (!success) {
System.err.println("批量查询失败: " + e.getMessage()); System.err.println(brand + " -> [查询失败: 已重试" + maxRetries + "次]");
return new HashMap<>(); resultMap.put(brand, false); // 失败也记录为未注册
}
} }
return resultMap;
} }
public synchronized void closeDriver() { public synchronized void closeDriver() {

View File

@@ -76,6 +76,7 @@ public class ClientAccountController extends BaseController {
private Auth auth; private Auth auth;
private static final String SPLASH_IMAGE_CACHE_KEY = "splash_image:"; private static final String SPLASH_IMAGE_CACHE_KEY = "splash_image:";
private static final String BRAND_LOGO_CACHE_KEY = "brand_logo:";
private AjaxResult checkDeviceLimit(Long accountId, String deviceId, int deviceLimit) { private AjaxResult checkDeviceLimit(Long accountId, String deviceId, int deviceLimit) {
int activeDeviceCount = accountDeviceMapper.countActiveDevicesByAccountId(accountId); int activeDeviceCount = accountDeviceMapper.countActiveDevicesByAccountId(accountId);
@@ -180,6 +181,11 @@ public class ClientAccountController extends BaseController {
if (StringUtils.isNotEmpty(account.getSplashImage())) { if (StringUtils.isNotEmpty(account.getSplashImage())) {
redisCache.setCacheObject(SPLASH_IMAGE_CACHE_KEY + username, account.getSplashImage()); redisCache.setCacheObject(SPLASH_IMAGE_CACHE_KEY + username, account.getSplashImage());
} }
// 更新品牌logo缓存到 Redis
if (StringUtils.isNotEmpty(account.getBrandLogo())) {
redisCache.setCacheObject(BRAND_LOGO_CACHE_KEY + username, account.getBrandLogo());
}
String token = Jwts.builder() String token = Jwts.builder()
.setHeaderParam("kid", jwtRsaKeyService.getKeyId()) .setHeaderParam("kid", jwtRsaKeyService.getKeyId())
@@ -402,7 +408,7 @@ public class ClientAccountController extends BaseController {
try { try {
ClientAccount account = clientAccountService.selectClientAccountByUsername(username); ClientAccount account = clientAccountService.selectClientAccountByUsername(username);
if (account == null) return AjaxResult.error("账号不存在"); if (account == null) return AjaxResult.error("账号不存在");
account.setSplashImage(null); account.setSplashImage(null);
clientAccountService.updateClientAccount(account); clientAccountService.updateClientAccount(account);
redisCache.deleteObject(SPLASH_IMAGE_CACHE_KEY + username); redisCache.deleteObject(SPLASH_IMAGE_CACHE_KEY + username);
@@ -413,4 +419,66 @@ public class ClientAccountController extends BaseController {
} }
} }
/**
* 上传品牌logo
*/
@PostMapping("/brand-logo/upload")
public AjaxResult uploadBrandLogo(@RequestParam("file") MultipartFile file, @RequestParam("username") String username) {
try {
ClientAccount account = clientAccountService.selectClientAccountByUsername(username);
if (account == null) return AjaxResult.error("账号不存在");
if (!file.getContentType().startsWith("image/")) return AjaxResult.error("只支持图片文件");
if (file.getSize() > 5 * 1024 * 1024) return AjaxResult.error("图片大小不能超过5MB");
String fileName = "brand-logo/" + DateUtil.format(new Date(), "yyyy/MM/") + IdUtil.simpleUUID() + "." + FileUtil.extName(file.getOriginalFilename());
try (InputStream is = file.getInputStream()) {
Response res = uploadManager.put(is, fileName, auth.uploadToken(qiniu.getBucket()), null, "");
if (!res.isOK()) return AjaxResult.error("上传失败");
}
String url = qiniu.getResourcesUrl() + fileName;
account.setBrandLogo(url);
clientAccountService.updateClientAccount(account);
redisCache.setCacheObject(BRAND_LOGO_CACHE_KEY + username, url);
return AjaxResult.success().put("url", url).put("fileName", fileName);
} catch (Exception e) {
return AjaxResult.error("上传失败");
}
}
/**
* 获取品牌logo
*/
@GetMapping("/brand-logo")
public AjaxResult getBrandLogo(@RequestParam("username") String username) {
String url = redisCache.getCacheObject(BRAND_LOGO_CACHE_KEY + username);
if (StringUtils.isEmpty(url)) {
ClientAccount account = clientAccountService.selectClientAccountByUsername(username);
if (account != null && StringUtils.isNotEmpty(account.getBrandLogo())) {
url = account.getBrandLogo();
redisCache.setCacheObject(BRAND_LOGO_CACHE_KEY + username, url);
}
}
return AjaxResult.success(Map.of("url", url != null ? url : ""));
}
/**
* 删除品牌logo
*/
@PostMapping("/brand-logo/delete")
public AjaxResult deleteBrandLogo(@RequestParam("username") String username) {
try {
ClientAccount account = clientAccountService.selectClientAccountByUsername(username);
if (account == null) return AjaxResult.error("账号不存在");
account.setBrandLogo(null);
clientAccountService.updateClientAccount(account);
redisCache.deleteObject(BRAND_LOGO_CACHE_KEY + username);
return AjaxResult.success("品牌logo已删除");
} catch (Exception e) {
e.printStackTrace();
return AjaxResult.error("删除失败: " + e.getMessage());
}
}
} }

View File

@@ -58,6 +58,9 @@ public class ClientAccount extends BaseEntity
/** 开屏图片URL */ /** 开屏图片URL */
private String splashImage; private String splashImage;
/** 品牌logo URL */
private String brandLogo;
public void setId(Long id) public void setId(Long id)
{ {
this.id = id; this.id = id;
@@ -174,4 +177,14 @@ public class ClientAccount extends BaseEntity
{ {
return splashImage; return splashImage;
} }
public void setBrandLogo(String brandLogo)
{
this.brandLogo = brandLogo;
}
public String getBrandLogo()
{
return brandLogo;
}
} }

View File

@@ -17,6 +17,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<result property="deviceLimit" column="device_limit" /> <result property="deviceLimit" column="device_limit" />
<result property="accountType" column="account_type" /> <result property="accountType" column="account_type" />
<result property="splashImage" column="splash_image" /> <result property="splashImage" column="splash_image" />
<result property="brandLogo" column="brand_logo" />
<result property="createBy" column="create_by" /> <result property="createBy" column="create_by" />
<result property="createTime" column="create_time" /> <result property="createTime" column="create_time" />
<result property="updateBy" column="update_by" /> <result property="updateBy" column="update_by" />
@@ -25,7 +26,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<sql id="selectClientAccountVo"> <sql id="selectClientAccountVo">
select id, account_name, username, password, status, expire_time, select id, account_name, username, password, status, expire_time,
allowed_ip_range, remark, permissions, device_limit, account_type, splash_image, create_by, create_time, update_by, update_time allowed_ip_range, remark, permissions, device_limit, account_type, splash_image, brand_logo, create_by, create_time, update_by, update_time
from client_account from client_account
</sql> </sql>
@@ -63,6 +64,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="deviceLimit != null">device_limit,</if> <if test="deviceLimit != null">device_limit,</if>
<if test="accountType != null">account_type,</if> <if test="accountType != null">account_type,</if>
<if test="splashImage != null">splash_image,</if> <if test="splashImage != null">splash_image,</if>
<if test="brandLogo != null">brand_logo,</if>
<if test="createBy != null">create_by,</if> <if test="createBy != null">create_by,</if>
create_time create_time
</trim> </trim>
@@ -78,6 +80,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="deviceLimit != null">#{deviceLimit},</if> <if test="deviceLimit != null">#{deviceLimit},</if>
<if test="accountType != null">#{accountType},</if> <if test="accountType != null">#{accountType},</if>
<if test="splashImage != null">#{splashImage},</if> <if test="splashImage != null">#{splashImage},</if>
<if test="brandLogo != null">#{brandLogo},</if>
<if test="createBy != null">#{createBy},</if> <if test="createBy != null">#{createBy},</if>
sysdate() sysdate()
</trim> </trim>
@@ -97,6 +100,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="deviceLimit != null">device_limit = #{deviceLimit},</if> <if test="deviceLimit != null">device_limit = #{deviceLimit},</if>
<if test="accountType != null">account_type = #{accountType},</if> <if test="accountType != null">account_type = #{accountType},</if>
<if test="splashImage != null">splash_image = #{splashImage},</if> <if test="splashImage != null">splash_image = #{splashImage},</if>
<if test="brandLogo != null">brand_logo = #{brandLogo},</if>
<if test="updateBy != null">update_by = #{updateBy},</if> <if test="updateBy != null">update_by = #{updateBy},</if>
update_time = sysdate() update_time = sysdate()
</trim> </trim>