增加 cloudtentacles发货平台

This commit is contained in:
yml2213
2026-05-02 21:41:43 +08:00
parent dc32eef08b
commit 5b747c7460
20 changed files with 2698 additions and 9 deletions
@@ -4,17 +4,26 @@ import { computed, onMounted, ref } from 'vue'
import { showError, showSuccess } from '@/lib/feedback'
import {
fetchAdminAgisoShopConfigs,
fetchAdminCloudtentaclesSourceConfig,
fetchAdminKhhaoSourceConfig,
queryAdminKhhaoOrders,
saveAdminCloudtentaclesSourceConfig,
saveAdminAgisoShopConfigs,
saveAdminKhhaoSourceConfig,
sendAdminCloudtentaclesSmsCode,
syncAdminKhhaoOrders,
testAdminCloudtentaclesLogin,
testAdminKhhaoLogin,
validateAdminCloudtentaclesSession,
} from '@/services/admin'
import type {
AdminAgisoMessagingDefaults,
AdminAgisoObservedShopItem,
AdminAgisoShopConfigItem,
AdminCloudtentaclesLoginTestResult,
AdminCloudtentaclesSendSmsResult,
AdminCloudtentaclesSourceConfig,
AdminCloudtentaclesValidateSessionResult,
AdminKhhaoLoginTestResult,
AdminKhhaoOrderQueryResult,
AdminKhhaoSyncState,
@@ -23,7 +32,7 @@ import type {
import { hasAdminRole } from '@/utils/admin-auth'
import { formatAdminDateTime } from '@/utils/admin-time'
type PlatformTab = 'agiso' | 'khhao'
type PlatformTab = 'agiso' | 'khhao' | 'cloudtentacles'
type EditableDefaults = {
messageTemplate: string
@@ -43,12 +52,14 @@ type EditableShop = {
const loading = ref(true)
const saving = ref(false)
const khhaoSaving = ref(false)
const cloudtentaclesSaving = ref(false)
const errorMessage = ref('')
const activePlatform = ref<PlatformTab>('agiso')
const agisoFilePath = ref('')
const khhaoFilePath = ref('')
const khhaoStateFilePath = ref('')
const cloudtentaclesFilePath = ref('')
const defaults = ref<EditableDefaults>(createEmptyDefaults())
const shops = ref<EditableShop[]>([])
const observedShops = ref<AdminAgisoObservedShopItem[]>([])
@@ -74,6 +85,23 @@ const khhaoLoginResult = ref<AdminKhhaoLoginTestResult | null>(null)
const khhaoQueryResult = ref<AdminKhhaoOrderQueryResult | null>(null)
const khhaoSyncResult = ref<AdminKhhaoOrderSyncResult | null>(null)
const khhaoAutoSyncState = ref<AdminKhhaoSyncState | null>(null)
const cloudtentaclesForm = ref({
baseUrl: 'https://123.207.217.176',
username: '',
password: '',
phone: '',
code: '',
token: '',
deviceId: '-',
deviceType: 0,
})
const cloudtentaclesSendingSms = ref(false)
const cloudtentaclesTesting = ref(false)
const cloudtentaclesValidating = ref(false)
const cloudtentaclesResultError = ref('')
const cloudtentaclesSmsResult = ref<AdminCloudtentaclesSendSmsResult | null>(null)
const cloudtentaclesLoginResult = ref<AdminCloudtentaclesLoginTestResult | null>(null)
const cloudtentaclesValidateResult = ref<AdminCloudtentaclesValidateSessionResult | null>(null)
const agisoStats = computed(() => ({
configuredShopCount: shops.value.length,
@@ -88,6 +116,16 @@ const khhaoStats = computed(() => ({
ignoredCount: khhaoAutoSyncState.value?.ignoredCount || khhaoSyncResult.value?.ignoredCount || 0,
}))
const cloudtentaclesStats = computed(() => ({
hasCredential: Boolean(
cloudtentaclesForm.value.username.trim()
&& cloudtentaclesForm.value.password.trim()
&& cloudtentaclesForm.value.phone.trim(),
),
smsSent: Boolean(cloudtentaclesSmsResult.value),
validated: Boolean(cloudtentaclesValidateResult.value || cloudtentaclesLoginResult.value),
}))
function createEmptyDefaults(): EditableDefaults {
return {
messageTemplate: '',
@@ -136,9 +174,10 @@ async function loadConfigs() {
errorMessage.value = ''
try {
const [agisoResponse, khhaoResponse] = await Promise.all([
const [agisoResponse, khhaoResponse, cloudtentaclesResponse] = await Promise.all([
fetchAdminAgisoShopConfigs(),
fetchAdminKhhaoSourceConfig(),
fetchAdminCloudtentaclesSourceConfig(),
])
agisoFilePath.value = agisoResponse.data.filePath
@@ -162,6 +201,18 @@ async function loadConfigs() {
autoSyncPages: khhaoResponse.data.source.autoSync?.pages || 2,
autoSyncPageSize: khhaoResponse.data.source.autoSync?.pageSize || 20,
}
cloudtentaclesFilePath.value = cloudtentaclesResponse.data.filePath
cloudtentaclesForm.value = {
baseUrl: cloudtentaclesResponse.data.source.baseUrl || 'https://123.207.217.176',
username: cloudtentaclesResponse.data.source.username || '',
password: cloudtentaclesResponse.data.source.password || '',
phone: cloudtentaclesResponse.data.source.phone || '',
code: '',
token: '',
deviceId: cloudtentaclesResponse.data.source.deviceId || '-',
deviceType: cloudtentaclesResponse.data.source.deviceType || 0,
}
} catch (error) {
errorMessage.value = error instanceof Error ? error.message : '读取平台配置失败'
} finally {
@@ -389,6 +440,135 @@ async function handleKhhaoSyncOrders() {
}
}
function buildCloudtentaclesConfigPayload(): AdminCloudtentaclesSourceConfig {
return {
enabled: true,
baseUrl: cloudtentaclesForm.value.baseUrl.trim() || 'https://123.207.217.176',
username: cloudtentaclesForm.value.username.trim(),
password: cloudtentaclesForm.value.password.trim(),
phone: cloudtentaclesForm.value.phone.trim(),
deviceId: cloudtentaclesForm.value.deviceId.trim() || '-',
deviceType: Number(cloudtentaclesForm.value.deviceType || 0),
}
}
function buildCloudtentaclesLoginPayload() {
return {
...buildCloudtentaclesConfigPayload(),
code: cloudtentaclesForm.value.code.trim(),
}
}
function ensureCloudtentaclesBaseFields() {
if (!cloudtentaclesForm.value.username.trim() || !cloudtentaclesForm.value.phone.trim()) {
cloudtentaclesResultError.value = '请先填写 cloudtentacles 账号和手机号'
return false
}
return true
}
function ensureCloudtentaclesLoginFields() {
if (
!cloudtentaclesForm.value.username.trim()
|| !cloudtentaclesForm.value.password.trim()
|| !cloudtentaclesForm.value.phone.trim()
) {
cloudtentaclesResultError.value = '请先填写 cloudtentacles 账号、密码和手机号'
return false
}
if (!cloudtentaclesForm.value.code.trim()) {
cloudtentaclesResultError.value = '请先填写短信验证码'
return false
}
return true
}
async function handleCloudtentaclesSaveSource() {
cloudtentaclesSaving.value = true
cloudtentaclesResultError.value = ''
try {
const response = await saveAdminCloudtentaclesSourceConfig(buildCloudtentaclesConfigPayload())
cloudtentaclesFilePath.value = response.data.filePath
showSuccess('cloudtentacles 履约平台配置已保存')
} catch (error) {
cloudtentaclesResultError.value = error instanceof Error ? error.message : 'cloudtentacles 配置保存失败'
showError(cloudtentaclesResultError.value)
} finally {
cloudtentaclesSaving.value = false
}
}
async function handleCloudtentaclesSendSmsCode() {
if (!ensureCloudtentaclesBaseFields()) {
return
}
cloudtentaclesSendingSms.value = true
cloudtentaclesResultError.value = ''
try {
const response = await sendAdminCloudtentaclesSmsCode(buildCloudtentaclesConfigPayload())
cloudtentaclesSmsResult.value = response.data
showSuccess('cloudtentacles 短信验证码已发送')
} catch (error) {
cloudtentaclesResultError.value = error instanceof Error ? error.message : 'cloudtentacles 发送短信验证码失败'
showError(cloudtentaclesResultError.value)
} finally {
cloudtentaclesSendingSms.value = false
}
}
async function handleCloudtentaclesTestLogin() {
if (!ensureCloudtentaclesLoginFields()) {
return
}
cloudtentaclesTesting.value = true
cloudtentaclesResultError.value = ''
try {
const response = await testAdminCloudtentaclesLogin(buildCloudtentaclesLoginPayload())
cloudtentaclesLoginResult.value = response.data
cloudtentaclesForm.value.token = response.data.token || cloudtentaclesForm.value.token
showSuccess('cloudtentacles 登录测试成功')
} catch (error) {
cloudtentaclesResultError.value = error instanceof Error ? error.message : 'cloudtentacles 登录测试失败'
showError(cloudtentaclesResultError.value)
} finally {
cloudtentaclesTesting.value = false
}
}
async function handleCloudtentaclesValidateSession() {
const token = cloudtentaclesForm.value.token.trim()
if (!token) {
cloudtentaclesResultError.value = '请先填写 token'
return
}
cloudtentaclesValidating.value = true
cloudtentaclesResultError.value = ''
try {
const response = await validateAdminCloudtentaclesSession({
baseUrl: cloudtentaclesForm.value.baseUrl.trim() || 'https://123.207.217.176',
token,
deviceId: cloudtentaclesForm.value.deviceId.trim() || '-',
deviceType: Number(cloudtentaclesForm.value.deviceType || 0),
})
cloudtentaclesValidateResult.value = response.data
showSuccess('cloudtentacles 会话校验成功')
} catch (error) {
cloudtentaclesResultError.value = error instanceof Error ? error.message : 'cloudtentacles 会话校验失败'
showError(cloudtentaclesResultError.value)
} finally {
cloudtentaclesValidating.value = false
}
}
onMounted(loadConfigs)
</script>
@@ -397,7 +577,7 @@ onMounted(loadConfigs)
<header class="hero-card">
<div class="hero-copy">
<h1>平台配置</h1>
<p>按平台分别管理来源接入凭据与店铺能力Agiso 负责 webhook / 店铺消息配置khhao 负责账号登录拉单与同步</p>
<p>按平台分别管理来源接入凭据与店铺能力Agiso 负责 webhook / 店铺消息配置khhao 负责来源拉单cloudtentacles 负责外部履约平台登录</p>
</div>
<div class="hero-actions">
<el-button round @click="loadConfigs">刷新全部配置</el-button>
@@ -446,6 +626,23 @@ onMounted(loadConfigs)
<span>最近同步成功 {{ khhaoStats.syncedCount }} </span>
</div>
</button>
<button
type="button"
:class="['platform-overview-card', { 'is-active': activePlatform === 'cloudtentacles' }]"
@click="switchPlatform('cloudtentacles')"
>
<div class="platform-head">
<span class="platform-badge">cloudtentacles</span>
<span class="platform-tag">履约 / 登录</span>
</div>
<strong>{{ cloudtentaclesStats.hasCredential ? '已配置' : '未配置' }}</strong>
<span>履约凭据</span>
<div class="platform-metrics">
<span>短信 {{ cloudtentaclesStats.smsSent ? '已发送' : '未发送' }}</span>
<span>会话 {{ cloudtentaclesStats.validated ? '已验证' : '未验证' }}</span>
</div>
</button>
</section>
<section class="platform-switcher">
@@ -463,6 +660,13 @@ onMounted(loadConfigs)
>
khhao 来源与同步
</button>
<button
type="button"
:class="['switch-chip', { 'is-active': activePlatform === 'cloudtentacles' }]"
@click="switchPlatform('cloudtentacles')"
>
cloudtentacles 履约平台
</button>
</section>
<template v-if="activePlatform === 'agiso'">
@@ -625,7 +829,7 @@ onMounted(loadConfigs)
</section>
</template>
<template v-else>
<template v-else-if="activePlatform === 'khhao'">
<section class="meta-card">
<div class="meta-line">
<span class="meta-label">配置文件</span>
@@ -842,6 +1046,136 @@ onMounted(loadConfigs)
</table>
</section>
</template>
<template v-else>
<section class="meta-card">
<div class="meta-line">
<span class="meta-label">配置文件</span>
<code>{{ cloudtentaclesFilePath || '-' }}</code>
</div>
<div class="meta-line">
<span class="meta-label">职责</span>
<span>管理 cloudtentacles 外部履约平台的账号短信验证码登录与 token 探活该区域不处理订单来源同步</span>
</div>
</section>
<section class="table-card">
<div class="section-title-row">
<div>
<h3>cloudtentacles 履约平台配置</h3>
<p>先保存账号密码手机号和设备头再发送短信验证码最后执行登录测试或手动校验 token</p>
</div>
<div class="section-actions">
<el-button :loading="cloudtentaclesSaving" round @click="handleCloudtentaclesSaveSource">保存履约配置</el-button>
<el-button :loading="cloudtentaclesSendingSms" round @click="handleCloudtentaclesSendSmsCode">发送短信验证码</el-button>
<el-button :loading="cloudtentaclesTesting" round type="primary" @click="handleCloudtentaclesTestLogin">测试登录</el-button>
<el-button :loading="cloudtentaclesValidating" round @click="handleCloudtentaclesValidateSession">校验 token</el-button>
</div>
</div>
<div class="form-grid">
<label class="field-block field-wide">
<span>Base URL</span>
<input v-model="cloudtentaclesForm.baseUrl" class="text-input" placeholder="https://123.207.217.176" />
</label>
<label class="field-block">
<span>账号</span>
<input v-model="cloudtentaclesForm.username" class="text-input" placeholder="cloudtentacles 登录账号" />
</label>
<label class="field-block">
<span>密码</span>
<input v-model="cloudtentaclesForm.password" class="text-input" type="password" placeholder="cloudtentacles 登录密码" />
</label>
<label class="field-block">
<span>手机号</span>
<input v-model="cloudtentaclesForm.phone" class="text-input" placeholder="接收短信验证码的手机号" />
</label>
<label class="field-block">
<span>短信验证码</span>
<input v-model="cloudtentaclesForm.code" class="text-input" placeholder="收到验证码后填写" />
</label>
<label class="field-block">
<span>deviceId</span>
<input v-model="cloudtentaclesForm.deviceId" class="text-input" placeholder="-" />
</label>
<label class="field-block">
<span>deviceType</span>
<input v-model.number="cloudtentaclesForm.deviceType" class="text-input" type="number" />
</label>
<label class="field-block field-wide">
<span>token可手动粘贴</span>
<input v-model="cloudtentaclesForm.token" class="text-input" placeholder="登录成功后可自动回填,也可手动粘贴" />
</label>
</div>
<p v-if="cloudtentaclesResultError" class="error-copy">{{ cloudtentaclesResultError }}</p>
<div class="result-grid">
<article class="result-card">
<span class="result-label">短信发送</span>
<strong>{{ cloudtentaclesSmsResult ? '成功' : '未执行' }}</strong>
<p v-if="cloudtentaclesSmsResult">
{{ cloudtentaclesSmsResult.phoneMasked }}{{ formatAdminDateTime(cloudtentaclesSmsResult.sentAt) }}
</p>
<p v-else>先发送短信验证码确认手机号可用</p>
</article>
<article class="result-card">
<span class="result-label">登录结果</span>
<strong>{{ cloudtentaclesLoginResult ? '成功' : '未执行' }}</strong>
<p v-if="cloudtentaclesLoginResult">
{{ cloudtentaclesLoginResult.phoneMasked }}{{ formatAdminDateTime(cloudtentaclesLoginResult.loggedInAt) }}
</p>
<p v-else>填写短信验证码后可执行测试登录</p>
</article>
<article class="result-card">
<span class="result-label">会话校验</span>
<strong>{{ cloudtentaclesValidateResult ? '成功' : '未执行' }}</strong>
<p v-if="cloudtentaclesValidateResult">
权限 {{ cloudtentaclesValidateResult.session.permissionCount }} 余额 {{ cloudtentaclesValidateResult.asset }}
</p>
<p v-else>登录成功后可继续校验 token 会话</p>
</article>
</div>
</section>
<section v-if="cloudtentaclesLoginResult || cloudtentaclesValidateResult" class="table-card">
<div class="section-title-row">
<div>
<h3>cloudtentacles 调试结果</h3>
<p>用于确认当前账号会话权限和用户信息后续发货执行器会复用这里的登录能力</p>
</div>
</div>
<div class="result-grid">
<article class="result-card">
<span class="result-label">当前 token</span>
<strong>{{ cloudtentaclesValidateResult?.session.tokenMasked || cloudtentaclesLoginResult?.session.tokenMasked || '-' }}</strong>
<p>原始 token 已写入上方输入框可继续用于校验或后续调试</p>
</article>
<article class="result-card">
<span class="result-label">权限</span>
<strong>{{ cloudtentaclesValidateResult?.session.permissionCount || cloudtentaclesLoginResult?.session.permissionCount || 0 }} </strong>
<p>{{ (cloudtentaclesValidateResult?.session.permissions || cloudtentaclesLoginResult?.session.permissions || []).join('、') || '暂无' }}</p>
</article>
<article class="result-card">
<span class="result-label">余额</span>
<strong>{{ cloudtentaclesValidateResult?.asset || cloudtentaclesLoginResult?.asset || 0 }}</strong>
<p>可用于确认当前履约账号状态</p>
</article>
</div>
</section>
</template>
</template>
</template>
</section>
@@ -906,7 +1240,7 @@ onMounted(loadConfigs)
.overview-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 16px;
}