增加 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
+21
View File
@@ -76,6 +76,27 @@ module.exports = {
captchaPath: '/verify_img.php',
orderListPath: '/c/payOrder/get.php',
},
cloudtentacles: {
baseUrl: 'https://123.207.217.176',
timeoutMs: 5000,
sendSmsPath: '/public/verif_code',
loginPath: '/public/login',
userInfoPath: '/user/info',
assetPath: '/user/get_asset',
permissionPath: '/user/get_permission',
clientSource: 'ct-client',
deviceId: '-',
deviceType: 0,
publicKeyPem: `-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsSzjk+9st2f8IpYqYaM5
F7K27ZR2IvzxTr2w4zgZAS2+BRdgMvXUOITdFQT+ytYF/r3pIgTkL7MF9UQuylmb
S57DiqGBLNBS7FzKyYzWTOc9yYA3+KrarbvpUn/Bm3UK+KKB17viPy+0pnNAHysJ
pzqadmAtQhI1ZDujJ2sW+FkV/pC8KIFahiyPIHjncy1ZG/BbRF3BDhvDcr6O7scO
GsHstGf9XsZBIITS5LYmlai7OdY4ZIVxUtQbfWzibihO+YDJJzFgt47drX0Ajo85
PfQTPNA++KsXtwRX9M6Re3vkTDRsutIIWKtj8jqhUbbYS3vzS8GJnAWavUFVkR15
0wIDAQAB
-----END PUBLIC KEY-----`,
},
},
redeem: {
@@ -0,0 +1,9 @@
{
"enabled": true,
"baseUrl": "https://123.207.217.176",
"username": "17665234375",
"password": "yaochao11",
"phone": "17665234375",
"deviceId": "-",
"deviceType": 0
}
+3 -3
View File
@@ -1,11 +1,11 @@
{
"syncFromCreatedAt": "2026-05-02T10:53:28.550Z",
"lastRunStartedAt": "2026-05-02T12:36:55.519Z",
"lastRunFinishedAt": "2026-05-02T12:36:56.704Z",
"lastRunStartedAt": "2026-05-02T13:40:21.932Z",
"lastRunFinishedAt": "2026-05-02T13:40:23.162Z",
"lastRunStatus": "success",
"lastErrorMessage": "",
"fetchedCount": 40,
"syncedCount": 0,
"ignoredCount": 40,
"lastOrderCreatedAt": "2026-05-02T12:36:45.000Z"
"lastOrderCreatedAt": "2026-05-02T13:39:40.000Z"
}
+55
View File
@@ -260,6 +260,61 @@ function applyEnvOverrides(baseConfig) {
nextConfig.platforms.khhao.orderListPath = khhaoOrderListPath
}
const cloudtentaclesBaseUrl = String(process.env.CLOUDTENTACLES_BASE_URL || '').trim()
if (cloudtentaclesBaseUrl) {
nextConfig.platforms.cloudtentacles.baseUrl = cloudtentaclesBaseUrl
}
const cloudtentaclesTimeoutMs = parseInteger(process.env.CLOUDTENTACLES_TIMEOUT_MS)
if (cloudtentaclesTimeoutMs !== null) {
nextConfig.platforms.cloudtentacles.timeoutMs = cloudtentaclesTimeoutMs
}
const cloudtentaclesSendSmsPath = String(process.env.CLOUDTENTACLES_SEND_SMS_PATH || '').trim()
if (cloudtentaclesSendSmsPath) {
nextConfig.platforms.cloudtentacles.sendSmsPath = cloudtentaclesSendSmsPath
}
const cloudtentaclesLoginPath = String(process.env.CLOUDTENTACLES_LOGIN_PATH || '').trim()
if (cloudtentaclesLoginPath) {
nextConfig.platforms.cloudtentacles.loginPath = cloudtentaclesLoginPath
}
const cloudtentaclesUserInfoPath = String(process.env.CLOUDTENTACLES_USER_INFO_PATH || '').trim()
if (cloudtentaclesUserInfoPath) {
nextConfig.platforms.cloudtentacles.userInfoPath = cloudtentaclesUserInfoPath
}
const cloudtentaclesAssetPath = String(process.env.CLOUDTENTACLES_ASSET_PATH || '').trim()
if (cloudtentaclesAssetPath) {
nextConfig.platforms.cloudtentacles.assetPath = cloudtentaclesAssetPath
}
const cloudtentaclesPermissionPath = String(process.env.CLOUDTENTACLES_PERMISSION_PATH || '').trim()
if (cloudtentaclesPermissionPath) {
nextConfig.platforms.cloudtentacles.permissionPath = cloudtentaclesPermissionPath
}
const cloudtentaclesPublicKeyPem = String(process.env.CLOUDTENTACLES_PUBLIC_KEY_PEM || '').trim()
if (cloudtentaclesPublicKeyPem) {
nextConfig.platforms.cloudtentacles.publicKeyPem = cloudtentaclesPublicKeyPem
}
const cloudtentaclesClientSource = String(process.env.CLOUDTENTACLES_CLIENT_SOURCE || '').trim()
if (cloudtentaclesClientSource) {
nextConfig.platforms.cloudtentacles.clientSource = cloudtentaclesClientSource
}
const cloudtentaclesDeviceId = String(process.env.CLOUDTENTACLES_DEVICE_ID || '').trim()
if (cloudtentaclesDeviceId) {
nextConfig.platforms.cloudtentacles.deviceId = cloudtentaclesDeviceId
}
const cloudtentaclesDeviceType = parseInteger(process.env.CLOUDTENTACLES_DEVICE_TYPE)
if (cloudtentaclesDeviceType !== null) {
nextConfig.platforms.cloudtentacles.deviceType = cloudtentaclesDeviceType
}
const proofMode = String(process.env.TENCENT_REDEEM_PROOF_MODE || '').trim()
if (proofMode) {
nextConfig.redeem.proofMode = proofMode
@@ -3,16 +3,21 @@
import { Router } from 'express'
import {
getAdminCloudtentaclesSourceConfig,
getAdminAgisoShopConfigs,
getAdminFulfillmentBindingConfigs,
getAdminKhhaoSourceConfig,
lookupAdminFulfillmentBindingOrder,
queryAdminKhhaoOrders,
sendAdminCloudtentaclesSmsCode,
syncAdminKhhaoOrders,
testAdminCloudtentaclesLogin,
testAdminKhhaoLogin,
updateAdminCloudtentaclesSourceConfig,
updateAdminAgisoShopConfigs,
updateAdminKhhaoSourceConfig,
updateAdminFulfillmentBindingConfigs,
validateAdminCloudtentaclesSession,
} from '../../services/admin/admin-platform-config-service.js'
import { createJsonHandler, requireAdminRoles } from './shared.js'
@@ -22,6 +27,10 @@ import { createJsonHandler, requireAdminRoles } from './shared.js'
/** @typedef {import('../../types/admin-route-inputs.js').AdminKhhaoOrderQueryRouteBody} AdminKhhaoOrderQueryRouteBody */
/** @typedef {import('../../types/admin-route-inputs.js').AdminKhhaoSourceConfigRouteBody} AdminKhhaoSourceConfigRouteBody */
/** @typedef {import('../../types/admin-route-inputs.js').AdminKhhaoTestLoginRouteBody} AdminKhhaoTestLoginRouteBody */
/** @typedef {import('../../types/admin-route-inputs.js').AdminCloudtentaclesSourceConfigRouteBody} AdminCloudtentaclesSourceConfigRouteBody */
/** @typedef {import('../../types/admin-route-inputs.js').AdminCloudtentaclesSendSmsCodeRouteBody} AdminCloudtentaclesSendSmsCodeRouteBody */
/** @typedef {import('../../types/admin-route-inputs.js').AdminCloudtentaclesTestLoginRouteBody} AdminCloudtentaclesTestLoginRouteBody */
/** @typedef {import('../../types/admin-route-inputs.js').AdminCloudtentaclesValidateSessionRouteBody} AdminCloudtentaclesValidateSessionRouteBody */
/** @typedef {import('../../types/admin-write-models.js').AdminAgisoShopConfigSaveResponse} AdminAgisoShopConfigSaveResponse */
const router = Router()
@@ -163,6 +172,102 @@ router.post('/platform-config/khhao/sync-orders', createJsonHandler(
},
))
router.get('/platform-config/cloudtentacles-source', createJsonHandler(
() => getAdminCloudtentaclesSourceConfig(),
{
successMessage: 'ok',
errorMessage: '读取 cloudtentacles 履约平台配置失败',
scope: '[admin/platform-config/cloudtentacles-source]',
},
))
router.post('/platform-config/cloudtentacles-source', createJsonHandler(
(req) => updateAdminCloudtentaclesSourceConfig(/** @type {AdminCloudtentaclesSourceConfigRouteBody} */ (req.body)),
{
successMessage: 'cloudtentacles 履约平台配置已保存',
errorMessage: '保存 cloudtentacles 履约平台配置失败',
scope: '[admin/platform-config/cloudtentacles-source]',
audit: (_req, data) => {
const result = /** @type {{ filePath?: string, source?: { username?: string, enabled?: boolean } }} */ (data)
return {
action: 'platform_cloudtentacles_source_updated',
targetType: 'platform_config',
targetId: 'cloudtentacles_source',
data: {
filePath: String(result.filePath || '').trim(),
username: String(result.source?.username || '').trim(),
enabled: Boolean(result.source?.enabled),
},
}
},
},
))
router.post('/platform-config/cloudtentacles/send-sms-code', createJsonHandler(
(req) => sendAdminCloudtentaclesSmsCode(/** @type {AdminCloudtentaclesSendSmsCodeRouteBody} */ (req.body)),
{
successMessage: 'cloudtentacles 短信验证码已发送',
errorMessage: 'cloudtentacles 发送短信验证码失败',
scope: '[admin/platform-config/cloudtentacles/send-sms-code]',
audit: (req, data) => {
const body = /** @type {AdminCloudtentaclesSendSmsCodeRouteBody} */ (req.body)
const result = /** @type {{ username?: string, phoneMasked?: string, baseUrl?: string }} */ (data)
return {
action: 'platform_cloudtentacles_send_sms_code',
targetType: 'platform_config',
targetId: String(result.username || body.username || '').trim() || 'cloudtentacles',
data: {
baseUrl: result.baseUrl || String(body.baseUrl || '').trim(),
phoneMasked: result.phoneMasked || '',
},
}
},
},
))
router.post('/platform-config/cloudtentacles/test-login', createJsonHandler(
(req) => testAdminCloudtentaclesLogin(/** @type {AdminCloudtentaclesTestLoginRouteBody} */ (req.body)),
{
successMessage: 'cloudtentacles 登录测试成功',
errorMessage: 'cloudtentacles 登录测试失败',
scope: '[admin/platform-config/cloudtentacles/test-login]',
audit: (req, data) => {
const body = /** @type {AdminCloudtentaclesTestLoginRouteBody} */ (req.body)
const result = /** @type {{ baseUrl?: string, username?: string, session?: { permissionCount?: number } }} */ (data)
return {
action: 'platform_cloudtentacles_test_login',
targetType: 'platform_config',
targetId: String(result.username || body.username || '').trim() || 'cloudtentacles',
data: {
baseUrl: result.baseUrl || String(body.baseUrl || '').trim(),
permissionCount: Number(result.session?.permissionCount || 0),
},
}
},
},
))
router.post('/platform-config/cloudtentacles/validate-session', createJsonHandler(
(req) => validateAdminCloudtentaclesSession(/** @type {AdminCloudtentaclesValidateSessionRouteBody} */ (req.body)),
{
successMessage: 'cloudtentacles 会话校验成功',
errorMessage: 'cloudtentacles 会话校验失败',
scope: '[admin/platform-config/cloudtentacles/validate-session]',
audit: (_req, data) => {
const result = /** @type {{ baseUrl?: string, session?: { permissionCount?: number } }} */ (data)
return {
action: 'platform_cloudtentacles_validate_session',
targetType: 'platform_config',
targetId: 'cloudtentacles_session',
data: {
baseUrl: String(result.baseUrl || '').trim(),
permissionCount: Number(result.session?.permissionCount || 0),
},
}
},
},
))
router.get('/platform-config/fulfillment-bindings', createJsonHandler(
() => getAdminFulfillmentBindingConfigs(),
{
@@ -15,6 +15,16 @@ import { syncKhhaoOrders } from '../platforms/khhao/order-sync-service.js'
import { getKhhaoSourceConfig, getKhhaoSourcesFilePath, saveKhhaoSourceConfig } from '../platforms/khhao/source-config-service.js'
import { ensureKhhaoSyncBaseline, getKhhaoSyncState, getKhhaoSyncStateFilePath } from '../platforms/khhao/sync-state-service.js'
import { loginKhhaoSession } from '../platforms/khhao/session-service.js'
import {
getCloudtentaclesSourceConfig,
getCloudtentaclesSourcesFilePath,
saveCloudtentaclesSourceConfig,
} from '../platforms/cloudtentacles/source-config-service.js'
import {
loginCloudtentaclesSession,
sendCloudtentaclesSmsCode,
validateCloudtentaclesSession,
} from '../platforms/cloudtentacles/session-service.js'
import { normalizeAgisoMessageTemplate } from '../platforms/agiso/xianyu/message-service.js'
import {
getOrderFulfillmentBindingConfigs,
@@ -32,6 +42,10 @@ import { resolveDisplayShopName } from './admin-read-shared-helpers.js'
/** @typedef {import('../../types/admin-write-inputs.js').AdminKhhaoSourceConfigInput} AdminKhhaoSourceConfigInput */
/** @typedef {import('../../types/admin-write-inputs.js').AdminKhhaoOrderQueryInput} AdminKhhaoOrderQueryInput */
/** @typedef {import('../../types/admin-write-inputs.js').AdminKhhaoTestLoginInput} AdminKhhaoTestLoginInput */
/** @typedef {import('../../types/admin-write-inputs.js').AdminCloudtentaclesSourceConfigInput} AdminCloudtentaclesSourceConfigInput */
/** @typedef {import('../../types/admin-write-inputs.js').AdminCloudtentaclesSendSmsCodeInput} AdminCloudtentaclesSendSmsCodeInput */
/** @typedef {import('../../types/admin-write-inputs.js').AdminCloudtentaclesTestLoginInput} AdminCloudtentaclesTestLoginInput */
/** @typedef {import('../../types/admin-write-inputs.js').AdminCloudtentaclesValidateSessionInput} AdminCloudtentaclesValidateSessionInput */
/** @typedef {import('../../types/admin-write-inputs.js').AdminFulfillmentBindingConfigSaveInput} AdminFulfillmentBindingConfigSaveInput */
/** @typedef {import('../../types/admin-write-inputs.js').AdminFulfillmentBindingLookupInput} AdminFulfillmentBindingLookupInput */
@@ -336,6 +350,119 @@ export async function syncAdminKhhaoOrders(payload = /** @type {AdminKhhaoOrderQ
})
}
export function getAdminCloudtentaclesSourceConfig() {
const config = getCloudtentaclesSourceConfig()
return {
filePath: getCloudtentaclesSourcesFilePath(),
source: {
enabled: config.enabled !== false,
baseUrl: String(config.baseUrl || '').trim(),
username: String(config.username || '').trim(),
password: String(config.password || '').trim(),
phone: String(config.phone || '').trim(),
deviceId: String(config.deviceId || '-').trim() || '-',
deviceType: Number(config.deviceType || 0),
},
}
}
/** @param {AdminCloudtentaclesSourceConfigInput} [payload] */
export function updateAdminCloudtentaclesSourceConfig(payload = /** @type {AdminCloudtentaclesSourceConfigInput} */ ({})) {
const saved = saveCloudtentaclesSourceConfig({
enabled: payload.enabled !== false,
baseUrl: String(payload.baseUrl || '').trim() || 'https://123.207.217.176',
username: String(payload.username || '').trim(),
password: String(payload.password || '').trim(),
phone: String(payload.phone || '').trim(),
deviceId: String(payload.deviceId || '-').trim() || '-',
deviceType: Number(payload.deviceType || 0),
})
return {
filePath: getCloudtentaclesSourcesFilePath(),
source: {
enabled: saved.enabled !== false,
baseUrl: String(saved.baseUrl || '').trim(),
username: String(saved.username || '').trim(),
password: String(saved.password || '').trim(),
phone: String(saved.phone || '').trim(),
deviceId: String(saved.deviceId || '-').trim() || '-',
deviceType: Number(saved.deviceType || 0),
},
}
}
/** @param {AdminCloudtentaclesSendSmsCodeInput} [payload] */
export async function sendAdminCloudtentaclesSmsCode(payload = /** @type {AdminCloudtentaclesSendSmsCodeInput} */ ({})) {
const savedSource = getCloudtentaclesSourceConfig()
const result = await sendCloudtentaclesSmsCode({
baseUrl: pickFirstNonEmpty([payload.baseUrl, savedSource.baseUrl, 'https://123.207.217.176']),
username: pickFirstNonEmpty([payload.username, savedSource.username]),
phone: pickFirstNonEmpty([payload.phone, savedSource.phone]),
deviceId: pickFirstNonEmpty([payload.deviceId, savedSource.deviceId, '-']),
deviceType: payload.deviceType ?? savedSource.deviceType,
})
return {
...result,
phoneMasked: maskPhone(result.phone),
}
}
/** @param {AdminCloudtentaclesTestLoginInput} [payload] */
export async function testAdminCloudtentaclesLogin(payload = /** @type {AdminCloudtentaclesTestLoginInput} */ ({})) {
const savedSource = getCloudtentaclesSourceConfig()
const session = await loginCloudtentaclesSession({
baseUrl: pickFirstNonEmpty([payload.baseUrl, savedSource.baseUrl, 'https://123.207.217.176']),
username: pickFirstNonEmpty([payload.username, savedSource.username]),
password: pickFirstNonEmpty([payload.password, savedSource.password]),
phone: pickFirstNonEmpty([payload.phone, savedSource.phone]),
code: String(payload.code || '').trim(),
deviceId: pickFirstNonEmpty([payload.deviceId, savedSource.deviceId, '-']),
deviceType: payload.deviceType ?? savedSource.deviceType,
})
return {
baseUrl: session.baseUrl,
username: session.username,
phoneMasked: maskPhone(session.phone),
loggedInAt: session.loggedInAt,
responseMessage: session.responseMessage,
token: session.token,
session: {
tokenMasked: maskSecret(session.token),
permissionCount: session.permissions.length,
permissions: session.permissions,
},
userInfo: session.userInfo,
asset: session.asset,
}
}
/** @param {AdminCloudtentaclesValidateSessionInput} [payload] */
export async function validateAdminCloudtentaclesSession(payload = /** @type {AdminCloudtentaclesValidateSessionInput} */ ({})) {
const savedSource = getCloudtentaclesSourceConfig()
const session = await validateCloudtentaclesSession({
baseUrl: pickFirstNonEmpty([payload.baseUrl, savedSource.baseUrl, 'https://123.207.217.176']),
token: String(payload.token || '').trim(),
deviceId: pickFirstNonEmpty([payload.deviceId, savedSource.deviceId, '-']),
deviceType: payload.deviceType ?? savedSource.deviceType,
})
return {
baseUrl: session.baseUrl,
loggedInAt: session.loggedInAt,
session: {
tokenMasked: maskSecret(session.token),
permissionCount: session.permissions.length,
permissions: session.permissions,
},
userInfo: session.userInfo,
asset: session.asset,
}
}
export async function getAdminFulfillmentBindingConfigs() {
const bindings = getOrderFulfillmentBindingConfigs()
const rowsResult = await query(
@@ -685,6 +812,19 @@ function maskSecret(value) {
return `${normalized.slice(0, 6)}****${normalized.slice(-6)}`
}
function maskPhone(value) {
const normalized = String(value || '').trim()
if (!normalized) {
return ''
}
if (normalized.length < 7) {
return normalized
}
return `${normalized.slice(0, 3)}****${normalized.slice(-4)}`
}
function isPlainObject(value) {
return Boolean(value) && typeof value === 'object' && !Array.isArray(value)
}
@@ -0,0 +1,65 @@
// @ts-check
import { createHash, constants, publicEncrypt } from 'node:crypto'
import { createHttpError } from '../../../utils/http.js'
import { resolveCloudtentaclesConfig } from './shared.js'
export function md5CloudtentaclesPassword(password) {
return createHash('md5').update(String(password || ''), 'utf8').digest('hex')
}
export function encryptCloudtentaclesPayload(payload = {}, options = {}) {
const config = resolveCloudtentaclesConfig(options)
const publicKeyPem = String(config.publicKeyPem || '').trim()
if (!publicKeyPem) {
throw createHttpError('cloudtentacles 公钥未配置', {
statusCode: 500,
errorCode: 'cloudtentacles_public_key_missing',
})
}
const timestamp = Number(options.timestamp || Date.now())
const randomValue = String(options.randomValue || Math.random().toString(16)).trim() || Math.random().toString(16)
const normalizedPayload = removeEmptyFields(payload)
const plaintext = JSON.stringify({
t: timestamp,
r: randomValue,
s: String(options.clientSource || config.clientSource || 'ct-client').trim() || 'ct-client',
...normalizedPayload,
})
try {
const encrypted = publicEncrypt(
{
key: publicKeyPem,
padding: constants.RSA_PKCS1_OAEP_PADDING,
oaepHash: 'sha256',
},
Buffer.from(plaintext, 'utf8'),
)
return {
covert: encrypted.toString('base64'),
t: timestamp,
r: randomValue,
plaintext,
}
} catch (error) {
throw createHttpError('cloudtentacles 加密失败', {
statusCode: 500,
errorCode: 'cloudtentacles_encrypt_failed',
})
}
}
function removeEmptyFields(payload) {
if (!payload || typeof payload !== 'object') {
return {}
}
return Object.fromEntries(
Object.entries(payload).filter(([, value]) => value !== '' && value !== null && typeof value !== 'undefined'),
)
}
@@ -0,0 +1,107 @@
// @ts-check
import { createHttpError } from '../../../utils/http.js'
import { logInfo } from '../../../utils/logger.js'
import { buildCloudtentaclesHeaders, buildCloudtentaclesUrl, resolveCloudtentaclesConfig } from './shared.js'
export async function cloudtentaclesRequest(pathname, options = {}) {
const config = resolveCloudtentaclesConfig(options)
const url = buildCloudtentaclesUrl(config.baseUrl, pathname, options.searchParams)
const timeoutMs = Number(options.timeoutMs || config.timeoutMs || 5000)
const controller = new AbortController()
const timer = setTimeout(() => controller.abort(), timeoutMs)
const method = String(options.method || 'GET').trim().toUpperCase()
const headers = buildCloudtentaclesHeaders({
token: options.token,
contentType: options.contentType === null ? '' : options.contentType || inferContentType(options.body),
deviceId: options.deviceId ?? config.deviceId,
deviceType: options.deviceType ?? config.deviceType,
extra: options.headers,
})
try {
const response = await fetch(url, {
method,
headers,
body: normalizeRequestBody(options.body, headers['content-type']),
signal: controller.signal,
})
const rawText = await response.text()
const payload = tryParseJson(rawText)
if (!response.ok) {
throw createHttpError(`cloudtentacles 请求失败,HTTP ${response.status}`, {
statusCode: 502,
errorCode: 'cloudtentacles_http_error',
})
}
if (options.requireBusinessSuccess !== false && Number(payload?.code ?? 1) !== 0) {
throw createHttpError(String(payload?.message || payload?.msg || 'cloudtentacles 接口返回失败'), {
statusCode: Number(options.businessErrorStatusCode || 400),
errorCode: String(options.businessErrorCode || 'cloudtentacles_business_error'),
})
}
logInfo('[cloudtentacles/http]', '请求完成', {
method,
pathname,
status: response.status,
})
return {
url: url.toString(),
status: response.status,
headers: response.headers,
payload,
rawText,
}
} catch (error) {
if (error instanceof Error && error.name === 'AbortError') {
throw createHttpError(`cloudtentacles 请求超时(${timeoutMs}ms`, {
statusCode: 504,
errorCode: 'cloudtentacles_request_timeout',
})
}
throw error
} finally {
clearTimeout(timer)
}
}
function inferContentType(body) {
if (body == null) {
return ''
}
if (typeof body === 'string' || body instanceof URLSearchParams) {
return 'application/json'
}
return 'application/json'
}
function normalizeRequestBody(body, contentType) {
if (body == null) {
return undefined
}
if (typeof body === 'string' || body instanceof URLSearchParams) {
return body
}
if (String(contentType || '').includes('application/json')) {
return JSON.stringify(body)
}
return String(body)
}
function tryParseJson(text) {
try {
return JSON.parse(text)
} catch {
return null
}
}
@@ -0,0 +1,195 @@
// @ts-check
import { createHttpError } from '../../../utils/http.js'
import { logInfo } from '../../../utils/logger.js'
import { encryptCloudtentaclesPayload, md5CloudtentaclesPassword } from './crypto-service.js'
import { cloudtentaclesRequest } from './http-client.js'
import { resolveCloudtentaclesConfig } from './shared.js'
export async function sendCloudtentaclesSmsCode(payload = {}) {
const username = String(payload.username || '').trim()
const phone = String(payload.phone || '').trim()
if (!username) {
throw createHttpError('cloudtentacles 缺少账号', {
statusCode: 400,
errorCode: 'cloudtentacles_missing_username',
})
}
if (!phone) {
throw createHttpError('cloudtentacles 缺少手机号', {
statusCode: 400,
errorCode: 'cloudtentacles_missing_phone',
})
}
const config = resolveCloudtentaclesConfig(payload)
const encrypted = encryptCloudtentaclesPayload({
account: username,
phone,
}, config)
const result = await cloudtentaclesRequest(config.sendSmsPath, {
...config,
method: 'POST',
body: {
covert: encrypted.covert,
t: encrypted.t,
r: encrypted.r,
},
businessErrorStatusCode: 400,
businessErrorCode: 'cloudtentacles_send_sms_failed',
})
logInfo('[cloudtentacles/session]', '短信验证码发送成功', {
username,
phone: maskPhone(phone),
})
return {
baseUrl: config.baseUrl,
username,
phone,
sentAt: new Date().toISOString(),
responseMessage: String(result.payload?.message || result.payload?.msg || 'success'),
}
}
export async function loginCloudtentaclesSession(payload = {}) {
const username = String(payload.username || '').trim()
const password = String(payload.password || '').trim()
const phone = String(payload.phone || '').trim()
const code = String(payload.code || '').trim()
if (!username) {
throw createHttpError('cloudtentacles 登录缺少账号', {
statusCode: 400,
errorCode: 'cloudtentacles_login_missing_username',
})
}
if (!password) {
throw createHttpError('cloudtentacles 登录缺少密码', {
statusCode: 400,
errorCode: 'cloudtentacles_login_missing_password',
})
}
if (!phone) {
throw createHttpError('cloudtentacles 登录缺少手机号', {
statusCode: 400,
errorCode: 'cloudtentacles_login_missing_phone',
})
}
if (!code) {
throw createHttpError('cloudtentacles 登录缺少短信验证码', {
statusCode: 400,
errorCode: 'cloudtentacles_login_missing_code',
})
}
const config = resolveCloudtentaclesConfig(payload)
const encrypted = encryptCloudtentaclesPayload({
account: username,
password: md5CloudtentaclesPassword(password),
phone,
code,
}, config)
const loginResult = await cloudtentaclesRequest(config.loginPath, {
...config,
method: 'POST',
body: {
covert: encrypted.covert,
t: encrypted.t,
r: encrypted.r,
},
businessErrorStatusCode: 401,
businessErrorCode: 'cloudtentacles_login_failed',
})
const token = String(loginResult.payload?.data || '').trim()
if (!token) {
throw createHttpError('cloudtentacles 登录成功但未返回 token', {
statusCode: 502,
errorCode: 'cloudtentacles_missing_token',
})
}
const session = await validateCloudtentaclesSession({
...config,
token,
})
return {
...session,
username,
phone,
responseMessage: String(loginResult.payload?.message || loginResult.payload?.msg || 'success'),
}
}
export async function validateCloudtentaclesSession(payload = {}) {
const token = String(payload.token || '').trim()
if (!token) {
throw createHttpError('cloudtentacles 会话校验缺少 token', {
statusCode: 400,
errorCode: 'cloudtentacles_validate_missing_token',
})
}
const config = resolveCloudtentaclesConfig(payload)
const [userInfoResult, assetResult, permissionResult] = await Promise.all([
cloudtentaclesRequest(config.userInfoPath, {
...config,
method: 'POST',
token,
body: {},
businessErrorStatusCode: 401,
businessErrorCode: 'cloudtentacles_user_info_failed',
}),
cloudtentaclesRequest(config.assetPath, {
...config,
method: 'GET',
token,
contentType: 'application/json',
businessErrorStatusCode: 401,
businessErrorCode: 'cloudtentacles_asset_failed',
}),
cloudtentaclesRequest(config.permissionPath, {
...config,
method: 'GET',
token,
contentType: 'application/json',
businessErrorStatusCode: 401,
businessErrorCode: 'cloudtentacles_permission_failed',
}),
])
return {
baseUrl: config.baseUrl,
token,
loggedInAt: new Date().toISOString(),
userInfo: isPlainObject(userInfoResult.payload?.data) ? userInfoResult.payload.data : {},
asset: Number(assetResult.payload?.data || 0),
permissions: Array.isArray(permissionResult.payload?.data)
? permissionResult.payload.data.map((item) => String(item || '').trim()).filter(Boolean)
: [],
}
}
function maskPhone(phone) {
const normalized = String(phone || '').trim()
if (normalized.length < 7) {
return normalized
}
return `${normalized.slice(0, 3)}****${normalized.slice(-4)}`
}
function isPlainObject(value) {
return Boolean(value) && typeof value === 'object' && !Array.isArray(value)
}
@@ -0,0 +1,118 @@
// @ts-check
import { runtimeConfig } from '../../../config/runtime.js'
/** @typedef {import('../../../types/runtime-config.js').RuntimeConfig} RuntimeConfig */
export function resolveCloudtentaclesConfig(overrides = {}) {
/** @type {RuntimeConfig['platforms']['cloudtentacles']} */
const baseConfig = runtimeConfig.platforms?.cloudtentacles || {
baseUrl: '',
timeoutMs: 5000,
sendSmsPath: '/public/verif_code',
loginPath: '/public/login',
userInfoPath: '/user/info',
assetPath: '/user/get_asset',
permissionPath: '/user/get_permission',
publicKeyPem: '',
clientSource: 'ct-client',
deviceId: '-',
deviceType: 0,
}
return {
baseUrl: normalizeBaseUrl(overrides.baseUrl || baseConfig.baseUrl || 'https://123.207.217.176'),
timeoutMs: normalizePositiveInteger(overrides.timeoutMs || baseConfig.timeoutMs, 5000),
sendSmsPath: normalizePath(overrides.sendSmsPath || baseConfig.sendSmsPath, '/public/verif_code'),
loginPath: normalizePath(overrides.loginPath || baseConfig.loginPath, '/public/login'),
userInfoPath: normalizePath(overrides.userInfoPath || baseConfig.userInfoPath, '/user/info'),
assetPath: normalizePath(overrides.assetPath || baseConfig.assetPath, '/user/get_asset'),
permissionPath: normalizePath(overrides.permissionPath || baseConfig.permissionPath, '/user/get_permission'),
publicKeyPem: normalizePem(overrides.publicKeyPem || baseConfig.publicKeyPem),
clientSource: String(overrides.clientSource || baseConfig.clientSource || 'ct-client').trim() || 'ct-client',
deviceId: String(overrides.deviceId ?? baseConfig.deviceId ?? '-').trim() || '-',
deviceType: normalizeInteger(overrides.deviceType ?? baseConfig.deviceType, 0),
}
}
export function buildCloudtentaclesUrl(baseUrl, pathname, searchParams = null) {
const url = new URL(normalizePath(pathname, '/'), normalizeBaseUrl(baseUrl) || 'https://123.207.217.176')
if (searchParams && typeof searchParams === 'object') {
for (const [key, value] of Object.entries(searchParams)) {
if (typeof value === 'undefined' || value === null || value === '') {
continue
}
url.searchParams.set(key, String(value))
}
}
return url
}
export function buildCloudtentaclesHeaders({
token = '',
contentType = 'application/json',
deviceId = '-',
deviceType = 0,
extra = {},
} = {}) {
const headers = {
accept: 'application/json, text/plain, */*',
...(contentType ? { 'content-type': contentType } : {}),
deviceid: String(deviceId || '-').trim() || '-',
devicetype: String(Number(deviceType || 0)),
...normalizeHeaderMap(extra),
}
const normalizedToken = String(token || '').trim()
if (normalizedToken) {
headers.authorization = normalizedToken
}
return headers
}
function normalizeBaseUrl(value) {
return String(value || '').trim().replace(/\/+$/, '')
}
function normalizePath(value, fallback) {
const normalized = String(value || '').trim()
if (!normalized) {
return fallback
}
return normalized.startsWith('/') ? normalized : `/${normalized}`
}
function normalizePositiveInteger(value, fallback) {
const parsed = Number(value)
return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback
}
function normalizeInteger(value, fallback) {
const parsed = Number(value)
return Number.isInteger(parsed) ? parsed : fallback
}
function normalizeHeaderMap(extra) {
if (!extra || typeof extra !== 'object') {
return {}
}
return Object.fromEntries(
Object.entries(extra)
.map(([key, value]) => [String(key || '').trim().toLowerCase(), String(value || '').trim()])
.filter(([key, value]) => key && value),
)
}
function normalizePem(value) {
return String(value || '')
.replace(/\r/g, '')
.split('\n')
.map((line) => line.trim())
.filter(Boolean)
.join('\n')
}
@@ -0,0 +1,71 @@
// @ts-check
import fs from 'node:fs'
import path from 'node:path'
import { PROJECT_ROOT } from '../../../config/runtime.js'
const CLOUDTENTACLES_SOURCES_FILE_PATH = path.join(PROJECT_ROOT, 'data', 'cloudtentacles-sources.json')
export function getCloudtentaclesSourcesFilePath() {
return CLOUDTENTACLES_SOURCES_FILE_PATH
}
export function getCloudtentaclesSourceConfig() {
return loadCloudtentaclesSourceConfigFromFile()
}
export function saveCloudtentaclesSourceConfig(rawValue) {
const normalized = normalizeCloudtentaclesSourceConfig(rawValue)
fs.mkdirSync(path.dirname(CLOUDTENTACLES_SOURCES_FILE_PATH), { recursive: true })
fs.writeFileSync(CLOUDTENTACLES_SOURCES_FILE_PATH, `${JSON.stringify(normalized, null, 2)}\n`, 'utf8')
return normalized
}
function loadCloudtentaclesSourceConfigFromFile() {
if (!fs.existsSync(CLOUDTENTACLES_SOURCES_FILE_PATH)) {
return createDefaultCloudtentaclesSourceConfig()
}
try {
const rawText = fs.readFileSync(CLOUDTENTACLES_SOURCES_FILE_PATH, 'utf8')
return normalizeCloudtentaclesSourceConfig(JSON.parse(rawText))
} catch {
return createDefaultCloudtentaclesSourceConfig()
}
}
function normalizeCloudtentaclesSourceConfig(rawValue) {
const source = isPlainObject(rawValue) ? rawValue : {}
return {
enabled: typeof source.enabled === 'boolean' ? source.enabled : true,
baseUrl: String(source.baseUrl || 'https://123.207.217.176').trim() || 'https://123.207.217.176',
username: String(source.username || '').trim(),
password: String(source.password || '').trim(),
phone: String(source.phone || '').trim(),
deviceId: String(source.deviceId || '-').trim() || '-',
deviceType: normalizeInteger(source.deviceType, 0),
}
}
function createDefaultCloudtentaclesSourceConfig() {
return {
enabled: true,
baseUrl: 'https://123.207.217.176',
username: '',
password: '',
phone: '',
deviceId: '-',
deviceType: 0,
}
}
function normalizeInteger(value, fallback) {
const parsed = Number(value)
return Number.isInteger(parsed) ? parsed : fallback
}
function isPlainObject(value) {
return Object.prototype.toString.call(value) === '[object Object]'
}
@@ -46,6 +46,22 @@ export {}
* @typedef {import('./admin-write-inputs.js').AdminKhhaoOrderQueryInput} AdminKhhaoOrderQueryRouteBody
*/
/**
* @typedef {import('./admin-write-inputs.js').AdminCloudtentaclesSourceConfigInput} AdminCloudtentaclesSourceConfigRouteBody
*/
/**
* @typedef {import('./admin-write-inputs.js').AdminCloudtentaclesSendSmsCodeInput} AdminCloudtentaclesSendSmsCodeRouteBody
*/
/**
* @typedef {import('./admin-write-inputs.js').AdminCloudtentaclesTestLoginInput} AdminCloudtentaclesTestLoginRouteBody
*/
/**
* @typedef {import('./admin-write-inputs.js').AdminCloudtentaclesValidateSessionInput} AdminCloudtentaclesValidateSessionRouteBody
*/
/**
* @typedef {import('./admin-write-inputs.js').AdminFulfillmentBindingConfigSaveInput} AdminFulfillmentBindingConfigRouteBody
*/
@@ -104,6 +104,49 @@ export {}
* }} AdminKhhaoOrderQueryInput
*/
/**
* @typedef {{
* enabled?: boolean
* baseUrl?: string
* username?: string
* password?: string
* phone?: string
* deviceId?: string
* deviceType?: number | string
* }} AdminCloudtentaclesSourceConfigInput
*/
/**
* @typedef {{
* baseUrl?: string
* username?: string
* phone?: string
* deviceId?: string
* deviceType?: number | string
* }} AdminCloudtentaclesSendSmsCodeInput
*/
/**
* @typedef {{
* baseUrl?: string
* username?: string
* password?: string
* phone?: string
* code?: string
* deviceId?: string
* deviceType?: number | string
* }} AdminCloudtentaclesTestLoginInput
*/
/**
* @typedef {{
* baseUrl?: string
* token?: string
* deviceId?: string
* deviceType?: number | string
* }} AdminCloudtentaclesValidateSessionInput
*/
/**
* @typedef {{
* provider?: string
+13
View File
@@ -96,6 +96,19 @@ export {}
* captchaPath: string
* orderListPath: string
* }
* cloudtentacles: {
* baseUrl: string
* timeoutMs: number
* sendSmsPath: string
* loginPath: string
* userInfoPath: string
* assetPath: string
* permissionPath: string
* publicKeyPem: string
* clientSource: string
* deviceId: string
* deviceType: number
* }
* }
* redeem: {
* proofMode: string
+49
View File
@@ -4,6 +4,10 @@ import type {
AdminAgisoObservedShopItem,
AdminAgisoShopConfigItem,
AdminAuditLogItem,
AdminCloudtentaclesLoginTestResult,
AdminCloudtentaclesSendSmsResult,
AdminCloudtentaclesSourceConfig,
AdminCloudtentaclesValidateSessionResult,
AdminFulfillmentLookupResult,
AdminFulfillmentBindingConfigItem,
AdminKhhaoLoginTestResult,
@@ -145,6 +149,51 @@ export function syncAdminKhhaoOrders(payload: {
return apiPost<AdminKhhaoOrderSyncResult>('/api/v1/admin/platform-config/khhao/sync-orders', payload)
}
export function fetchAdminCloudtentaclesSourceConfig() {
return apiGet<{
filePath: string
source: AdminCloudtentaclesSourceConfig
}>('/api/v1/admin/platform-config/cloudtentacles-source')
}
export function saveAdminCloudtentaclesSourceConfig(payload: AdminCloudtentaclesSourceConfig) {
return apiPost<{
filePath: string
source: AdminCloudtentaclesSourceConfig
}>('/api/v1/admin/platform-config/cloudtentacles-source', payload as unknown as Record<string, unknown>)
}
export function sendAdminCloudtentaclesSmsCode(payload: {
baseUrl?: string
username?: string
phone?: string
deviceId?: string
deviceType?: number
}) {
return apiPost<AdminCloudtentaclesSendSmsResult>('/api/v1/admin/platform-config/cloudtentacles/send-sms-code', payload)
}
export function testAdminCloudtentaclesLogin(payload: {
baseUrl?: string
username?: string
password?: string
phone?: string
code?: string
deviceId?: string
deviceType?: number
}) {
return apiPost<AdminCloudtentaclesLoginTestResult>('/api/v1/admin/platform-config/cloudtentacles/test-login', payload)
}
export function validateAdminCloudtentaclesSession(payload: {
baseUrl?: string
token?: string
deviceId?: string
deviceType?: number
}) {
return apiPost<AdminCloudtentaclesValidateSessionResult>('/api/v1/admin/platform-config/cloudtentacles/validate-session', payload)
}
export function fetchAdminFulfillmentBindingConfigs() {
return apiGet<{
filePath: string
+45
View File
@@ -175,6 +175,51 @@ export interface AdminKhhaoOrderSyncResult {
results: AdminKhhaoOrderSyncItem[]
}
export interface AdminCloudtentaclesSourceConfig {
enabled: boolean
baseUrl: string
username: string
password: string
phone: string
deviceId: string
deviceType: number
}
export interface AdminCloudtentaclesSendSmsResult {
baseUrl: string
username: string
phone: string
phoneMasked: string
sentAt: string
responseMessage: string
}
export interface AdminCloudtentaclesSessionSummary {
tokenMasked: string
permissionCount: number
permissions: string[]
}
export interface AdminCloudtentaclesLoginTestResult {
baseUrl: string
username: string
phoneMasked: string
loggedInAt: string
responseMessage: string
token: string
session: AdminCloudtentaclesSessionSummary
userInfo: Record<string, unknown>
asset: number
}
export interface AdminCloudtentaclesValidateSessionResult {
baseUrl: string
loggedInAt: string
session: AdminCloudtentaclesSessionSummary
userInfo: Record<string, unknown>
asset: number
}
export interface AdminFulfillmentBindingConfigItem {
provider: string
platform: string
@@ -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;
}