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