feat(genmai): 集成跟卖精灵账号管理系统跟
- 新增卖精灵账号管理功能,支持多账号切换 - 实现账号增删改查接口与前端交互逻辑 -优化打开跟卖精灵流程,增加账号选择界面 - 添加账号权限限制与订阅升级提醒 - 完善后端账号实体类及数据库映射 - 更新系统控制器以支持指定账号启动功能- 调整HTTP请求路径适配新工具模块路由- 升级客户端版本至2.5.3并优化代码结构
This commit is contained in:
@@ -166,7 +166,6 @@ function startSpringBoot() {
|
||||
`--logging.config=file:${logbackConfigPath}`,
|
||||
`--logging.file.path=${logDir}`
|
||||
];
|
||||
|
||||
springProcess = spawn(javaPath, springArgs, {
|
||||
cwd: dataDir,
|
||||
detached: false,
|
||||
@@ -224,7 +223,7 @@ function startSpringBoot() {
|
||||
app.quit();
|
||||
}
|
||||
}
|
||||
startSpringBoot();
|
||||
startSpringBoot();
|
||||
function stopSpringBoot() {
|
||||
if (!springProcess) return;
|
||||
try {
|
||||
|
||||
39
electron-vue-template/src/renderer/api/genmai.ts
Normal file
39
electron-vue-template/src/renderer/api/genmai.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { http } from './http'
|
||||
|
||||
export interface GenmaiAccount {
|
||||
id?: number
|
||||
name?: string
|
||||
username: string
|
||||
password: string
|
||||
clientUsername?: string
|
||||
token?: string
|
||||
tokenExpireAt?: string
|
||||
status?: number
|
||||
remark?: string
|
||||
createTime?: string
|
||||
updateTime?: string
|
||||
}
|
||||
|
||||
export const genmaiApi = {
|
||||
getAccounts(name?: string) {
|
||||
return http.get('/tool/genmai/accounts', name ? { name } : undefined)
|
||||
},
|
||||
|
||||
getAccountLimit(name?: string) {
|
||||
return http.get('/tool/genmai/account-limit', name ? { name } : undefined)
|
||||
},
|
||||
|
||||
saveAccount(body: GenmaiAccount, name?: string) {
|
||||
const url = name ? `/tool/genmai/accounts?name=${encodeURIComponent(name)}` : '/tool/genmai/accounts'
|
||||
return http.post(url, body)
|
||||
},
|
||||
|
||||
removeAccount(id: number) {
|
||||
return http.delete(`/tool/genmai/accounts/${id}`)
|
||||
},
|
||||
|
||||
validateAndRefresh(id: number) {
|
||||
return http.post(`/tool/genmai/accounts/${id}/validate`)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ export const CONFIG = {
|
||||
} as const;
|
||||
|
||||
function resolveBase(path: string): string {
|
||||
if (path.startsWith('/monitor/') || path.startsWith('/system/') || path.startsWith('/tool/banma')) {
|
||||
if (path.startsWith('/monitor/') || path.startsWith('/system/') || path.startsWith('/tool/banma') || path.startsWith('/tool/genmai')) {
|
||||
return CONFIG.RUOYI_BASE;
|
||||
}
|
||||
return CONFIG.CLIENT_BASE;
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { http } from './http';
|
||||
|
||||
export const systemApi = {
|
||||
openGenmaiSpirit() {
|
||||
return http.post('/api/system/genmai/open');
|
||||
openGenmaiSpirit(accountId?: number | null) {
|
||||
const url = accountId ? `/api/system/genmai/open?accountId=${accountId}` : '/api/system/genmai/open';
|
||||
return http.post(url);
|
||||
},
|
||||
|
||||
clearCache() {
|
||||
|
||||
@@ -3,11 +3,13 @@ import { ref, computed, onMounted, defineAsyncComponent, inject } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { amazonApi } from '../../api/amazon'
|
||||
import { systemApi } from '../../api/system'
|
||||
import { genmaiApi, type GenmaiAccount } from '../../api/genmai'
|
||||
import { handlePlatformFileExport } from '../../utils/settings'
|
||||
import { getUsernameFromToken } from '../../utils/token'
|
||||
import { useFileDrop } from '../../composables/useFileDrop'
|
||||
|
||||
const TrialExpiredDialog = defineAsyncComponent(() => import('../common/TrialExpiredDialog.vue'))
|
||||
const AccountManager = defineAsyncComponent(() => import('../common/AccountManager.vue'))
|
||||
|
||||
const refreshVipStatus = inject<() => Promise<boolean>>('refreshVipStatus')
|
||||
|
||||
@@ -24,6 +26,9 @@ const progressVisible = ref(false) // 进度条是否显示(完成后仍保留
|
||||
const localProductData = ref<any[]>([]) // 本地产品数据
|
||||
const currentAsin = ref('') // 当前处理的ASIN
|
||||
const genmaiLoading = ref(false) // Genmai Spirit加载状态
|
||||
const genmaiAccounts = ref<GenmaiAccount[]>([]) // 跟卖精灵账号列表
|
||||
const selectedGenmaiAccountId = ref<number | null>(null) // 选中的跟卖精灵账号ID
|
||||
const currentTab = ref<'asin' | 'genmai'>('asin') // 当前选中的tab
|
||||
let abortController: AbortController | null = null // 请求取消控制器
|
||||
|
||||
// 分页配置
|
||||
@@ -35,6 +40,10 @@ const amazonUpload = ref<HTMLInputElement | null>(null)
|
||||
const showTrialExpiredDialog = ref(false)
|
||||
const trialExpiredType = ref<'device' | 'account' | 'both' | 'subscribe'>('account')
|
||||
|
||||
// 账号管理弹框
|
||||
const showAccountManager = ref(false)
|
||||
const accountManagerRef = ref<any>(null)
|
||||
|
||||
const vipStatus = inject<any>('vipStatus')
|
||||
|
||||
// 计算属性 - 当前页数据
|
||||
@@ -257,25 +266,25 @@ function stopFetch() {
|
||||
}
|
||||
|
||||
async function openGenmaiSpirit() {
|
||||
try {
|
||||
await ElMessageBox.confirm('打开跟卖精灵会关闭所有谷歌浏览器进程,是否继续?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
genmaiLoading.value = true
|
||||
try {
|
||||
await systemApi.openGenmaiSpirit()
|
||||
showMessage('跟卖精灵已打开', 'success')
|
||||
} catch (error: any) {
|
||||
const errorMsg = error?.msg || error?.message || '打开跟卖精灵失败'
|
||||
showMessage(errorMsg, 'error')
|
||||
} finally {
|
||||
genmaiLoading.value = false
|
||||
}
|
||||
} catch {
|
||||
// 用户取消
|
||||
if (!genmaiAccounts.value.length) {
|
||||
showAccountManager.value = true
|
||||
return
|
||||
}
|
||||
genmaiLoading.value = true
|
||||
try {
|
||||
await systemApi.openGenmaiSpirit(selectedGenmaiAccountId.value)
|
||||
showMessage('跟卖精灵已打开', 'success')
|
||||
} finally {
|
||||
genmaiLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function loadGenmaiAccounts() {
|
||||
try {
|
||||
const res = await genmaiApi.getAccounts(getUsernameFromToken())
|
||||
genmaiAccounts.value = (res as any)?.data ?? []
|
||||
if (genmaiAccounts.value[0]) selectedGenmaiAccountId.value = genmaiAccounts.value[0].id
|
||||
} catch {}
|
||||
}
|
||||
|
||||
// 分页处理
|
||||
@@ -310,14 +319,12 @@ function downloadAmazonTemplate() {
|
||||
URL.revokeObjectURL(url)
|
||||
}
|
||||
|
||||
// 组件挂载时获取最新数据
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const resp = await amazonApi.getLatestProducts()
|
||||
localProductData.value = resp.data?.products || []
|
||||
} catch {
|
||||
// 静默处理初始化失败
|
||||
}
|
||||
} catch {}
|
||||
await loadGenmaiAccounts()
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -329,18 +336,79 @@ onMounted(async () => {
|
||||
<aside class="steps-sidebar">
|
||||
<!-- 顶部标签栏 -->
|
||||
<div class="top-tabs">
|
||||
<div class="tab-item active">
|
||||
<div :class="['tab-item', { active: currentTab === 'asin' }]" @click="currentTab = 'asin'">
|
||||
<span class="tab-icon">📦</span>
|
||||
<span class="tab-text">ASIN查询</span>
|
||||
</div>
|
||||
<div class="tab-item" :class="{ loading: genmaiLoading }" @click="!genmaiLoading && openGenmaiSpirit()">
|
||||
<span class="tab-icon" v-if="!genmaiLoading">🔍</span>
|
||||
<span class="tab-icon spinner-icon" v-else>⟳</span>
|
||||
<span class="tab-text">{{ genmaiLoading ? '启动中...' : '跟卖精灵' }}</span>
|
||||
<div :class="['tab-item', { active: currentTab === 'genmai' }]" @click="currentTab = 'genmai'">
|
||||
<span class="tab-icon">🔍</span>
|
||||
<span class="tab-text">跟卖精灵</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="steps-title">操作流程:</div>
|
||||
<div class="steps-flow">
|
||||
|
||||
<!-- 跟卖精灵流程区 -->
|
||||
<div v-if="currentTab === 'genmai'" class="steps-title">操作流程:</div>
|
||||
<div v-if="currentTab === 'genmai'" class="steps-flow">
|
||||
<!-- 1. 选择账号 -->
|
||||
<div class="flow-item">
|
||||
<div class="step-index">1</div>
|
||||
<div class="step-card">
|
||||
<div class="step-header"><div class="title">需启动的跟卖精灵账号</div></div>
|
||||
<div class="desc">请选择需启动的跟卖精灵账号</div>
|
||||
<template v-if="genmaiAccounts.length">
|
||||
<el-scrollbar :class="['account-list', { 'scroll-limit': genmaiAccounts.length > 3 }]">
|
||||
<div>
|
||||
<div
|
||||
v-for="acc in genmaiAccounts"
|
||||
:key="acc.id"
|
||||
:class="['acct-item', { selected: selectedGenmaiAccountId === acc.id }]"
|
||||
@click="selectedGenmaiAccountId = acc.id"
|
||||
>
|
||||
<span class="acct-row">
|
||||
<span :class="['status-dot', acc.status === 1 ? 'on' : 'off']"></span>
|
||||
<img class="avatar" src="/image/img_v3_02qd_052605f0-4be3-44db-9691-35ee5ff6201g.jpg" alt="avatar" />
|
||||
<span class="acct-text">{{ acc.name || acc.username }}</span>
|
||||
<span v-if="selectedGenmaiAccountId === acc.id" class="acct-check">✔️</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="placeholder-box">
|
||||
<img class="placeholder-img" src="/icon/image.png" alt="add-account" />
|
||||
<div class="placeholder-tip">请添加跟卖精灵账号</div>
|
||||
</div>
|
||||
</template>
|
||||
<div class="step-actions btn-row">
|
||||
<el-button size="small" class="w50" @click="showAccountManager = true">添加账号</el-button>
|
||||
<el-button size="small" class="w50 btn-blue" @click="showAccountManager = true">账号管理</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 2. 启动服务 -->
|
||||
<div class="flow-item">
|
||||
<div class="step-index">2</div>
|
||||
<div class="step-card">
|
||||
<div class="step-header"><div class="title">启动服务</div></div>
|
||||
<div class="desc">请确保设备已安装Chrome浏览器,否则服务将无法启动。打开跟卖精灵将关闭Chrome浏览器进程。</div>
|
||||
<div class="action-buttons column">
|
||||
<el-button
|
||||
size="small"
|
||||
class="w100 btn-blue"
|
||||
:disabled="genmaiLoading || !genmaiAccounts.length"
|
||||
@click="openGenmaiSpirit"
|
||||
>
|
||||
<span v-if="!genmaiLoading">启动服务</span>
|
||||
<span v-else>⟳ 启动中...</span>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="currentTab === 'asin'" class="steps-title">操作流程:</div>
|
||||
<div v-if="currentTab === 'asin'" class="steps-flow">
|
||||
<!-- 1 -->
|
||||
<div class="flow-item">
|
||||
<div class="step-index">1</div>
|
||||
@@ -423,6 +491,14 @@ onMounted(async () => {
|
||||
<!-- 试用期过期弹框 -->
|
||||
<TrialExpiredDialog v-model="showTrialExpiredDialog" :expired-type="trialExpiredType" />
|
||||
|
||||
<!-- 账号管理弹框 -->
|
||||
<AccountManager
|
||||
ref="accountManagerRef"
|
||||
v-model="showAccountManager"
|
||||
platform="genmai"
|
||||
@refresh="loadGenmaiAccounts"
|
||||
/>
|
||||
|
||||
<!-- 表格上方进度条 -->
|
||||
<div v-if="progressVisible" class="progress-head">
|
||||
<div class="progress-section">
|
||||
@@ -502,11 +578,26 @@ onMounted(async () => {
|
||||
.tab-item:last-child { border-radius: 0 3px 3px 0; border-left: none; }
|
||||
.tab-item:hover { background: #e8f4ff; color: #409EFF; }
|
||||
.tab-item.active { background: #1677FF; color: #fff; border-color: #1677FF; cursor: default; }
|
||||
.tab-item.loading { background: #e8f4ff; color: #409EFF; cursor: not-allowed; opacity: 0.8; }
|
||||
.tab-icon { font-size: 12px; }
|
||||
.spinner-icon { animation: spin 1s linear infinite; display: inline-block; }
|
||||
.tab-text { line-height: 1; }
|
||||
|
||||
/* 账号列表样式(和斑马一致) */
|
||||
.account-list { height: auto; }
|
||||
.scroll-limit { max-height: 140px; }
|
||||
.placeholder-box { display: flex; align-items: center; justify-content: center; flex-direction: column; height: 100px; background: #fff; border: 1px solid #ebeef5; border-radius: 4px; margin-bottom: 8px; }
|
||||
.placeholder-img { width: 80px; opacity: 0.9; }
|
||||
.placeholder-tip { margin-top: 6px; font-size: 12px; color: #a8abb2; }
|
||||
.avatar { width: 18px; height: 18px; border-radius: 50%; }
|
||||
.acct-row { display: grid; grid-template-columns: 6px 18px 1fr auto; align-items: center; gap: 6px; width: 100%; }
|
||||
.acct-text { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; font-size: 12px; }
|
||||
.status-dot { width: 6px; height: 6px; border-radius: 50%; display: inline-block; }
|
||||
.status-dot.on { background: #22c55e; }
|
||||
.status-dot.off { background: #f87171; }
|
||||
.acct-item { padding: 6px 8px; border-radius: 6px; cursor: pointer; margin-bottom: 4px; }
|
||||
.acct-item.selected { background: #eef5ff; box-shadow: inset 0 0 0 1px #d6e4ff; }
|
||||
.acct-check { display: inline-flex; align-items: center; justify-content: center; width: 16px; height: 16px; border-radius: 50%; background: transparent; color: #111; font-size: 12px; }
|
||||
.account-list::-webkit-scrollbar { width: 0; height: 0; }
|
||||
|
||||
.body-layout { display: flex; gap: 12px; flex: 1; overflow: hidden; }
|
||||
.steps-sidebar { width: 220px; background: #fff; border: 1px solid #ebeef5; border-radius: 6px; padding: 10px; height: 100%; flex-shrink: 0; }
|
||||
.steps-title { font-size: 14px; font-weight: 600; color: #303133; text-align: left; }
|
||||
@@ -536,6 +627,9 @@ onMounted(async () => {
|
||||
.file-chip .name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.single-input.left { display: flex; gap: 8px; }
|
||||
.action-buttons.column { display: flex; flex-direction: column; gap: 8px; }
|
||||
.step-actions { margin-top: 8px; display: flex; gap: 8px; }
|
||||
.btn-row { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
|
||||
.w50 { width: 100%; }
|
||||
.form-row { margin-bottom: 10px; }
|
||||
.label { display: block; font-size: 12px; color: #606266; margin-bottom: 6px; }
|
||||
|
||||
|
||||
@@ -1,32 +1,50 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, computed } from 'vue'
|
||||
import { ref, onMounted, computed, defineAsyncComponent, watch } from 'vue'
|
||||
import { zebraApi, type BanmaAccount } from '../../api/zebra'
|
||||
import { genmaiApi, type GenmaiAccount } from '../../api/genmai'
|
||||
import { ElMessageBox, ElMessage } from 'element-plus'
|
||||
import { getUsernameFromToken } from '../../utils/token'
|
||||
type PlatformKey = 'zebra' | 'shopee' | 'rakuten' | 'amazon'
|
||||
|
||||
const TrialExpiredDialog = defineAsyncComponent(() => import('./TrialExpiredDialog.vue'))
|
||||
|
||||
type PlatformKey = 'zebra' | 'shopee' | 'rakuten' | 'amazon' | 'genmai'
|
||||
const props = defineProps<{ modelValue: boolean; platform?: PlatformKey }>()
|
||||
const emit = defineEmits(['update:modelValue', 'add', 'refresh'])
|
||||
const emit = defineEmits(['update:modelValue', 'refresh'])
|
||||
const visible = computed({ get: () => props.modelValue, set: v => emit('update:modelValue', v) })
|
||||
const curPlatform = ref<PlatformKey>(props.platform || 'zebra')
|
||||
|
||||
// 监听弹框打开,同步平台并加载数据
|
||||
watch(() => props.modelValue, (newVal) => {
|
||||
if (newVal && props.platform) {
|
||||
curPlatform.value = props.platform
|
||||
load()
|
||||
}
|
||||
})
|
||||
|
||||
// 升级订阅弹框
|
||||
const showUpgradeDialog = ref(false)
|
||||
const PLATFORM_LABEL: Record<PlatformKey, string> = {
|
||||
zebra: '斑马 ERP',
|
||||
shopee: 'Shopee 虾皮购物',
|
||||
rakuten: 'Rakuten 乐天购物',
|
||||
amazon: 'Amazon 亚马逊'
|
||||
amazon: 'Amazon 亚马逊',
|
||||
genmai: '跟卖精灵'
|
||||
}
|
||||
|
||||
const accounts = ref<BanmaAccount[]>([])
|
||||
const accounts = ref<(BanmaAccount | GenmaiAccount)[]>([])
|
||||
const accountLimit = ref({ limit: 1, count: 0 })
|
||||
|
||||
// 添加账号对话框
|
||||
const accountDialogVisible = ref(false)
|
||||
const formUsername = ref('')
|
||||
const formPassword = ref('')
|
||||
|
||||
async function load() {
|
||||
const api = curPlatform.value === 'genmai' ? genmaiApi : zebraApi
|
||||
const username = getUsernameFromToken()
|
||||
const [res, limitRes] = await Promise.all([
|
||||
zebraApi.getAccounts(username),
|
||||
zebraApi.getAccountLimit(username)
|
||||
])
|
||||
const list = (res as any)?.data ?? res
|
||||
accounts.value = Array.isArray(list) ? list : []
|
||||
const limitData = (limitRes as any)?.data ?? limitRes
|
||||
accountLimit.value = { limit: limitData?.limit ?? 1, count: limitData?.count ?? 0 }
|
||||
const [res, limitRes] = await Promise.all([api.getAccounts(username), api.getAccountLimit(username)])
|
||||
accounts.value = (res as any)?.data ?? res
|
||||
accountLimit.value = (limitRes as any)?.data ?? limitRes
|
||||
}
|
||||
|
||||
// 暴露方法供父组件调用
|
||||
@@ -48,11 +66,39 @@ async function onDelete(a: any) {
|
||||
try {
|
||||
await ElMessageBox.confirm(`确定删除账号 "${a?.name || a?.username || id}" 吗?`, '提示', { type: 'warning' })
|
||||
} catch { return }
|
||||
await zebraApi.removeAccount(id)
|
||||
const api = curPlatform.value === 'genmai' ? genmaiApi : zebraApi
|
||||
await api.removeAccount(id)
|
||||
ElMessage({ message: '删除成功', type: 'success' })
|
||||
await load()
|
||||
emit('refresh') // 通知外层组件刷新账号列表
|
||||
}
|
||||
|
||||
async function handleAddAccount() {
|
||||
if (accountLimit.value.count >= accountLimit.value.limit) {
|
||||
ElMessage({ message: `账号数量已达上限`, type: 'warning' })
|
||||
return
|
||||
}
|
||||
formUsername.value = ''
|
||||
formPassword.value = ''
|
||||
accountDialogVisible.value = true
|
||||
}
|
||||
|
||||
async function submitAccount() {
|
||||
const api = curPlatform.value === 'genmai' ? genmaiApi : zebraApi
|
||||
try {
|
||||
await api.saveAccount({
|
||||
username: formUsername.value,
|
||||
password: formPassword.value,
|
||||
status: 1
|
||||
}, getUsernameFromToken())
|
||||
ElMessage({ message: '添加成功', type: 'success' })
|
||||
accountDialogVisible.value = false
|
||||
await load()
|
||||
emit('refresh')
|
||||
} catch (e: any) {
|
||||
ElMessage({ message: e.message || '添加失败', type: 'error' })
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
@@ -69,8 +115,9 @@ export default defineComponent({ name: 'AccountManager' })
|
||||
<div class="layout">
|
||||
<aside class="sider">
|
||||
<div class="sider-title">全账号管理</div>
|
||||
<div class="nav only-zebra">
|
||||
<div class="nav">
|
||||
<div :class="['nav-item', {active: curPlatform==='zebra'}]" @click="switchPlatform('zebra')">斑马 ERP</div>
|
||||
<div :class="['nav-item', {active: curPlatform==='genmai'}]" @click="switchPlatform('genmai')">跟卖精灵</div>
|
||||
</div>
|
||||
</aside>
|
||||
<section class="content">
|
||||
@@ -80,8 +127,8 @@ export default defineComponent({ name: 'AccountManager' })
|
||||
<div class="head-main">
|
||||
<div class="main-title">在线账号管理({{ accountLimit.count }}/{{ accountLimit.limit }})</div>
|
||||
<div class="main-sub">
|
||||
您当前订阅可同时托管{{ accountLimit.limit }}个斑马账号<br>
|
||||
<span v-if="accountLimit.limit < 3">如需扩增账号数量,请 <span class="upgrade">升级订阅</span>。</span>
|
||||
您当前订阅可同时托管{{ accountLimit.limit }}个{{ curPlatform === 'genmai' ? '跟卖精灵' : '斑马' }}账号<br>
|
||||
<span v-if="accountLimit.limit < 3">如需扩增账号数量,请 <span class="upgrade" @click="showUpgradeDialog = true">升级订阅</span>。</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -97,10 +144,32 @@ export default defineComponent({ name: 'AccountManager' })
|
||||
</div>
|
||||
</div>
|
||||
<div class="footer">
|
||||
<el-button type="primary" class="btn" @click="$emit('add')">添加账号</el-button>
|
||||
<el-button type="primary" class="btn" @click="handleAddAccount">添加账号</el-button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<!-- 添加账号对话框 -->
|
||||
<el-dialog v-model="accountDialogVisible" width="420px" class="add-account-dialog">
|
||||
<template #header>
|
||||
<div class="aad-header">
|
||||
<img class="aad-icon" src="/icon/image.png" alt="logo" />
|
||||
<div class="aad-title">添加{{ curPlatform === 'genmai' ? '跟卖精灵' : '斑马' }}账号</div>
|
||||
</div>
|
||||
</template>
|
||||
<div class="aad-row">
|
||||
<el-input v-model="formUsername" :placeholder="curPlatform === 'genmai' ? '请输入账号(nickname)' : '请输入账号'" />
|
||||
</div>
|
||||
<div class="aad-row">
|
||||
<el-input v-model="formPassword" placeholder="请输入密码" type="password" show-password />
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button type="primary" class="btn-blue" style="width: 100%" @click="submitAccount">添加</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 升级订阅弹框 -->
|
||||
<TrialExpiredDialog v-model="showUpgradeDialog" expired-type="subscribe" />
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
@@ -120,9 +189,19 @@ export default defineComponent({ name: 'AccountManager' })
|
||||
.head-main { text-align:center; }
|
||||
.main-title { font-size: 16px; font-weight: 600; color:#303133; margin-bottom: 4px; }
|
||||
.main-sub { color:#909399; font-size: 11px; line-height: 1.4; }
|
||||
.upgrade { color:#409EFF; cursor: pointer; }
|
||||
.upgrade { color:#409EFF; cursor: pointer; font-weight: 600; transition: all 0.2s ease; }
|
||||
.upgrade:hover { color:#0d5ed6; text-decoration: underline; }
|
||||
.list { border:1px solid #ebeef5; border-radius: 6px; background: #fff; flex: 0 0 auto; width: 100%; max-height: 160px; overflow-y: auto; }
|
||||
.list.compact { max-height: 48px; }
|
||||
|
||||
/* 添加账号对话框样式 */
|
||||
.add-account-dialog .aad-header { display:flex; flex-direction: column; align-items:center; gap:8px; padding-top: 8px; width: 100%; }
|
||||
.add-account-dialog .aad-icon { width: 120px; height: auto; }
|
||||
.add-account-dialog .aad-title { font-weight: 600; font-size: 18px; text-align: center; }
|
||||
.add-account-dialog .aad-row { margin-top: 12px; }
|
||||
:deep(.add-account-dialog .el-dialog__header) { text-align: center; padding-right: 0; display: block; }
|
||||
.btn-blue { background: #1677FF; border-color: #1677FF; color: #fff; }
|
||||
.btn-blue:hover { background: #0d5ed6; border-color: #0d5ed6; }
|
||||
.row { display:grid; grid-template-columns: 8px 1fr 120px 60px; gap: 8px; align-items:center; padding: 4px 8px; border-bottom: 1px solid #f5f5f5; height: 28px; }
|
||||
.row:last-child { border-bottom:none; }
|
||||
.row:hover { background:#fafafa; }
|
||||
|
||||
@@ -564,7 +564,7 @@ async function removeCurrentAccount() {
|
||||
<!-- 试用期过期弹框 -->
|
||||
<TrialExpiredDialog v-model="showTrialExpiredDialog" :expired-type="trialExpiredType" />
|
||||
|
||||
<AccountManager ref="accountManagerRef" v-model="managerVisible" platform="zebra" @add="openAddAccount" @refresh="loadAccounts" />
|
||||
<AccountManager ref="accountManagerRef" v-model="managerVisible" platform="zebra" @refresh="loadAccounts" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
</parent>
|
||||
<groupId>com.tashow.erp</groupId>
|
||||
<artifactId>erp_client_sb</artifactId>
|
||||
<version>2.5.2</version>
|
||||
<version>2.5.3</version>
|
||||
<name>erp_client_sb</name>
|
||||
<description>erp客户端</description>
|
||||
<properties>
|
||||
|
||||
@@ -83,9 +83,9 @@ public class SystemController {
|
||||
}
|
||||
|
||||
@PostMapping("/genmai/open")
|
||||
public JsonData openGenmaiWebsite() {
|
||||
public JsonData openGenmaiWebsite(@RequestParam(required = false) Long accountId) {
|
||||
try {
|
||||
genmaiService.openGenmaiWebsite();
|
||||
genmaiService.openGenmaiWebsite(accountId);
|
||||
return JsonData.buildSuccess("跟卖精灵已打开");
|
||||
} catch (Exception e) {
|
||||
logger.error("打开跟卖精灵失败", e);
|
||||
|
||||
@@ -97,6 +97,11 @@ public class AmazonScrapingServiceImpl implements AmazonScrapingService, PagePro
|
||||
// 第一步:清理1小时前的所有旧数据
|
||||
amazonProductRepository.deleteAllDataBefore(LocalDateTime.now().minusHours(1));
|
||||
|
||||
// 优化:批次内复用代理检测和工具实例
|
||||
RakutenProxyUtil proxyUtil = new RakutenProxyUtil();
|
||||
String sampleUrl = buildAmazonUrl(region, "B00000000");
|
||||
var proxyDownloader = proxyUtil.createProxyDownloader(proxyUtil.detectSystemProxy(sampleUrl));
|
||||
|
||||
// 第二步:处理每个ASIN
|
||||
Map<String, AmazonProductEntity> allProducts = new HashMap<>();
|
||||
for (String asin : asinList.stream().distinct().toList()) {
|
||||
@@ -113,9 +118,8 @@ public class AmazonScrapingServiceImpl implements AmazonScrapingService, PagePro
|
||||
} else {
|
||||
String url = buildAmazonUrl(region, cleanAsin);
|
||||
this.site = configureSiteForRegion(region);
|
||||
RakutenProxyUtil proxyUtil = new RakutenProxyUtil();
|
||||
synchronized (spiderLock) {
|
||||
activeSpider = Spider.create(this).addUrl(url).setDownloader(proxyUtil.createProxyDownloader(proxyUtil.detectSystemProxy(url))).thread(1);
|
||||
activeSpider = Spider.create(this).addUrl(url).setDownloader(proxyDownloader).thread(1);
|
||||
activeSpider.run();
|
||||
activeSpider = null;
|
||||
}
|
||||
@@ -152,7 +156,7 @@ public class AmazonScrapingServiceImpl implements AmazonScrapingService, PagePro
|
||||
boolean isUS = "US".equals(region);
|
||||
return Site.me()
|
||||
.setRetryTimes(3)
|
||||
.setSleepTime(3000 + random.nextInt(3000))
|
||||
.setSleepTime(2000 + random.nextInt(2000))
|
||||
.setTimeOut(20000)
|
||||
.setUserAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36")
|
||||
.addHeader("accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8")
|
||||
|
||||
@@ -3,93 +3,76 @@ import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.qiniu.util.UrlUtils;
|
||||
import org.openqa.selenium.JavascriptExecutor;
|
||||
import org.openqa.selenium.WebDriver;
|
||||
import org.openqa.selenium.chrome.ChromeDriver;
|
||||
import org.openqa.selenium.chrome.ChromeOptions;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.http.*;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Service
|
||||
public class GenmaiServiceImpl {
|
||||
@Value("${api.server.base-url}")
|
||||
private String serverApiUrl;
|
||||
@Value("${api.server.paths.getGenmaijlToken}")
|
||||
private String genmaijlToken;
|
||||
@Value("${api.server.paths.updateGenmaijlToken}")
|
||||
private String updateGenmaijlToken;
|
||||
private final RestTemplate restTemplate = new RestTemplate();
|
||||
ObjectMapper objectMapper = new ObjectMapper();
|
||||
public void openGenmaiWebsite() {
|
||||
try {
|
||||
// 先关闭所有Chrome进程
|
||||
Runtime.getRuntime().exec("taskkill /f /im chrome.exe");
|
||||
Thread.sleep(1000); // 等待进程关闭
|
||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
ChromeOptions options = new ChromeOptions();
|
||||
String username = System.getProperty("user.name", "user");
|
||||
String safeUsername;
|
||||
char firstChar = username.charAt(0);
|
||||
char flippedFirstChar = Character.isUpperCase(firstChar) ? Character.toLowerCase(firstChar) : Character.toUpperCase(firstChar);
|
||||
safeUsername = flippedFirstChar + username.substring(1);
|
||||
String chromeUserData = System.getProperty("user.home").replace(username, UrlUtils.urlEncode(safeUsername))
|
||||
+ "\\AppData\\Local\\Google\\Chrome\\User Data";
|
||||
options.addArguments("user-data-dir=" + chromeUserData.toLowerCase());
|
||||
options.addArguments("profile-directory=Default");
|
||||
public void openGenmaiWebsite(Long accountId) throws Exception {
|
||||
String token = getAndValidateToken(accountId);
|
||||
Runtime.getRuntime().exec("taskkill /f /im chrome.exe");
|
||||
Thread.sleep(1000);
|
||||
|
||||
WebDriver driver = new ChromeDriver(options);
|
||||
driver.get("https://www.genmaijl.com/#/profile");
|
||||
JavascriptExecutor js = (JavascriptExecutor) driver;
|
||||
js.executeScript(String.format("localStorage.setItem('token','%s')", checkTokenExpired()));
|
||||
driver.navigate().refresh();
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("打开跟卖精灵失败,请确保已安装Chrome浏览器", e);
|
||||
}
|
||||
ChromeOptions options = new ChromeOptions();
|
||||
String username = System.getProperty("user.name");
|
||||
char firstChar = username.charAt(0);
|
||||
char flipped = Character.isUpperCase(firstChar) ? Character.toLowerCase(firstChar) : Character.toUpperCase(firstChar);
|
||||
String chromeUserData = System.getProperty("user.home")
|
||||
.replace(username, UrlUtils.urlEncode(flipped + username.substring(1)))
|
||||
+ "\\AppData\\Local\\Google\\Chrome\\User Data";
|
||||
options.addArguments("user-data-dir=" + chromeUserData.toLowerCase(), "profile-directory=Default");
|
||||
|
||||
ChromeDriver driver = new ChromeDriver(options);
|
||||
driver.get("https://www.genmaijl.com/#/profile");
|
||||
((JavascriptExecutor) driver).executeScript("localStorage.setItem('token','" + token + "')");
|
||||
driver.navigate().refresh();
|
||||
}
|
||||
/**
|
||||
* 获取token验证token是否过期
|
||||
*/
|
||||
public String checkTokenExpired() {
|
||||
ResponseEntity<String> exchange = restTemplate.exchange(serverApiUrl+genmaijlToken, HttpMethod.GET, null, String.class);
|
||||
String token = exchange.getBody();
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.set("Token", token);
|
||||
HttpEntity<String> entity = new HttpEntity<>(null, headers);
|
||||
try {
|
||||
ResponseEntity<String> response = restTemplate.exchange("https://www.genmaijl.com/manage/user/balance", HttpMethod.GET, entity, String.class);
|
||||
String body = response.getBody();
|
||||
JsonNode root = objectMapper.readTree(body);
|
||||
return root.get("code").asInt() == 0 ? token : login();
|
||||
} catch (Exception e) {
|
||||
return login();
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private String getAndValidateToken(Long accountId) {
|
||||
Map<String, Object> resp = restTemplate.getForObject(serverApiUrl + "/tool/genmai/accounts", Map.class);
|
||||
List<Map<String, Object>> list = (List<Map<String, Object>>) resp.get("data");
|
||||
|
||||
Map<String, Object> account = accountId != null
|
||||
? list.stream().filter(m -> accountId.equals(((Number) m.get("id")).longValue())).findFirst().orElse(list.get(0))
|
||||
: list.get(0);
|
||||
String token = (String) account.get("token");
|
||||
Long id = ((Number) account.get("id")).longValue();
|
||||
// 验证token是否有效
|
||||
if (!validateToken(token)) {
|
||||
// token无效,调用服务器刷新
|
||||
restTemplate.postForObject(serverApiUrl + "/tool/genmai/accounts/" + id + "/validate", null, Map.class);
|
||||
// 重新获取token
|
||||
resp = restTemplate.getForObject(serverApiUrl + "/tool/genmai/accounts", Map.class);
|
||||
list = (List<Map<String, Object>>) resp.get("data");
|
||||
account = list.stream().filter(m -> id.equals(((Number) m.get("id")).longValue())).findFirst().orElse(null);
|
||||
token = (String) account.get("token");
|
||||
}
|
||||
return token;
|
||||
}
|
||||
/**
|
||||
* 登录
|
||||
*/
|
||||
public String login() {
|
||||
|
||||
private boolean validateToken(String token) {
|
||||
try {
|
||||
Map<String, String> requestBody = new HashMap<>();
|
||||
requestBody.put("nickname", "changzhu-4");
|
||||
requestBody.put("password", "123456QWe@");
|
||||
HttpEntity<Map<String, String>> entity = new HttpEntity<>(requestBody);
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.TEXT_PLAIN);
|
||||
ResponseEntity<String> response = restTemplate.exchange("https://www.genmaijl.com/manage/user/subLogin", HttpMethod.POST, entity, String.class);
|
||||
String body = response.getBody();
|
||||
JsonNode root = objectMapper.readTree(body);
|
||||
requestBody.clear();
|
||||
String token = root.get("result").asText();
|
||||
HttpEntity<String> updateEntity = new HttpEntity<>(token, headers);
|
||||
ResponseEntity<String> exchange = restTemplate.exchange(serverApiUrl + updateGenmaijlToken, HttpMethod.POST, updateEntity, String.class);
|
||||
System.out.println(exchange.getBody());
|
||||
return token;
|
||||
headers.set("Token", token);
|
||||
ResponseEntity<String> response = restTemplate.exchange(
|
||||
"https://www.genmaijl.com/manage/user/balance",
|
||||
HttpMethod.GET, new HttpEntity<>(headers), String.class);
|
||||
JsonNode root = objectMapper.readTree(response.getBody());
|
||||
return root.get("code").asInt() == 0;
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("跟卖精灵登录失败", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -53,8 +53,6 @@ api:
|
||||
data: "/monitor/client/api/data"
|
||||
alibaba1688: "/monitor/client/api/alibaba1688"
|
||||
version: "/system/version/check"
|
||||
getGenmaijlToken: "/getToken"
|
||||
updateGenmaijlToken: "/saveToken"
|
||||
# 项目信息配置
|
||||
project:
|
||||
version: @project.version@
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
package com.ruoyi.web.controller.tool;
|
||||
import java.util.Date;
|
||||
import java.util.Map;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import com.ruoyi.common.annotation.Anonymous;
|
||||
import com.ruoyi.common.core.domain.R;
|
||||
import com.ruoyi.system.domain.GenmaiAccount;
|
||||
import com.ruoyi.system.domain.ClientAccount;
|
||||
import com.ruoyi.system.mapper.GenmaiAccountMapper;
|
||||
import com.ruoyi.system.mapper.ClientAccountMapper;
|
||||
import com.ruoyi.system.service.IGenmaiAccountService;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/tool/genmai")
|
||||
@Anonymous
|
||||
public class GenmaiAccountController {
|
||||
|
||||
@Autowired
|
||||
private IGenmaiAccountService accountService;
|
||||
@Autowired
|
||||
private ClientAccountMapper clientAccountMapper;
|
||||
@Autowired
|
||||
private GenmaiAccountMapper genmaiAccountMapper;
|
||||
|
||||
@GetMapping("/accounts")
|
||||
public R<?> listAccounts(String name) {
|
||||
return R.ok(accountService.listSimple(name));
|
||||
}
|
||||
|
||||
@GetMapping("/account-limit")
|
||||
public R<?> getAccountLimit(String name) {
|
||||
ClientAccount client = clientAccountMapper.selectClientAccountByUsername(name);
|
||||
int limit = "paid".equals(client.getAccountType()) && new Date().before(client.getExpireTime()) ? 3 : 1;
|
||||
GenmaiAccount query = new GenmaiAccount();
|
||||
query.setClientUsername(name);
|
||||
int count = genmaiAccountMapper.selectList(query).size();
|
||||
return R.ok(Map.of("limit", limit, "count", count));
|
||||
}
|
||||
|
||||
@PostMapping("/accounts")
|
||||
public R<?> saveAccount(@RequestBody GenmaiAccount body, String name) {
|
||||
if (body.getId() == null) {
|
||||
String token = accountService.validateAndGetToken(body.getUsername(), body.getPassword());
|
||||
body.setToken(token);
|
||||
body.setTokenExpireAt(new Date(System.currentTimeMillis() + 30L * 24 * 60 * 60 * 1000));
|
||||
}
|
||||
return R.ok(Map.of("id", accountService.saveOrUpdate(body, name)));
|
||||
}
|
||||
|
||||
@DeleteMapping("/accounts/{id}")
|
||||
public R<?> remove(@PathVariable Long id) {
|
||||
accountService.remove(id);
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
@PostMapping("/accounts/{id}/validate")
|
||||
public R<?> validateAndRefresh(@PathVariable Long id) {
|
||||
return accountService.refreshTokenIfNeeded(id) ? R.ok() : R.fail("刷新失败");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
package com.ruoyi.system.domain;
|
||||
|
||||
import java.util.Date;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import com.ruoyi.common.core.domain.BaseEntity;
|
||||
|
||||
/**
|
||||
* 跟卖精灵账号实体(数据库持久化)
|
||||
*/
|
||||
public class GenmaiAccount extends BaseEntity {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private Long id;
|
||||
/** 显示名 */
|
||||
private String name;
|
||||
/** 登录用户名(nickname) */
|
||||
private String username;
|
||||
/** 登录密码(服务端刷新Token用,不对外返回) */
|
||||
private String password;
|
||||
/** 客户端账号用户名(关联client_account.username) */
|
||||
private String clientUsername;
|
||||
/** 访问 Token(跨设备共享) */
|
||||
private String token;
|
||||
/** Token 过期时间(可选) */
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date tokenExpireAt;
|
||||
/** 状态 1启用 0停用 */
|
||||
private Integer status;
|
||||
/** 备注 */
|
||||
private String remark;
|
||||
|
||||
public Long getId() { return id; }
|
||||
public void setId(Long id) { this.id = id; }
|
||||
public String getName() { return name; }
|
||||
public void setName(String name) { this.name = name; }
|
||||
public String getUsername() { return username; }
|
||||
public void setUsername(String username) { this.username = username; }
|
||||
public String getPassword() { return password; }
|
||||
public void setPassword(String password) { this.password = password; }
|
||||
public String getClientUsername() { return clientUsername; }
|
||||
public void setClientUsername(String clientUsername) { this.clientUsername = clientUsername; }
|
||||
public String getToken() { return token; }
|
||||
public void setToken(String token) { this.token = token; }
|
||||
public Date getTokenExpireAt() { return tokenExpireAt; }
|
||||
public void setTokenExpireAt(Date tokenExpireAt) { this.tokenExpireAt = tokenExpireAt; }
|
||||
public Integer getStatus() { return status; }
|
||||
public void setStatus(Integer status) { this.status = status; }
|
||||
@Override public String getRemark() { return remark; }
|
||||
@Override public void setRemark(String remark) { this.remark = remark; }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.ruoyi.system.mapper;
|
||||
|
||||
import java.util.List;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import com.ruoyi.system.domain.GenmaiAccount;
|
||||
|
||||
/**
|
||||
* 跟卖精灵账号 Mapper
|
||||
*/
|
||||
public interface GenmaiAccountMapper {
|
||||
GenmaiAccount selectById(Long id);
|
||||
List<GenmaiAccount> selectList(GenmaiAccount query);
|
||||
int insert(GenmaiAccount entity);
|
||||
int update(GenmaiAccount entity);
|
||||
int deleteById(Long id);
|
||||
int deleteByClientUsername(@Param("clientUsername") String clientUsername);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
package com.ruoyi.system.service;
|
||||
|
||||
import java.util.List;
|
||||
import com.ruoyi.system.domain.GenmaiAccount;
|
||||
|
||||
/**
|
||||
* 跟卖精灵账号 Service 接口
|
||||
*/
|
||||
public interface IGenmaiAccountService {
|
||||
/**
|
||||
* 获取所有账号(简化信息,密码置空)
|
||||
*/
|
||||
List<GenmaiAccount> listSimple();
|
||||
|
||||
/**
|
||||
* 获取指定客户端用户的账号列表
|
||||
*/
|
||||
List<GenmaiAccount> listSimple(String clientUsername);
|
||||
|
||||
/**
|
||||
* 保存或更新账号
|
||||
*/
|
||||
Long saveOrUpdate(GenmaiAccount entity);
|
||||
|
||||
/**
|
||||
* 保存或更新账号(带客户端用户名)
|
||||
*/
|
||||
Long saveOrUpdate(GenmaiAccount entity, String clientUsername);
|
||||
|
||||
/**
|
||||
* 删除账号
|
||||
*/
|
||||
void remove(Long id);
|
||||
|
||||
/**
|
||||
* 验证token是否有效(调用跟卖精灵接口)
|
||||
*/
|
||||
boolean validateToken(String token);
|
||||
|
||||
/**
|
||||
* 刷新token(仅在token失效时)
|
||||
*/
|
||||
boolean refreshTokenIfNeeded(Long id);
|
||||
|
||||
/**
|
||||
* 验证账号密码并获取Token(不保存到数据库)
|
||||
*/
|
||||
String validateAndGetToken(String username, String password);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
package com.ruoyi.system.service.impl;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.http.HttpEntity;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.ruoyi.system.domain.GenmaiAccount;
|
||||
import com.ruoyi.system.domain.ClientAccount;
|
||||
import com.ruoyi.system.mapper.GenmaiAccountMapper;
|
||||
import com.ruoyi.system.mapper.ClientAccountMapper;
|
||||
import com.ruoyi.system.service.IGenmaiAccountService;
|
||||
|
||||
@Service
|
||||
public class GenmaiAccountServiceImpl implements IGenmaiAccountService {
|
||||
|
||||
@Autowired
|
||||
private GenmaiAccountMapper mapper;
|
||||
@Autowired
|
||||
private ClientAccountMapper clientAccountMapper;
|
||||
private final RestTemplate restTemplate = new RestTemplate();
|
||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
private int getAccountLimit(String clientUsername) {
|
||||
ClientAccount client = clientAccountMapper.selectClientAccountByUsername(clientUsername);
|
||||
return "paid".equals(client.getAccountType()) && new Date().before(client.getExpireTime()) ? 3 : 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<GenmaiAccount> listSimple() {
|
||||
return listSimple(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<GenmaiAccount> listSimple(String clientUsername) {
|
||||
GenmaiAccount query = new GenmaiAccount();
|
||||
query.setClientUsername(clientUsername);
|
||||
List<GenmaiAccount> list = mapper.selectList(query);
|
||||
list.forEach(a -> a.setPassword(null));
|
||||
return list;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long saveOrUpdate(GenmaiAccount entity) {
|
||||
return saveOrUpdate(entity, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long saveOrUpdate(GenmaiAccount entity, String clientUsername) {
|
||||
entity.setClientUsername(clientUsername);
|
||||
|
||||
if (entity.getId() == null) {
|
||||
int count = mapper.selectList(entity).size();
|
||||
if (count >= getAccountLimit(clientUsername)) {
|
||||
throw new RuntimeException("账号数量已达上限");
|
||||
}
|
||||
mapper.insert(entity);
|
||||
} else {
|
||||
mapper.update(entity);
|
||||
}
|
||||
return entity.getId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void remove(Long id) {
|
||||
mapper.deleteById(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean validateToken(String token) {
|
||||
try {
|
||||
HttpEntity<String> entity = new HttpEntity<>(null, null);
|
||||
ResponseEntity<String> response = restTemplate.exchange(
|
||||
"https://www.genmaijl.com/manage/user/balance",
|
||||
HttpMethod.GET, entity, String.class);
|
||||
return objectMapper.readTree(response.getBody()).get("code").asInt() == 0;
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean refreshTokenIfNeeded(Long id) {
|
||||
GenmaiAccount account = mapper.selectById(id);
|
||||
if (validateToken(account.getToken())) return true;
|
||||
|
||||
String token = validateAndGetToken(account.getUsername(), account.getPassword());
|
||||
account.setToken(token);
|
||||
account.setTokenExpireAt(new Date(System.currentTimeMillis() + 30L * 24 * 60 * 60 * 1000));
|
||||
mapper.update(account);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String validateAndGetToken(String username, String password) {
|
||||
try {
|
||||
Map<String, String> body = new HashMap<>();
|
||||
body.put("nickname", username);
|
||||
body.put("password", password);
|
||||
ResponseEntity<String> response = restTemplate.exchange(
|
||||
"https://www.genmaijl.com/manage/user/subLogin",
|
||||
HttpMethod.POST, new HttpEntity<>(body), String.class);
|
||||
return objectMapper.readTree(response.getBody()).get("result").asText();
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
<?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.GenmaiAccountMapper">
|
||||
|
||||
<resultMap id="GenmaiAccountResult" type="com.ruoyi.system.domain.GenmaiAccount">
|
||||
<result property="id" column="id"/>
|
||||
<result property="name" column="name"/>
|
||||
<result property="username" column="username"/>
|
||||
<result property="password" column="password"/>
|
||||
<result property="clientUsername" column="client_username"/>
|
||||
<result property="token" column="token"/>
|
||||
<result property="tokenExpireAt" column="token_expire_at"/>
|
||||
<result property="status" column="status"/>
|
||||
<result property="remark" column="remark"/>
|
||||
<result property="createBy" column="create_by"/>
|
||||
<result property="createTime" column="create_time"/>
|
||||
<result property="updateBy" column="update_by"/>
|
||||
<result property="updateTime" column="update_time"/>
|
||||
</resultMap>
|
||||
|
||||
<sql id="Base_Column_List">
|
||||
id, name, username, password, client_username, token, token_expire_at, status, remark, create_by, create_time, update_by, update_time
|
||||
</sql>
|
||||
|
||||
<select id="selectById" parameterType="long" resultMap="GenmaiAccountResult">
|
||||
select <include refid="Base_Column_List"/> from genmai_account where id = #{id}
|
||||
</select>
|
||||
|
||||
<select id="selectList" parameterType="com.ruoyi.system.domain.GenmaiAccount" resultMap="GenmaiAccountResult">
|
||||
select <include refid="Base_Column_List"/> from genmai_account
|
||||
<where>
|
||||
<if test="clientUsername != null and clientUsername != ''"> and client_username = #{clientUsername}</if>
|
||||
<if test="name != null and name != ''"> and name like concat('%', #{name}, '%')</if>
|
||||
<if test="username != null and username != ''"> and username like concat('%', #{username}, '%')</if>
|
||||
<if test="status != null"> and status = #{status}</if>
|
||||
</where>
|
||||
order by create_time desc
|
||||
</select>
|
||||
|
||||
<insert id="insert" parameterType="com.ruoyi.system.domain.GenmaiAccount" useGeneratedKeys="true" keyProperty="id">
|
||||
insert into genmai_account
|
||||
<trim prefix="(" suffix=")" suffixOverrides=",">
|
||||
<if test="name != null">name,</if>
|
||||
<if test="username != null">username,</if>
|
||||
<if test="password != null">password,</if>
|
||||
<if test="clientUsername != null">client_username,</if>
|
||||
<if test="token != null">token,</if>
|
||||
<if test="tokenExpireAt != null">token_expire_at,</if>
|
||||
<if test="status != null">status,</if>
|
||||
<if test="remark != null">remark,</if>
|
||||
<if test="createBy != null">create_by,</if>
|
||||
create_time
|
||||
</trim>
|
||||
<trim prefix="values (" suffix=")" suffixOverrides=",">
|
||||
<if test="name != null">#{name},</if>
|
||||
<if test="username != null">#{username},</if>
|
||||
<if test="password != null">#{password},</if>
|
||||
<if test="clientUsername != null">#{clientUsername},</if>
|
||||
<if test="token != null">#{token},</if>
|
||||
<if test="tokenExpireAt != null">#{tokenExpireAt},</if>
|
||||
<if test="status != null">#{status},</if>
|
||||
<if test="remark != null">#{remark},</if>
|
||||
<if test="createBy != null">#{createBy},</if>
|
||||
sysdate()
|
||||
</trim>
|
||||
</insert>
|
||||
|
||||
<update id="update" parameterType="com.ruoyi.system.domain.GenmaiAccount">
|
||||
update genmai_account
|
||||
<trim prefix="set" suffixOverrides=",">
|
||||
<if test="name != null">name=#{name},</if>
|
||||
<if test="username != null">username=#{username},</if>
|
||||
<if test="password != null">password=#{password},</if>
|
||||
<if test="clientUsername != null">client_username=#{clientUsername},</if>
|
||||
<if test="token != null">token=#{token},</if>
|
||||
<if test="tokenExpireAt != null">token_expire_at=#{tokenExpireAt},</if>
|
||||
<if test="status != null">status=#{status},</if>
|
||||
<if test="remark != null">remark=#{remark},</if>
|
||||
<if test="updateBy != null">update_by=#{updateBy},</if>
|
||||
update_time = sysdate()
|
||||
</trim>
|
||||
where id = #{id}
|
||||
</update>
|
||||
|
||||
<delete id="deleteById" parameterType="long">
|
||||
delete from genmai_account where id = #{id}
|
||||
</delete>
|
||||
|
||||
<delete id="deleteByClientUsername">
|
||||
delete from genmai_account where client_username = #{clientUsername}
|
||||
</delete>
|
||||
|
||||
</mapper>
|
||||
|
||||
Reference in New Issue
Block a user