增加 cloudtentacles发货平台
This commit is contained in:
@@ -76,6 +76,27 @@ module.exports = {
|
|||||||
captchaPath: '/verify_img.php',
|
captchaPath: '/verify_img.php',
|
||||||
orderListPath: '/c/payOrder/get.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: {
|
redeem: {
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"enabled": true,
|
||||||
|
"baseUrl": "https://123.207.217.176",
|
||||||
|
"username": "17665234375",
|
||||||
|
"password": "yaochao11",
|
||||||
|
"phone": "17665234375",
|
||||||
|
"deviceId": "-",
|
||||||
|
"deviceType": 0
|
||||||
|
}
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
{
|
{
|
||||||
"syncFromCreatedAt": "2026-05-02T10:53:28.550Z",
|
"syncFromCreatedAt": "2026-05-02T10:53:28.550Z",
|
||||||
"lastRunStartedAt": "2026-05-02T12:36:55.519Z",
|
"lastRunStartedAt": "2026-05-02T13:40:21.932Z",
|
||||||
"lastRunFinishedAt": "2026-05-02T12:36:56.704Z",
|
"lastRunFinishedAt": "2026-05-02T13:40:23.162Z",
|
||||||
"lastRunStatus": "success",
|
"lastRunStatus": "success",
|
||||||
"lastErrorMessage": "",
|
"lastErrorMessage": "",
|
||||||
"fetchedCount": 40,
|
"fetchedCount": 40,
|
||||||
"syncedCount": 0,
|
"syncedCount": 0,
|
||||||
"ignoredCount": 40,
|
"ignoredCount": 40,
|
||||||
"lastOrderCreatedAt": "2026-05-02T12:36:45.000Z"
|
"lastOrderCreatedAt": "2026-05-02T13:39:40.000Z"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -260,6 +260,61 @@ function applyEnvOverrides(baseConfig) {
|
|||||||
nextConfig.platforms.khhao.orderListPath = khhaoOrderListPath
|
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()
|
const proofMode = String(process.env.TENCENT_REDEEM_PROOF_MODE || '').trim()
|
||||||
if (proofMode) {
|
if (proofMode) {
|
||||||
nextConfig.redeem.proofMode = proofMode
|
nextConfig.redeem.proofMode = proofMode
|
||||||
|
|||||||
@@ -3,16 +3,21 @@
|
|||||||
import { Router } from 'express'
|
import { Router } from 'express'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
getAdminCloudtentaclesSourceConfig,
|
||||||
getAdminAgisoShopConfigs,
|
getAdminAgisoShopConfigs,
|
||||||
getAdminFulfillmentBindingConfigs,
|
getAdminFulfillmentBindingConfigs,
|
||||||
getAdminKhhaoSourceConfig,
|
getAdminKhhaoSourceConfig,
|
||||||
lookupAdminFulfillmentBindingOrder,
|
lookupAdminFulfillmentBindingOrder,
|
||||||
queryAdminKhhaoOrders,
|
queryAdminKhhaoOrders,
|
||||||
|
sendAdminCloudtentaclesSmsCode,
|
||||||
syncAdminKhhaoOrders,
|
syncAdminKhhaoOrders,
|
||||||
|
testAdminCloudtentaclesLogin,
|
||||||
testAdminKhhaoLogin,
|
testAdminKhhaoLogin,
|
||||||
|
updateAdminCloudtentaclesSourceConfig,
|
||||||
updateAdminAgisoShopConfigs,
|
updateAdminAgisoShopConfigs,
|
||||||
updateAdminKhhaoSourceConfig,
|
updateAdminKhhaoSourceConfig,
|
||||||
updateAdminFulfillmentBindingConfigs,
|
updateAdminFulfillmentBindingConfigs,
|
||||||
|
validateAdminCloudtentaclesSession,
|
||||||
} from '../../services/admin/admin-platform-config-service.js'
|
} from '../../services/admin/admin-platform-config-service.js'
|
||||||
import { createJsonHandler, requireAdminRoles } from './shared.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').AdminKhhaoOrderQueryRouteBody} AdminKhhaoOrderQueryRouteBody */
|
||||||
/** @typedef {import('../../types/admin-route-inputs.js').AdminKhhaoSourceConfigRouteBody} AdminKhhaoSourceConfigRouteBody */
|
/** @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').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 */
|
/** @typedef {import('../../types/admin-write-models.js').AdminAgisoShopConfigSaveResponse} AdminAgisoShopConfigSaveResponse */
|
||||||
|
|
||||||
const router = Router()
|
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(
|
router.get('/platform-config/fulfillment-bindings', createJsonHandler(
|
||||||
() => getAdminFulfillmentBindingConfigs(),
|
() => 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 { getKhhaoSourceConfig, getKhhaoSourcesFilePath, saveKhhaoSourceConfig } from '../platforms/khhao/source-config-service.js'
|
||||||
import { ensureKhhaoSyncBaseline, getKhhaoSyncState, getKhhaoSyncStateFilePath } from '../platforms/khhao/sync-state-service.js'
|
import { ensureKhhaoSyncBaseline, getKhhaoSyncState, getKhhaoSyncStateFilePath } from '../platforms/khhao/sync-state-service.js'
|
||||||
import { loginKhhaoSession } from '../platforms/khhao/session-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 { normalizeAgisoMessageTemplate } from '../platforms/agiso/xianyu/message-service.js'
|
||||||
import {
|
import {
|
||||||
getOrderFulfillmentBindingConfigs,
|
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').AdminKhhaoSourceConfigInput} AdminKhhaoSourceConfigInput */
|
||||||
/** @typedef {import('../../types/admin-write-inputs.js').AdminKhhaoOrderQueryInput} AdminKhhaoOrderQueryInput */
|
/** @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').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').AdminFulfillmentBindingConfigSaveInput} AdminFulfillmentBindingConfigSaveInput */
|
||||||
/** @typedef {import('../../types/admin-write-inputs.js').AdminFulfillmentBindingLookupInput} AdminFulfillmentBindingLookupInput */
|
/** @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() {
|
export async function getAdminFulfillmentBindingConfigs() {
|
||||||
const bindings = getOrderFulfillmentBindingConfigs()
|
const bindings = getOrderFulfillmentBindingConfigs()
|
||||||
const rowsResult = await query(
|
const rowsResult = await query(
|
||||||
@@ -685,6 +812,19 @@ function maskSecret(value) {
|
|||||||
return `${normalized.slice(0, 6)}****${normalized.slice(-6)}`
|
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) {
|
function isPlainObject(value) {
|
||||||
return Boolean(value) && typeof value === 'object' && !Array.isArray(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').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
|
* @typedef {import('./admin-write-inputs.js').AdminFulfillmentBindingConfigSaveInput} AdminFulfillmentBindingConfigRouteBody
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -104,6 +104,49 @@ export {}
|
|||||||
* }} AdminKhhaoOrderQueryInput
|
* }} 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 {{
|
* @typedef {{
|
||||||
* provider?: string
|
* provider?: string
|
||||||
|
|||||||
@@ -96,6 +96,19 @@ export {}
|
|||||||
* captchaPath: string
|
* captchaPath: string
|
||||||
* orderListPath: 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: {
|
* redeem: {
|
||||||
* proofMode: string
|
* proofMode: string
|
||||||
|
|||||||
@@ -4,6 +4,10 @@ import type {
|
|||||||
AdminAgisoObservedShopItem,
|
AdminAgisoObservedShopItem,
|
||||||
AdminAgisoShopConfigItem,
|
AdminAgisoShopConfigItem,
|
||||||
AdminAuditLogItem,
|
AdminAuditLogItem,
|
||||||
|
AdminCloudtentaclesLoginTestResult,
|
||||||
|
AdminCloudtentaclesSendSmsResult,
|
||||||
|
AdminCloudtentaclesSourceConfig,
|
||||||
|
AdminCloudtentaclesValidateSessionResult,
|
||||||
AdminFulfillmentLookupResult,
|
AdminFulfillmentLookupResult,
|
||||||
AdminFulfillmentBindingConfigItem,
|
AdminFulfillmentBindingConfigItem,
|
||||||
AdminKhhaoLoginTestResult,
|
AdminKhhaoLoginTestResult,
|
||||||
@@ -145,6 +149,51 @@ export function syncAdminKhhaoOrders(payload: {
|
|||||||
return apiPost<AdminKhhaoOrderSyncResult>('/api/v1/admin/platform-config/khhao/sync-orders', 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() {
|
export function fetchAdminFulfillmentBindingConfigs() {
|
||||||
return apiGet<{
|
return apiGet<{
|
||||||
filePath: string
|
filePath: string
|
||||||
|
|||||||
@@ -175,6 +175,51 @@ export interface AdminKhhaoOrderSyncResult {
|
|||||||
results: AdminKhhaoOrderSyncItem[]
|
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 {
|
export interface AdminFulfillmentBindingConfigItem {
|
||||||
provider: string
|
provider: string
|
||||||
platform: string
|
platform: string
|
||||||
|
|||||||
@@ -4,17 +4,26 @@ import { computed, onMounted, ref } from 'vue'
|
|||||||
import { showError, showSuccess } from '@/lib/feedback'
|
import { showError, showSuccess } from '@/lib/feedback'
|
||||||
import {
|
import {
|
||||||
fetchAdminAgisoShopConfigs,
|
fetchAdminAgisoShopConfigs,
|
||||||
|
fetchAdminCloudtentaclesSourceConfig,
|
||||||
fetchAdminKhhaoSourceConfig,
|
fetchAdminKhhaoSourceConfig,
|
||||||
queryAdminKhhaoOrders,
|
queryAdminKhhaoOrders,
|
||||||
|
saveAdminCloudtentaclesSourceConfig,
|
||||||
saveAdminAgisoShopConfigs,
|
saveAdminAgisoShopConfigs,
|
||||||
saveAdminKhhaoSourceConfig,
|
saveAdminKhhaoSourceConfig,
|
||||||
|
sendAdminCloudtentaclesSmsCode,
|
||||||
syncAdminKhhaoOrders,
|
syncAdminKhhaoOrders,
|
||||||
|
testAdminCloudtentaclesLogin,
|
||||||
testAdminKhhaoLogin,
|
testAdminKhhaoLogin,
|
||||||
|
validateAdminCloudtentaclesSession,
|
||||||
} from '@/services/admin'
|
} from '@/services/admin'
|
||||||
import type {
|
import type {
|
||||||
AdminAgisoMessagingDefaults,
|
AdminAgisoMessagingDefaults,
|
||||||
AdminAgisoObservedShopItem,
|
AdminAgisoObservedShopItem,
|
||||||
AdminAgisoShopConfigItem,
|
AdminAgisoShopConfigItem,
|
||||||
|
AdminCloudtentaclesLoginTestResult,
|
||||||
|
AdminCloudtentaclesSendSmsResult,
|
||||||
|
AdminCloudtentaclesSourceConfig,
|
||||||
|
AdminCloudtentaclesValidateSessionResult,
|
||||||
AdminKhhaoLoginTestResult,
|
AdminKhhaoLoginTestResult,
|
||||||
AdminKhhaoOrderQueryResult,
|
AdminKhhaoOrderQueryResult,
|
||||||
AdminKhhaoSyncState,
|
AdminKhhaoSyncState,
|
||||||
@@ -23,7 +32,7 @@ import type {
|
|||||||
import { hasAdminRole } from '@/utils/admin-auth'
|
import { hasAdminRole } from '@/utils/admin-auth'
|
||||||
import { formatAdminDateTime } from '@/utils/admin-time'
|
import { formatAdminDateTime } from '@/utils/admin-time'
|
||||||
|
|
||||||
type PlatformTab = 'agiso' | 'khhao'
|
type PlatformTab = 'agiso' | 'khhao' | 'cloudtentacles'
|
||||||
|
|
||||||
type EditableDefaults = {
|
type EditableDefaults = {
|
||||||
messageTemplate: string
|
messageTemplate: string
|
||||||
@@ -43,12 +52,14 @@ type EditableShop = {
|
|||||||
const loading = ref(true)
|
const loading = ref(true)
|
||||||
const saving = ref(false)
|
const saving = ref(false)
|
||||||
const khhaoSaving = ref(false)
|
const khhaoSaving = ref(false)
|
||||||
|
const cloudtentaclesSaving = ref(false)
|
||||||
const errorMessage = ref('')
|
const errorMessage = ref('')
|
||||||
const activePlatform = ref<PlatformTab>('agiso')
|
const activePlatform = ref<PlatformTab>('agiso')
|
||||||
|
|
||||||
const agisoFilePath = ref('')
|
const agisoFilePath = ref('')
|
||||||
const khhaoFilePath = ref('')
|
const khhaoFilePath = ref('')
|
||||||
const khhaoStateFilePath = ref('')
|
const khhaoStateFilePath = ref('')
|
||||||
|
const cloudtentaclesFilePath = ref('')
|
||||||
const defaults = ref<EditableDefaults>(createEmptyDefaults())
|
const defaults = ref<EditableDefaults>(createEmptyDefaults())
|
||||||
const shops = ref<EditableShop[]>([])
|
const shops = ref<EditableShop[]>([])
|
||||||
const observedShops = ref<AdminAgisoObservedShopItem[]>([])
|
const observedShops = ref<AdminAgisoObservedShopItem[]>([])
|
||||||
@@ -74,6 +85,23 @@ const khhaoLoginResult = ref<AdminKhhaoLoginTestResult | null>(null)
|
|||||||
const khhaoQueryResult = ref<AdminKhhaoOrderQueryResult | null>(null)
|
const khhaoQueryResult = ref<AdminKhhaoOrderQueryResult | null>(null)
|
||||||
const khhaoSyncResult = ref<AdminKhhaoOrderSyncResult | null>(null)
|
const khhaoSyncResult = ref<AdminKhhaoOrderSyncResult | null>(null)
|
||||||
const khhaoAutoSyncState = ref<AdminKhhaoSyncState | 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(() => ({
|
const agisoStats = computed(() => ({
|
||||||
configuredShopCount: shops.value.length,
|
configuredShopCount: shops.value.length,
|
||||||
@@ -88,6 +116,16 @@ const khhaoStats = computed(() => ({
|
|||||||
ignoredCount: khhaoAutoSyncState.value?.ignoredCount || khhaoSyncResult.value?.ignoredCount || 0,
|
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 {
|
function createEmptyDefaults(): EditableDefaults {
|
||||||
return {
|
return {
|
||||||
messageTemplate: '',
|
messageTemplate: '',
|
||||||
@@ -136,9 +174,10 @@ async function loadConfigs() {
|
|||||||
errorMessage.value = ''
|
errorMessage.value = ''
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const [agisoResponse, khhaoResponse] = await Promise.all([
|
const [agisoResponse, khhaoResponse, cloudtentaclesResponse] = await Promise.all([
|
||||||
fetchAdminAgisoShopConfigs(),
|
fetchAdminAgisoShopConfigs(),
|
||||||
fetchAdminKhhaoSourceConfig(),
|
fetchAdminKhhaoSourceConfig(),
|
||||||
|
fetchAdminCloudtentaclesSourceConfig(),
|
||||||
])
|
])
|
||||||
|
|
||||||
agisoFilePath.value = agisoResponse.data.filePath
|
agisoFilePath.value = agisoResponse.data.filePath
|
||||||
@@ -162,6 +201,18 @@ async function loadConfigs() {
|
|||||||
autoSyncPages: khhaoResponse.data.source.autoSync?.pages || 2,
|
autoSyncPages: khhaoResponse.data.source.autoSync?.pages || 2,
|
||||||
autoSyncPageSize: khhaoResponse.data.source.autoSync?.pageSize || 20,
|
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) {
|
} catch (error) {
|
||||||
errorMessage.value = error instanceof Error ? error.message : '读取平台配置失败'
|
errorMessage.value = error instanceof Error ? error.message : '读取平台配置失败'
|
||||||
} finally {
|
} 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)
|
onMounted(loadConfigs)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -397,7 +577,7 @@ onMounted(loadConfigs)
|
|||||||
<header class="hero-card">
|
<header class="hero-card">
|
||||||
<div class="hero-copy">
|
<div class="hero-copy">
|
||||||
<h1>平台配置</h1>
|
<h1>平台配置</h1>
|
||||||
<p>按平台分别管理来源接入、凭据与店铺能力。Agiso 负责 webhook / 店铺消息配置,khhao 负责账号登录拉单与同步。</p>
|
<p>按平台分别管理来源接入、凭据与店铺能力。Agiso 负责 webhook / 店铺消息配置,khhao 负责来源拉单,cloudtentacles 负责外部履约平台登录。</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="hero-actions">
|
<div class="hero-actions">
|
||||||
<el-button round @click="loadConfigs">刷新全部配置</el-button>
|
<el-button round @click="loadConfigs">刷新全部配置</el-button>
|
||||||
@@ -446,6 +626,23 @@ onMounted(loadConfigs)
|
|||||||
<span>最近同步成功 {{ khhaoStats.syncedCount }} 条</span>
|
<span>最近同步成功 {{ khhaoStats.syncedCount }} 条</span>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</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>
|
||||||
|
|
||||||
<section class="platform-switcher">
|
<section class="platform-switcher">
|
||||||
@@ -463,6 +660,13 @@ onMounted(loadConfigs)
|
|||||||
>
|
>
|
||||||
khhao 来源与同步
|
khhao 来源与同步
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
:class="['switch-chip', { 'is-active': activePlatform === 'cloudtentacles' }]"
|
||||||
|
@click="switchPlatform('cloudtentacles')"
|
||||||
|
>
|
||||||
|
cloudtentacles 履约平台
|
||||||
|
</button>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<template v-if="activePlatform === 'agiso'">
|
<template v-if="activePlatform === 'agiso'">
|
||||||
@@ -625,7 +829,7 @@ onMounted(loadConfigs)
|
|||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template v-else>
|
<template v-else-if="activePlatform === 'khhao'">
|
||||||
<section class="meta-card">
|
<section class="meta-card">
|
||||||
<div class="meta-line">
|
<div class="meta-line">
|
||||||
<span class="meta-label">配置文件</span>
|
<span class="meta-label">配置文件</span>
|
||||||
@@ -842,6 +1046,136 @@ onMounted(loadConfigs)
|
|||||||
</table>
|
</table>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</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>
|
||||||
</template>
|
</template>
|
||||||
</section>
|
</section>
|
||||||
@@ -906,7 +1240,7 @@ onMounted(loadConfigs)
|
|||||||
|
|
||||||
.overview-grid {
|
.overview-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -75,3 +75,10 @@ https://221329.cc.cd/api/v1/webhooks/agiso/trade
|
|||||||
服务器 https://order.khhao.com/api/v1/webhooks/agiso/trade
|
服务器 https://order.khhao.com/api/v1/webhooks/agiso/trade
|
||||||
|
|
||||||
调试 https://926d-193-176-84-20.ngrok-free.app/api/v1/webhooks/agiso/trade
|
调试 https://926d-193-176-84-20.ngrok-free.app/api/v1/webhooks/agiso/trade
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
==========
|
||||||
|
https://123.207.217.176/#/home 发货
|
||||||
|
账号 17665234375 密码 yaochao11
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,733 @@
|
|||||||
|
# cloudtentacles 外部履约平台接入设计
|
||||||
|
|
||||||
|
## 1. 背景
|
||||||
|
|
||||||
|
当前项目里现有链路大致分两层:
|
||||||
|
|
||||||
|
- **订单来源层**
|
||||||
|
- `agiso`:Webhook 推送型;
|
||||||
|
- `khhao`:账号密码登录 + 验证码识别 + Cookie 会话拉取型。
|
||||||
|
- **履约执行层**
|
||||||
|
- 当前以内置人工发货、腾讯领取兑换等执行器为主。
|
||||||
|
|
||||||
|
现在新增一个发货相关的平台,当前已知入口为:
|
||||||
|
|
||||||
|
- `https://123.207.217.176`
|
||||||
|
- 前端资源来自 `gp.playinjoy.com`
|
||||||
|
- 前端代码里根实例名显示为 `cloudtentacles`
|
||||||
|
|
||||||
|
这个平台**不是订单数据源**,而是**订单创建后用于执行发货/兑换的外部履约平台**。
|
||||||
|
它和 `khhao` 的相同点只是“都需要后台登录”,但系统角色完全不同:
|
||||||
|
|
||||||
|
- `khhao`:订单来源 / 上游平台;
|
||||||
|
- `cloudtentacles`:履约执行 / 下游发货平台。
|
||||||
|
|
||||||
|
它的登录链路与 `khhao` 明显不同:
|
||||||
|
|
||||||
|
- 不是图片验证码识别;
|
||||||
|
- 是“账号 + 密码 + 手机号 + 短信验证码”登录;
|
||||||
|
- 登录参数不是明文提交,而是前端先做 `MD5 + RSA-OAEP(SHA-256)` 加密;
|
||||||
|
- 登录成功后不是 Cookie 会话,而是返回 `token`,后续通过 `Authorization` 请求头访问接口。
|
||||||
|
|
||||||
|
因此它应当作为**履约执行器能力**接入,复用 `khhao` 的“HTTP 登录平台”经验,但不要按“来源平台”建模。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 当前已确认事实
|
||||||
|
|
||||||
|
### 2.1 证据来源
|
||||||
|
|
||||||
|
- HAR:`/Users/yml/Desktop/抓包/123.207.217.176.har`
|
||||||
|
- 前端代码:`tems/test1.js`
|
||||||
|
- 打包代码:`tems/index.93fe093c.js`
|
||||||
|
|
||||||
|
### 2.2 已确认接口链路
|
||||||
|
|
||||||
|
1. 发送短信验证码
|
||||||
|
`POST https://123.207.217.176/public/verif_code`
|
||||||
|
|
||||||
|
2. 提交登录
|
||||||
|
`POST https://123.207.217.176/public/login`
|
||||||
|
|
||||||
|
3. 登录后探活接口
|
||||||
|
- `POST /user/info`
|
||||||
|
- `GET /user/get_asset`
|
||||||
|
- `GET /user/get_permission`
|
||||||
|
|
||||||
|
4. 当前 HAR 里还看到的业务接口
|
||||||
|
- `GET /categories/get`
|
||||||
|
- `GET /sku/list`
|
||||||
|
|
||||||
|
### 2.3 登录态机制
|
||||||
|
|
||||||
|
不是依赖 Cookie。
|
||||||
|
|
||||||
|
前端在登录成功后会把 `/public/login` 返回的 `data` 当成 token 保存,并在后续请求里自动带上:
|
||||||
|
|
||||||
|
```http
|
||||||
|
Authorization: <token>
|
||||||
|
deviceid: -
|
||||||
|
devicetype: 0
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.4 登录参数的真实结构
|
||||||
|
|
||||||
|
根据 `tems/test1.js`,发验证码与登录都不是直接提交原始字段,而是先调用 `encryptWithPublicKey(...)`。
|
||||||
|
|
||||||
|
#### 发验证码原始参数
|
||||||
|
|
||||||
|
```js
|
||||||
|
{
|
||||||
|
account: username,
|
||||||
|
phone: phone
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 登录原始参数
|
||||||
|
|
||||||
|
```js
|
||||||
|
{
|
||||||
|
account: username,
|
||||||
|
password: MD5(password),
|
||||||
|
phone: phone,
|
||||||
|
code: smsCode
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 加密前最终明文
|
||||||
|
|
||||||
|
```js
|
||||||
|
{
|
||||||
|
t: Date.now(),
|
||||||
|
r: Math.random().toString(16),
|
||||||
|
s: 'ct-client',
|
||||||
|
...payload
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
然后:
|
||||||
|
|
||||||
|
1. `JSON.stringify`
|
||||||
|
2. 使用 RSA-OAEP + SHA-256 公钥加密
|
||||||
|
3. Base64 编码,作为 `covert`
|
||||||
|
|
||||||
|
最终请求体:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"covert": "<base64>",
|
||||||
|
"t": 1777726021956,
|
||||||
|
"r": "0.8601863c152d6"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.5 密码处理
|
||||||
|
|
||||||
|
密码不是明文传输。
|
||||||
|
|
||||||
|
前端代码中:
|
||||||
|
|
||||||
|
```js
|
||||||
|
password: cu(password)
|
||||||
|
```
|
||||||
|
|
||||||
|
而 `cu` 实际是:
|
||||||
|
|
||||||
|
```js
|
||||||
|
MD5(password).toString(Hex)
|
||||||
|
```
|
||||||
|
|
||||||
|
即:
|
||||||
|
|
||||||
|
- 先 `MD5`
|
||||||
|
- 再参与 RSA 加密
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 设计目标
|
||||||
|
|
||||||
|
## 3.1 第一阶段目标
|
||||||
|
|
||||||
|
这一版只做“基础登录能力”,不直接做完整发货自动化:
|
||||||
|
|
||||||
|
1. 能发送短信验证码;
|
||||||
|
2. 能使用账号、密码、手机号、短信验证码自动登录;
|
||||||
|
3. 能持久化保存该平台的履约配置;
|
||||||
|
4. 能在后台手动测试登录;
|
||||||
|
5. 能在登录后调用一个轻量探活接口确认登录成功;
|
||||||
|
6. 输出统一的会话对象,给后续“SKU 查询 / 背包读取 / 发货执行”复用。
|
||||||
|
|
||||||
|
## 3.2 第二阶段目标
|
||||||
|
|
||||||
|
后续逐步补:
|
||||||
|
|
||||||
|
1. token 缓存与自动续登;
|
||||||
|
2. SKU/分类/背包读取;
|
||||||
|
3. 发货动作封装;
|
||||||
|
4. 发货结果记录与截图/证据保存;
|
||||||
|
5. 接成新的 `executor_key`,纳入现有履约任务执行链路;
|
||||||
|
6. 由“订单支付成功 / 任务就绪”触发自动发货。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 关键设计决策
|
||||||
|
|
||||||
|
## 4.1 领域定位
|
||||||
|
|
||||||
|
这里不应该新增 `provider = 'cloudtentacles'` 作为订单来源标识。
|
||||||
|
|
||||||
|
更合理的定位是:
|
||||||
|
|
||||||
|
- 订单来源 `provider/platform/shopId` 仍然来自 `agiso` / `khhao`
|
||||||
|
- `cloudtentacles` 属于**履约执行器 / 外部发货平台**
|
||||||
|
|
||||||
|
因此它更适合进入这条链路:
|
||||||
|
|
||||||
|
- `fulfillment_profiles.profile_key`
|
||||||
|
- `fulfillment_profiles.executor_key`
|
||||||
|
- 任务执行服务
|
||||||
|
|
||||||
|
建议后续新增类似:
|
||||||
|
|
||||||
|
- `profileKey = 'cloudtentacles_dispatch'`
|
||||||
|
- `executorKey = 'cloudtentacles_dispatch'`
|
||||||
|
|
||||||
|
而不是把它混进订单来源层。
|
||||||
|
|
||||||
|
## 4.2 接入模式
|
||||||
|
|
||||||
|
这个平台本质上是 **外部履约平台 + token 会话** 模式。
|
||||||
|
|
||||||
|
因此结构上可以先放在 `platforms/` 下管理其登录与 API 封装,但业务接入点要落到**履约执行层**:
|
||||||
|
|
||||||
|
- 配置服务
|
||||||
|
- 加密服务
|
||||||
|
- HTTP 客户端
|
||||||
|
- 登录/会话服务
|
||||||
|
- 发货业务服务
|
||||||
|
- 履约执行适配层
|
||||||
|
|
||||||
|
## 4.3 不建议直接照抄 khhao 的原因
|
||||||
|
|
||||||
|
`khhao` 当前链路核心是:
|
||||||
|
|
||||||
|
- 打开登录页
|
||||||
|
- 拉验证码图片
|
||||||
|
- OCR
|
||||||
|
- 表单提交
|
||||||
|
- Cookie 维持会话
|
||||||
|
|
||||||
|
而当前平台核心是:
|
||||||
|
|
||||||
|
- 组装明文
|
||||||
|
- MD5 密码
|
||||||
|
- RSA 公钥加密
|
||||||
|
- 请求登录接口
|
||||||
|
- token 维持会话
|
||||||
|
|
||||||
|
所以如果把所有逻辑都塞进一个 `session-service.js`,后面会越来越乱。
|
||||||
|
更合理的方式是把“加密”、“请求头”、“token 管理”单独拆开。
|
||||||
|
|
||||||
|
## 4.4 第一版不引入浏览器自动化
|
||||||
|
|
||||||
|
当前证据显示不需要浏览器:
|
||||||
|
|
||||||
|
- HAR 中登录接口是纯 XHR;
|
||||||
|
- 参数生成逻辑已经从 JS 中还原;
|
||||||
|
- token 机制清晰;
|
||||||
|
- 没看到依赖浏览器页面上下文的动态签名。
|
||||||
|
|
||||||
|
因此第一版应坚持 **纯 HTTP 实现**。
|
||||||
|
|
||||||
|
只有后续遇到这些问题时,才考虑浏览器方案:
|
||||||
|
|
||||||
|
- 服务端增加不可还原的前端签名;
|
||||||
|
- 短信验证码流程强依赖页面态;
|
||||||
|
- 登录成功但服务端对业务接口做浏览器环境校验。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 推荐目录结构
|
||||||
|
|
||||||
|
建议新增目录:
|
||||||
|
|
||||||
|
`apps/backend/src/services/platforms/cloudtentacles/`
|
||||||
|
|
||||||
|
同时第二阶段补一个执行适配入口,例如:
|
||||||
|
|
||||||
|
`apps/backend/src/services/fulfillment/cloudtentacles-dispatch-service.js`
|
||||||
|
|
||||||
|
### 5.1 第一阶段实际落地文件
|
||||||
|
|
||||||
|
#### `shared.js`
|
||||||
|
|
||||||
|
职责:
|
||||||
|
|
||||||
|
- 读取运行时配置;
|
||||||
|
- 规范化 baseUrl / 路径 / timeout;
|
||||||
|
- 构建 URL;
|
||||||
|
- 构建公共请求头;
|
||||||
|
- 统一设备头默认值。
|
||||||
|
|
||||||
|
建议内容:
|
||||||
|
|
||||||
|
- `resolveCloudtentaclesConfig(overrides)`
|
||||||
|
- `buildCloudtentaclesUrl(baseUrl, pathname, searchParams?)`
|
||||||
|
- `buildCloudtentaclesHeaders({ token?, contentType? })`
|
||||||
|
|
||||||
|
#### `crypto-service.js`
|
||||||
|
|
||||||
|
职责:
|
||||||
|
|
||||||
|
- 密码 MD5;
|
||||||
|
- 公钥 PEM 转 ArrayBuffer / Buffer;
|
||||||
|
- 组装 `t/r/s` 明文;
|
||||||
|
- 执行 RSA-OAEP(SHA-256) 加密;
|
||||||
|
- 返回 `{ covert, t, r }`
|
||||||
|
|
||||||
|
建议暴露:
|
||||||
|
|
||||||
|
- `md5CloudtentaclesPassword(password)`
|
||||||
|
- `encryptCloudtentaclesPayload(payload, options?)`
|
||||||
|
|
||||||
|
这样后续不仅登录能用,发验证码、改密等其它加密接口也都能复用。
|
||||||
|
|
||||||
|
#### `http-client.js`
|
||||||
|
|
||||||
|
职责:
|
||||||
|
|
||||||
|
- 对 `fetch` 做一层轻封装;
|
||||||
|
- 统一 timeout;
|
||||||
|
- 自动带 `Authorization / deviceid / devicetype`;
|
||||||
|
- 统一解析平台响应格式;
|
||||||
|
- 统一抛出带平台上下文的错误。
|
||||||
|
|
||||||
|
建议暴露:
|
||||||
|
|
||||||
|
- `cloudtentaclesRequest(path, options)`
|
||||||
|
|
||||||
|
注意:第一版不用做成全局通用 HTTP 框架,只服务这个平台即可。
|
||||||
|
|
||||||
|
#### `session-service.js`
|
||||||
|
|
||||||
|
职责:
|
||||||
|
|
||||||
|
- 发送短信验证码;
|
||||||
|
- 使用短信验证码登录;
|
||||||
|
- 登录后调用 `/user/info` 验证会话;
|
||||||
|
- 返回标准会话对象。
|
||||||
|
|
||||||
|
建议暴露:
|
||||||
|
|
||||||
|
- `sendCloudtentaclesSmsCode(payload)`
|
||||||
|
- `loginCloudtentaclesSession(payload)`
|
||||||
|
- `validateCloudtentaclesSession(payload)`
|
||||||
|
|
||||||
|
建议会话对象:
|
||||||
|
|
||||||
|
```js
|
||||||
|
{
|
||||||
|
baseUrl,
|
||||||
|
token,
|
||||||
|
loggedInAt,
|
||||||
|
username,
|
||||||
|
phone,
|
||||||
|
userInfo,
|
||||||
|
permissions
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `source-config-service.js`
|
||||||
|
|
||||||
|
职责:
|
||||||
|
|
||||||
|
- 读写履约平台配置文件;
|
||||||
|
- 管理默认账号、密码、手机号;
|
||||||
|
- 管理后续自动化开关预留字段。
|
||||||
|
|
||||||
|
建议文件:
|
||||||
|
|
||||||
|
`apps/backend/data/cloudtentacles-sources.json`
|
||||||
|
|
||||||
|
建议结构:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"enabled": true,
|
||||||
|
"baseUrl": "https://123.207.217.176",
|
||||||
|
"username": "",
|
||||||
|
"password": "",
|
||||||
|
"phone": "",
|
||||||
|
"deviceId": "-",
|
||||||
|
"deviceType": 0
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.2 第二阶段预留文件
|
||||||
|
|
||||||
|
这些先不一定创建实现,但目录命名先按这个方向约束:
|
||||||
|
|
||||||
|
#### `catalog-service.js`
|
||||||
|
|
||||||
|
- 分类
|
||||||
|
- SKU
|
||||||
|
- 背包
|
||||||
|
- 库存类读取
|
||||||
|
|
||||||
|
#### `delivery-service.js`
|
||||||
|
|
||||||
|
- 发货动作
|
||||||
|
- CDK / 虚拟物品使用
|
||||||
|
- 回滚 / 失败重试
|
||||||
|
|
||||||
|
#### `record-service.js`
|
||||||
|
|
||||||
|
- 交易记录
|
||||||
|
- 发货记录
|
||||||
|
- 订单对账
|
||||||
|
|
||||||
|
#### `auto-sync-service.js`
|
||||||
|
|
||||||
|
- 定时探活
|
||||||
|
- 自动续登
|
||||||
|
- 预热会话
|
||||||
|
|
||||||
|
#### `apps/backend/src/services/fulfillment/cloudtentacles-dispatch-service.js`
|
||||||
|
|
||||||
|
职责:
|
||||||
|
|
||||||
|
- 接收履约任务;
|
||||||
|
- 读取任务绑定的库存 / 凭据;
|
||||||
|
- 调用 `platforms/cloudtentacles/*` 完成实际发货;
|
||||||
|
- 回写任务状态、发货结果、证据。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 配置设计
|
||||||
|
|
||||||
|
## 6.1 运行时配置
|
||||||
|
|
||||||
|
建议在:
|
||||||
|
|
||||||
|
- `apps/backend/config/default.cjs`
|
||||||
|
- `apps/backend/src/types/runtime-config.js`
|
||||||
|
- `apps/backend/src/config/runtime.js`
|
||||||
|
|
||||||
|
增加:
|
||||||
|
|
||||||
|
```js
|
||||||
|
platforms: {
|
||||||
|
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',
|
||||||
|
publicKeyPem: '-----BEGIN PUBLIC KEY----- ... -----END PUBLIC KEY-----',
|
||||||
|
clientSource: 'ct-client',
|
||||||
|
deviceId: '-',
|
||||||
|
deviceType: 0,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 设计说明
|
||||||
|
|
||||||
|
- `publicKeyPem` 放运行时配置,而不是硬编码到 service;
|
||||||
|
- `clientSource` 默认是 `ct-client`;
|
||||||
|
- `deviceId / deviceType` 先允许配置,避免后面服务端开始校验时需要改代码。
|
||||||
|
|
||||||
|
## 6.2 持久化来源配置
|
||||||
|
|
||||||
|
建议和 `khhao` 一样,先用数据文件落地,不急着进数据库。
|
||||||
|
|
||||||
|
建议文件:
|
||||||
|
|
||||||
|
`apps/backend/data/cloudtentacles-sources.json`
|
||||||
|
|
||||||
|
建议第一版结构:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"enabled": true,
|
||||||
|
"baseUrl": "https://123.207.217.176",
|
||||||
|
"username": "",
|
||||||
|
"password": "",
|
||||||
|
"phone": "",
|
||||||
|
"deviceId": "-",
|
||||||
|
"deviceType": 0
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
后续如果要支持多套账号,再演进为:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"sources": [
|
||||||
|
{
|
||||||
|
"sourceKey": "cloudtentacles-main",
|
||||||
|
"enabled": true,
|
||||||
|
"baseUrl": "https://123.207.217.176",
|
||||||
|
"username": "",
|
||||||
|
"password": "",
|
||||||
|
"phone": "",
|
||||||
|
"deviceId": "-",
|
||||||
|
"deviceType": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
第一版先单来源,能明显降低 UI 和逻辑复杂度。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. API 设计建议
|
||||||
|
|
||||||
|
建议先在管理后台补一组最小接口:
|
||||||
|
|
||||||
|
### 7.1 配置读取
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /api/v1/admin/platform-config/cloudtentacles-source
|
||||||
|
```
|
||||||
|
|
||||||
|
返回:
|
||||||
|
|
||||||
|
- 配置文件路径
|
||||||
|
- 当前履约平台配置
|
||||||
|
|
||||||
|
### 7.2 配置保存
|
||||||
|
|
||||||
|
```http
|
||||||
|
POST /api/v1/admin/platform-config/cloudtentacles-source
|
||||||
|
```
|
||||||
|
|
||||||
|
用于保存:
|
||||||
|
|
||||||
|
- 账号
|
||||||
|
- 密码
|
||||||
|
- 手机号
|
||||||
|
- baseUrl
|
||||||
|
- 设备头默认值
|
||||||
|
|
||||||
|
### 7.3 发送短信验证码
|
||||||
|
|
||||||
|
```http
|
||||||
|
POST /api/v1/admin/platforms/cloudtentacles/send-sms-code
|
||||||
|
```
|
||||||
|
|
||||||
|
入参支持:
|
||||||
|
|
||||||
|
- 使用已保存配置;
|
||||||
|
- 或临时覆盖账号/手机号/baseUrl。
|
||||||
|
|
||||||
|
### 7.4 手动登录测试
|
||||||
|
|
||||||
|
```http
|
||||||
|
POST /api/v1/admin/platforms/cloudtentacles/login
|
||||||
|
```
|
||||||
|
|
||||||
|
入参:
|
||||||
|
|
||||||
|
- `username`
|
||||||
|
- `password`
|
||||||
|
- `phone`
|
||||||
|
- `code`
|
||||||
|
|
||||||
|
返回:
|
||||||
|
|
||||||
|
- `token`
|
||||||
|
- `userInfo`
|
||||||
|
- `permissions`
|
||||||
|
- `loggedInAt`
|
||||||
|
|
||||||
|
### 7.5 会话探活测试
|
||||||
|
|
||||||
|
```http
|
||||||
|
POST /api/v1/admin/platforms/cloudtentacles/validate-session
|
||||||
|
```
|
||||||
|
|
||||||
|
入参:
|
||||||
|
|
||||||
|
- `token`
|
||||||
|
|
||||||
|
返回:
|
||||||
|
|
||||||
|
- 是否有效
|
||||||
|
- 用户信息
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 后台 UI 设计建议
|
||||||
|
|
||||||
|
第一版 UI 不需要太复杂,但应与 `khhao` 分开,避免把“订单来源配置”和“履约平台配置”混在一起。
|
||||||
|
|
||||||
|
建议在“平台配置”页新增独立区块,归类到“履约平台”分组:
|
||||||
|
|
||||||
|
### 8.1 履约平台配置区
|
||||||
|
|
||||||
|
字段:
|
||||||
|
|
||||||
|
- 启用状态
|
||||||
|
- baseUrl
|
||||||
|
- 账号
|
||||||
|
- 密码
|
||||||
|
- 手机号
|
||||||
|
- deviceId
|
||||||
|
- deviceType
|
||||||
|
|
||||||
|
操作:
|
||||||
|
|
||||||
|
- 保存配置
|
||||||
|
|
||||||
|
### 8.2 登录测试区
|
||||||
|
|
||||||
|
字段:
|
||||||
|
|
||||||
|
- 短信验证码输入框
|
||||||
|
|
||||||
|
操作:
|
||||||
|
|
||||||
|
- 发送验证码
|
||||||
|
- 执行登录
|
||||||
|
- 验证当前 token
|
||||||
|
|
||||||
|
### 8.3 调试结果区
|
||||||
|
|
||||||
|
显示:
|
||||||
|
|
||||||
|
- token 是否返回
|
||||||
|
- userInfo
|
||||||
|
- permissions
|
||||||
|
- 最近一次错误消息
|
||||||
|
|
||||||
|
注意:
|
||||||
|
|
||||||
|
- 不要把这个平台和 `khhao` 放在同一张混合表单里;
|
||||||
|
- 页面上要明确区分:
|
||||||
|
- 订单来源平台
|
||||||
|
- 履约执行平台
|
||||||
|
- 后面平台越来越多时,这个结构才能撑住。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. 错误处理与日志建议
|
||||||
|
|
||||||
|
建议统一给这个平台加明确日志前缀:
|
||||||
|
|
||||||
|
- `[cloudtentacles/crypto]`
|
||||||
|
- `[cloudtentacles/session]`
|
||||||
|
- `[cloudtentacles/http]`
|
||||||
|
|
||||||
|
第一版重点记录:
|
||||||
|
|
||||||
|
1. 发送验证码是否成功;
|
||||||
|
2. 登录是否成功;
|
||||||
|
3. `/user/info` 探活是否成功;
|
||||||
|
4. 平台返回的 `code/message`;
|
||||||
|
5. 不记录明文密码、短信验证码、完整 token。
|
||||||
|
|
||||||
|
建议在日志和接口返回里都做脱敏:
|
||||||
|
|
||||||
|
- 手机号只显示部分;
|
||||||
|
- token 只显示前后几位;
|
||||||
|
- password 永不回显。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. 与现有履约架构的关系
|
||||||
|
|
||||||
|
当前项目里的履约主链路已经是:
|
||||||
|
|
||||||
|
1. 订单进入系统;
|
||||||
|
2. 根据 `provider/platform/shopId/sku` 命中 `order-fulfillment-bindings.json`;
|
||||||
|
3. 解析到 `fulfillment_profile`;
|
||||||
|
4. 创建 `delivery task`;
|
||||||
|
5. 根据 `executor_key` 决定后续执行方式。
|
||||||
|
|
||||||
|
所以 `cloudtentacles` 后续正确的接入位置是:
|
||||||
|
|
||||||
|
- 第一阶段:只补平台登录能力;
|
||||||
|
- 第二阶段:新增 `cloudtentacles_dispatch` 执行器;
|
||||||
|
- 第三阶段:把某些 SKU 的履约绑定切到这个执行器。
|
||||||
|
|
||||||
|
也就是说:
|
||||||
|
|
||||||
|
- `agiso/khhao` 决定“订单从哪里来”
|
||||||
|
- `cloudtentacles` 决定“订单怎么发出去”
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. 实施顺序
|
||||||
|
|
||||||
|
建议按这个顺序落地:
|
||||||
|
|
||||||
|
### 第 1 步:后端基础模块
|
||||||
|
|
||||||
|
先实现:
|
||||||
|
|
||||||
|
- `shared.js`
|
||||||
|
- `crypto-service.js`
|
||||||
|
- `http-client.js`
|
||||||
|
- `session-service.js`
|
||||||
|
- `source-config-service.js`
|
||||||
|
|
||||||
|
目标:
|
||||||
|
|
||||||
|
- 本地可完成发送验证码;
|
||||||
|
- 收到短信后可手动输入验证码登录;
|
||||||
|
- 登录后可拉 `/user/info`。
|
||||||
|
|
||||||
|
### 第 2 步:后台接口
|
||||||
|
|
||||||
|
增加:
|
||||||
|
|
||||||
|
- 配置读取/保存
|
||||||
|
- 发送验证码
|
||||||
|
- 登录测试
|
||||||
|
- token 探活
|
||||||
|
|
||||||
|
### 第 3 步:后台 UI
|
||||||
|
|
||||||
|
增加:
|
||||||
|
|
||||||
|
- 单独的平台配置区
|
||||||
|
- 发送验证码与登录测试区
|
||||||
|
- 调试反馈区
|
||||||
|
|
||||||
|
### 第 4 步:后续业务能力
|
||||||
|
|
||||||
|
登录能力稳定后,再补:
|
||||||
|
|
||||||
|
- SKU/分类/背包读取
|
||||||
|
- 发货动作
|
||||||
|
- `cloudtentacles_dispatch` 执行器
|
||||||
|
- 自动发货链路
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 12. 当前阶段结论
|
||||||
|
|
||||||
|
这个平台不是新的订单数据源,而是新的**外部履约能力**。
|
||||||
|
|
||||||
|
第一版接入重点不是立刻改主流程,而是先把 **可复用的登录能力** 做扎实。
|
||||||
|
|
||||||
|
最重要的不是把接口先堆进去,而是先把结构定对:
|
||||||
|
|
||||||
|
- `khhao` 型平台:验证码图片 + Cookie
|
||||||
|
- `cloudtentacles` 型平台:短信码 + RSA 加密 + token
|
||||||
|
|
||||||
|
二者都属于“后台登录型平台”,但在系统分层上不同:
|
||||||
|
|
||||||
|
- `khhao`:来源层
|
||||||
|
- `cloudtentacles`:履约层
|
||||||
|
|
||||||
|
因此第一版推荐结论是:
|
||||||
|
|
||||||
|
1. 作为独立履约平台能力接入;
|
||||||
|
2. 先做“配置 + 发验证码 + 登录 + 探活”;
|
||||||
|
3. 目录结构按“配置 / 加密 / HTTP / 会话 / 业务”拆开;
|
||||||
|
4. 后续通过新的 `executor_key` 接入履约任务执行,而不是改订单来源模型。
|
||||||
+563
@@ -0,0 +1,563 @@
|
|||||||
|
$s = class {
|
||||||
|
constructor() {
|
||||||
|
gl(this, "root_url", "https://www.cloudtentacles.com");
|
||||||
|
gl(this, "cache", new Map)
|
||||||
|
}
|
||||||
|
static getInstance() {
|
||||||
|
return $s.instance || ($s.instance = new $s)
|
||||||
|
}
|
||||||
|
PemToArrayBuffer(t) {
|
||||||
|
const r = t.split(`
|
||||||
|
`).filter(s => s.trim() && !s.includes("---")).join("")
|
||||||
|
, a = window.atob(r)
|
||||||
|
, o = new Uint8Array(a.length);
|
||||||
|
for (let s = 0; s < a.length; s++)
|
||||||
|
o[s] = a.charCodeAt(s);
|
||||||
|
return o.buffer
|
||||||
|
}
|
||||||
|
async encryptWithPublicKey(t) {
|
||||||
|
for (let s in t)
|
||||||
|
(t[s] === void 0 || t[s] === null || t[s] === "") && delete t[s];
|
||||||
|
const n = Date.now()
|
||||||
|
, r = Math.random().toString(16)
|
||||||
|
, a = "ct-client"
|
||||||
|
, o = JSON.stringify(Object.assign({
|
||||||
|
t: n,
|
||||||
|
r,
|
||||||
|
s: a
|
||||||
|
}, t));
|
||||||
|
try {
|
||||||
|
if (window.crypto && window.crypto.subtle && window.crypto.subtle.importKey) {
|
||||||
|
const i = await window.crypto.subtle.importKey("spki", this.PemToArrayBuffer($s.PublicKey), {
|
||||||
|
name: "RSA-OAEP",
|
||||||
|
hash: {
|
||||||
|
name: "SHA-256"
|
||||||
|
}
|
||||||
|
}, !0, ["encrypt"])
|
||||||
|
, u = new TextEncoder().encode(o)
|
||||||
|
, c = await window.crypto.subtle.encrypt({
|
||||||
|
name: "RSA-OAEP"
|
||||||
|
}, i, u)
|
||||||
|
, f = new Uint8Array(c);
|
||||||
|
let p = "";
|
||||||
|
for (let h = 0; h < f.byteLength; h++)
|
||||||
|
p += String.fromCharCode(f[h]);
|
||||||
|
return {
|
||||||
|
covert: window.btoa(p),
|
||||||
|
t: n,
|
||||||
|
r
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.warn("\u5F53\u524D\u5904\u4E8E\u975E\u5B89\u5168\u73AF\u5883\u6216\u6D4F\u89C8\u5668\u4E0D\u652F\u6301 WebCrypto\uFF0C\u5DF2\u56DE\u9000\u5230 forge \u52A0\u5BC6");
|
||||||
|
const l = Td.pki.publicKeyFromPem($s.PublicKey).encrypt(o, "RSA-OAEP", {
|
||||||
|
md: Td.md.sha256.create(),
|
||||||
|
mgf1: {
|
||||||
|
md: Td.md.sha256.create()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
covert: Td.util.encode64(l),
|
||||||
|
t: n,
|
||||||
|
r
|
||||||
|
}
|
||||||
|
} catch (s) {
|
||||||
|
throw console.error("\u52A0\u5BC6\u5931\u8D25:", s),
|
||||||
|
"\u52A0\u5BC6\u5904\u7406\u5931\u8D25\uFF0C\u8BF7\u68C0\u67E5\u6D4F\u89C8\u5668\u73AF\u5883"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cacheCheck(t, n) {
|
||||||
|
if (n > 0) {
|
||||||
|
const r = this.cache.get(t);
|
||||||
|
if (r && Date.now() - r.time <= n)
|
||||||
|
return JSON.parse(JSON.stringify(r.value))
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
parametersToKey(t, n) {
|
||||||
|
let r = t;
|
||||||
|
for (let a in n)
|
||||||
|
a !== "token" && (r += `${a}:${n[a]}`);
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
async httpProto(t, n, r) {
|
||||||
|
const a = this.parametersToKey(t, n)
|
||||||
|
, o = this.cacheCheck(a, r.interval || 0);
|
||||||
|
if (o)
|
||||||
|
return o;
|
||||||
|
const s = await B6({
|
||||||
|
url: t,
|
||||||
|
data: n,
|
||||||
|
method: r.type
|
||||||
|
})
|
||||||
|
, {data: l, code: i, message: u, token: c} = s.data;
|
||||||
|
switch (i) {
|
||||||
|
case bu.AlreadyExpired:
|
||||||
|
throw hs().setToken(""),
|
||||||
|
ei.replace("/login"),
|
||||||
|
"\u767B\u5F55\u5DF2\u8FC7\u671F";
|
||||||
|
case bu.UpdateSuccess:
|
||||||
|
hs().setToken(c);
|
||||||
|
case bu.Success:
|
||||||
|
return r.interval && r.interval > 0 && this.cache.set(a, {
|
||||||
|
time: Date.now(),
|
||||||
|
value: JSON.parse(JSON.stringify(l))
|
||||||
|
}),
|
||||||
|
l;
|
||||||
|
case bu.Fail:
|
||||||
|
throw u;
|
||||||
|
default:
|
||||||
|
throw "\u672A\u77E5\u9519\u8BEF"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async https(t, n={}, r={}) {
|
||||||
|
return await this.httpProto(t, n, Object.assign({}, $s.default_header, r))
|
||||||
|
}
|
||||||
|
async getVerifCode(t, n) {
|
||||||
|
const r = await this.encryptWithPublicKey({
|
||||||
|
account: t,
|
||||||
|
phone: n
|
||||||
|
});
|
||||||
|
await this.https("/public/verif_code", r)
|
||||||
|
}
|
||||||
|
async logIn(t, n, r, a) {
|
||||||
|
const o = await this.encryptWithPublicKey({
|
||||||
|
account: t,
|
||||||
|
password: cu(n),
|
||||||
|
phone: r,
|
||||||
|
code: a
|
||||||
|
});
|
||||||
|
return await this.https("/public/login", o)
|
||||||
|
}
|
||||||
|
async getUserInfo() {
|
||||||
|
return await this.https("/user/info", {}, {
|
||||||
|
load: !1
|
||||||
|
})
|
||||||
|
}
|
||||||
|
async getAsset() {
|
||||||
|
return await this.https("/user/get_asset", {}, {
|
||||||
|
type: "GET",
|
||||||
|
load: !1
|
||||||
|
})
|
||||||
|
}
|
||||||
|
async getPermission() {
|
||||||
|
return await this.https("/user/get_permission", {}, {
|
||||||
|
type: "GET",
|
||||||
|
load: !1
|
||||||
|
})
|
||||||
|
}
|
||||||
|
async getRecordRange(t, n, r) {
|
||||||
|
return await this.https("/record/range", {
|
||||||
|
record_code: t,
|
||||||
|
start_date: n,
|
||||||
|
end_date: r
|
||||||
|
}, {
|
||||||
|
load: !1
|
||||||
|
})
|
||||||
|
}
|
||||||
|
async getSkuList() {
|
||||||
|
return await this.https("/sku/list", {}, {
|
||||||
|
type: "GET",
|
||||||
|
interval: 6e4
|
||||||
|
})
|
||||||
|
}
|
||||||
|
async selectSkuList(t) {
|
||||||
|
return await this.https("/sku/select_list", {
|
||||||
|
id: t
|
||||||
|
})
|
||||||
|
}
|
||||||
|
async getCategoryList() {
|
||||||
|
return await this.https("/categories/get", {}, {
|
||||||
|
type: "GET",
|
||||||
|
interval: 6e4
|
||||||
|
})
|
||||||
|
}
|
||||||
|
async getSubList() {
|
||||||
|
return await this.https("/user/sub", {}, {
|
||||||
|
type: "GET"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
async updatePwd(t, n) {
|
||||||
|
const r = await this.encryptWithPublicKey({
|
||||||
|
o_password: cu(t),
|
||||||
|
n_password: cu(n)
|
||||||
|
});
|
||||||
|
return await this.https("/user/modify_pwd", r)
|
||||||
|
}
|
||||||
|
async giveAmount(t, n, r) {
|
||||||
|
return await this.https("/user/give", {
|
||||||
|
amount: t,
|
||||||
|
sub_id: n,
|
||||||
|
notes: r
|
||||||
|
})
|
||||||
|
}
|
||||||
|
async giveSku(t, n, r, a) {
|
||||||
|
return await this.https("/user/give_sku", {
|
||||||
|
sku_id: t,
|
||||||
|
sub_id: n,
|
||||||
|
count: r,
|
||||||
|
notes: a
|
||||||
|
})
|
||||||
|
}
|
||||||
|
async recoveryAmount(t, n, r) {
|
||||||
|
return await this.https("/user/recovery", {
|
||||||
|
amount: t,
|
||||||
|
sub_id: n,
|
||||||
|
notes: r
|
||||||
|
})
|
||||||
|
}
|
||||||
|
async getUserPreferentialPolicies() {
|
||||||
|
return await this.https("/sku/user_pref_poli", {}, {
|
||||||
|
type: "GET",
|
||||||
|
interval: 6e4
|
||||||
|
})
|
||||||
|
}
|
||||||
|
async buyGoods(t, n) {
|
||||||
|
return await this.https("/sku/buy", {
|
||||||
|
id: t,
|
||||||
|
count: n
|
||||||
|
})
|
||||||
|
}
|
||||||
|
async getKnapsack() {
|
||||||
|
const t = await this.https("/user/get_knapsack", {}, {
|
||||||
|
type: "GET"
|
||||||
|
});
|
||||||
|
return R8e().setKnapsackInfo(t),
|
||||||
|
t
|
||||||
|
}
|
||||||
|
async use(t) {
|
||||||
|
return await this.https("/sku/use", t)
|
||||||
|
}
|
||||||
|
async getIdleVirtualNumber(t) {
|
||||||
|
return await this.https("/vn/list", {
|
||||||
|
key: t
|
||||||
|
})
|
||||||
|
}
|
||||||
|
async getVirtualNumberBindUrl(t, n) {
|
||||||
|
return await this.https("/vn/bind_url", {
|
||||||
|
id: t,
|
||||||
|
key: n
|
||||||
|
})
|
||||||
|
}
|
||||||
|
async getVirtualNumberGameUserName(t, n) {
|
||||||
|
return await this.https("/vn/bind_info", [{
|
||||||
|
id: t,
|
||||||
|
key: n
|
||||||
|
}])
|
||||||
|
}
|
||||||
|
async inquiryUID(t, n) {
|
||||||
|
return await this.https("/vn/inquiry", {
|
||||||
|
uid: t,
|
||||||
|
key: n
|
||||||
|
})
|
||||||
|
}
|
||||||
|
async applyVirtualNumber(t, n) {
|
||||||
|
return await this.https("/vn/appoint", {
|
||||||
|
key: t,
|
||||||
|
phone: n
|
||||||
|
})
|
||||||
|
}
|
||||||
|
async returnVirtualNumber(t, n) {
|
||||||
|
return await this.https("/vn/back", {
|
||||||
|
key: t,
|
||||||
|
id: n
|
||||||
|
})
|
||||||
|
}
|
||||||
|
async generateLoginCode(t, n) {
|
||||||
|
return await this.https("/vn/generate_login_code", {
|
||||||
|
key: t,
|
||||||
|
id: n
|
||||||
|
})
|
||||||
|
}
|
||||||
|
async verifyLoginCode(t, n, r) {
|
||||||
|
return await this.https("/vn/verify_login_code", {
|
||||||
|
key: t,
|
||||||
|
id: n,
|
||||||
|
code: r
|
||||||
|
})
|
||||||
|
}
|
||||||
|
async vnVerifCode(t, n) {
|
||||||
|
return await this.https("/public/vn_verif_code", {
|
||||||
|
key: t,
|
||||||
|
phone: n
|
||||||
|
})
|
||||||
|
}
|
||||||
|
async getRecordList(t, n, r, a, o) {
|
||||||
|
return await this.https("/record/paging", {
|
||||||
|
record_code: t,
|
||||||
|
page: n,
|
||||||
|
size: r,
|
||||||
|
start_date: a,
|
||||||
|
end_date: o
|
||||||
|
})
|
||||||
|
}
|
||||||
|
async sendEMailCode(t, n, r) {
|
||||||
|
return await this.https("/vn/send_email_code", {
|
||||||
|
id: t,
|
||||||
|
key: n,
|
||||||
|
uid: r
|
||||||
|
})
|
||||||
|
}
|
||||||
|
async verifyEMailBindGame(t, n, r, a) {
|
||||||
|
return await this.https("/vn/verify_email_code", {
|
||||||
|
id: t,
|
||||||
|
key: n,
|
||||||
|
uid: r,
|
||||||
|
code: a
|
||||||
|
})
|
||||||
|
}
|
||||||
|
async direct_bind(t, n, r) {
|
||||||
|
return await this.https("/vn/direct_bind", {
|
||||||
|
id: t,
|
||||||
|
key: n,
|
||||||
|
uid: r
|
||||||
|
})
|
||||||
|
}
|
||||||
|
async getUserAuthority(t) {
|
||||||
|
return await this.https("/admin/get_user_permissions", {
|
||||||
|
id: t
|
||||||
|
})
|
||||||
|
}
|
||||||
|
async updateUserAuthority(t) {
|
||||||
|
return await this.https("/admin/modify_user_permissions", t)
|
||||||
|
}
|
||||||
|
async modifyUser(t) {
|
||||||
|
return await this.https("/admin/modify_user", t)
|
||||||
|
}
|
||||||
|
async getUserList() {
|
||||||
|
return await this.https("/user/list", {}, {
|
||||||
|
type: "GET"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
async getRelationshipList() {
|
||||||
|
return await this.https("/relationship/list", {}, {
|
||||||
|
type: "GET"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
async addRelationshipList(t, n) {
|
||||||
|
return await this.https("/relationship/add", {
|
||||||
|
user_id: t,
|
||||||
|
sub_id: n
|
||||||
|
})
|
||||||
|
}
|
||||||
|
async delRelationshipList(t, n) {
|
||||||
|
return await this.https("/relationship/del", {
|
||||||
|
user_id: t,
|
||||||
|
sub_id: n
|
||||||
|
})
|
||||||
|
}
|
||||||
|
async editRelationship(t, n) {
|
||||||
|
return await this.https("/relationship/modify", {
|
||||||
|
id: t,
|
||||||
|
open_repay: n
|
||||||
|
})
|
||||||
|
}
|
||||||
|
async getCategoryAllList() {
|
||||||
|
return await this.https("/admin/get_categories_list", {}, {
|
||||||
|
type: "GET"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
async getSkuAllList() {
|
||||||
|
return await this.https("/admin/get_sku_list", {}, {
|
||||||
|
type: "GET"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
async getPreferentialPoliciesAll() {
|
||||||
|
return await this.https("/preferential/list", {}, {
|
||||||
|
type: "GET"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
async getUserPreferentialPoliciesToID(t) {
|
||||||
|
return await this.https("/preferential/get_relationship", {
|
||||||
|
id: t
|
||||||
|
})
|
||||||
|
}
|
||||||
|
async createUser(t) {
|
||||||
|
const n = cu(t.password);
|
||||||
|
return await this.https("/admin/creator_user", Object.assign(t, {
|
||||||
|
password: n
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
async updateUserPassword(t, n) {
|
||||||
|
const r = await this.encryptWithPublicKey({
|
||||||
|
account: t,
|
||||||
|
n_password: cu(n)
|
||||||
|
});
|
||||||
|
return await this.https("/admin/modify_pwd", r)
|
||||||
|
}
|
||||||
|
async updateUserStatus(t, n) {
|
||||||
|
return await this.https("/admin/ban_user", {
|
||||||
|
id: t,
|
||||||
|
expire: n
|
||||||
|
})
|
||||||
|
}
|
||||||
|
async createSku(t) {
|
||||||
|
return await this.https("/admin/add_sku", t)
|
||||||
|
}
|
||||||
|
async createCategory(t) {
|
||||||
|
return await this.https("/admin/add_category", t)
|
||||||
|
}
|
||||||
|
async createPreferentialPolicies(t) {
|
||||||
|
return await this.https("/preferential/add", t)
|
||||||
|
}
|
||||||
|
async addUserPreferentialPolicies(t, n, r, a) {
|
||||||
|
return await this.https("/preferential/add_relationship", {
|
||||||
|
p_id: t,
|
||||||
|
u_id: n,
|
||||||
|
show_time: r,
|
||||||
|
hide_time: a
|
||||||
|
})
|
||||||
|
}
|
||||||
|
async addRelationshipLow(t) {
|
||||||
|
return await this.https("/preferential/add_relationship_low", t)
|
||||||
|
}
|
||||||
|
async batchRelationshipLow(t) {
|
||||||
|
return await this.https("/preferential/batch_relationship_low", t)
|
||||||
|
}
|
||||||
|
async delUserPreferentialPolicies(t, n) {
|
||||||
|
return await this.https("/preferential/del_Relationship", {
|
||||||
|
p_id: t,
|
||||||
|
u_id: n
|
||||||
|
})
|
||||||
|
}
|
||||||
|
async updateCategory(t) {
|
||||||
|
return await this.https("/admin/modify_category", t)
|
||||||
|
}
|
||||||
|
async updateSku(t) {
|
||||||
|
return await this.https("/admin/modify_sku", t)
|
||||||
|
}
|
||||||
|
async updatePreferentialPolicies(t) {
|
||||||
|
return await this.https("/preferential/modify", t)
|
||||||
|
}
|
||||||
|
async upsertCustom(t) {
|
||||||
|
return await this.https("/record/edit_custom", t)
|
||||||
|
}
|
||||||
|
async delCustom(t) {
|
||||||
|
return await this.https("/record/remove_custom", {
|
||||||
|
id: t
|
||||||
|
})
|
||||||
|
}
|
||||||
|
async subRangeRecord(t) {
|
||||||
|
return await this.https("/record/sub_range", {
|
||||||
|
ids: t
|
||||||
|
})
|
||||||
|
}
|
||||||
|
async transaction_record() {
|
||||||
|
return await this.https("/record/transaction_record")
|
||||||
|
}
|
||||||
|
async recipient_record() {
|
||||||
|
return await this.https("/record/recipient_record")
|
||||||
|
}
|
||||||
|
async observerSalesRecords(t, n, r) {
|
||||||
|
return await this.https("/observer/sales_records", {
|
||||||
|
ids: t,
|
||||||
|
start_date: n,
|
||||||
|
end_date: r
|
||||||
|
})
|
||||||
|
}
|
||||||
|
async observerTransactionRecord(t) {
|
||||||
|
return await this.https("/observer/transaction_record", {
|
||||||
|
ids: t
|
||||||
|
})
|
||||||
|
}
|
||||||
|
async observerHoldConsume(t) {
|
||||||
|
return await this.https("/observer/hold_consume", {
|
||||||
|
ids: t
|
||||||
|
})
|
||||||
|
}
|
||||||
|
async observerPaging(t, n, r, a, o, s) {
|
||||||
|
return await this.https("/observer/paging", {
|
||||||
|
ids: t,
|
||||||
|
record_code: n,
|
||||||
|
page: r,
|
||||||
|
size: a,
|
||||||
|
start_date: o,
|
||||||
|
end_date: s
|
||||||
|
})
|
||||||
|
}
|
||||||
|
async observerSub(t) {
|
||||||
|
return await this.https("/observer/sub", {
|
||||||
|
ids: t
|
||||||
|
})
|
||||||
|
}
|
||||||
|
async getYeZiList() {
|
||||||
|
return await this.https("/utils/list")
|
||||||
|
}
|
||||||
|
async applyYeZiPhone(t) {
|
||||||
|
return await this.https("/utils/apply", {
|
||||||
|
id: t
|
||||||
|
})
|
||||||
|
}
|
||||||
|
async releaseYeZiPhone(t) {
|
||||||
|
return await this.https("/utils/release", {
|
||||||
|
phone: t
|
||||||
|
})
|
||||||
|
}
|
||||||
|
async messageYeZiPhone(t) {
|
||||||
|
return await this.https("/utils/message", {
|
||||||
|
phone: t
|
||||||
|
})
|
||||||
|
}
|
||||||
|
async getProjectList() {
|
||||||
|
return await this.https("/utils/project_list")
|
||||||
|
}
|
||||||
|
async getHaoZhuList() {
|
||||||
|
return await this.https("/utils/list_hz")
|
||||||
|
}
|
||||||
|
async applyHaoZhuPhone(t) {
|
||||||
|
return await this.https("/utils/apply_hz", {
|
||||||
|
id: t + ""
|
||||||
|
})
|
||||||
|
}
|
||||||
|
async releaseHaoZhuPhone(t) {
|
||||||
|
return await this.https("/utils/release_hz", {
|
||||||
|
phone: t
|
||||||
|
})
|
||||||
|
}
|
||||||
|
async messageHaoZhuPhone(t) {
|
||||||
|
return await this.https("/utils/message_hz", {
|
||||||
|
phone: t
|
||||||
|
})
|
||||||
|
}
|
||||||
|
async getHaoZhuProjectList() {
|
||||||
|
return await this.https("/utils/project_list_hz")
|
||||||
|
}
|
||||||
|
async returnOfGoods(t) {
|
||||||
|
return await this.https("/admin/return_of_goods", t)
|
||||||
|
}
|
||||||
|
async customRepay(t) {
|
||||||
|
return await this.https("/admin/custom_repay", t)
|
||||||
|
}
|
||||||
|
async upsertSKU(t) {
|
||||||
|
return await this.https("/sku/upsert", {
|
||||||
|
data: t
|
||||||
|
})
|
||||||
|
}
|
||||||
|
async upsertCDK(t) {
|
||||||
|
await this.https("/admin/upsert_cdk", {
|
||||||
|
data: t
|
||||||
|
})
|
||||||
|
}
|
||||||
|
async getCDKList(t, n) {
|
||||||
|
return await this.https("/admin/cdk", {
|
||||||
|
page: t,
|
||||||
|
size: n
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
;
|
||||||
|
let Sr = $s;
|
||||||
|
gl(Sr, "instance"),
|
||||||
|
gl(Sr, "default_header", {
|
||||||
|
type: "POST",
|
||||||
|
interval: 0
|
||||||
|
}),
|
||||||
|
gl(Sr, "PublicKey", `-----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-----`),
|
||||||
|
gl(Sr, "LocalCacheKey", "YCJ-LocalCacheKey");
|
||||||
Reference in New Issue
Block a user