diff --git a/apps/backend/config/default.cjs b/apps/backend/config/default.cjs index 682f82fb..90707f0b 100644 --- a/apps/backend/config/default.cjs +++ b/apps/backend/config/default.cjs @@ -84,6 +84,16 @@ module.exports = { userInfoPath: '/user/info', assetPath: '/user/get_asset', permissionPath: '/user/get_permission', + categoriesPath: '/categories/get', + skuListPath: '/sku/list', + skuBuyPath: '/sku/buy', + knapsackPath: '/user/get_knapsack', + vnListPath: '/vn/list', + vnAppointPath: '/vn/appoint', + vnGenerateLoginCodePath: '/vn/generate_login_code', + vnVerifCodePath: '/public/vn_verif_code', + vnVerifyLoginCodePath: '/vn/verify_login_code', + vnBindUrlPath: '/vn/bind_url', clientSource: 'ct-client', deviceId: '-', deviceType: 0, diff --git a/apps/backend/data/cloudtentacles-session.json b/apps/backend/data/cloudtentacles-session.json new file mode 100644 index 00000000..e10147bc --- /dev/null +++ b/apps/backend/data/cloudtentacles-session.json @@ -0,0 +1,9 @@ +{ + "token": "rEX46KZqfspDZ5CVtDcyLuo9GF5PUqsp2TOHvLuleA3zdQev8sgLJOOHiTjxxo2NLXEm6VrxNx6i9hhEdbFJQwSI", + "baseUrl": "https://123.207.217.176", + "username": "17665234375", + "phone": "17665234375", + "loggedInAt": "2026-05-02T14:56:41.055Z", + "deviceId": "-", + "deviceType": 0 +} diff --git a/apps/backend/data/khhao-sync-state.json b/apps/backend/data/khhao-sync-state.json index 785dcafc..60d49432 100644 --- a/apps/backend/data/khhao-sync-state.json +++ b/apps/backend/data/khhao-sync-state.json @@ -1,11 +1,11 @@ { "syncFromCreatedAt": "2026-05-02T10:53:28.550Z", - "lastRunStartedAt": "2026-05-02T13:46:11.938Z", - "lastRunFinishedAt": "2026-05-02T13:46:13.175Z", + "lastRunStartedAt": "2026-05-02T15:00:54.051Z", + "lastRunFinishedAt": "2026-05-02T15:00:55.112Z", "lastRunStatus": "success", "lastErrorMessage": "", "fetchedCount": 40, "syncedCount": 0, "ignoredCount": 40, - "lastOrderCreatedAt": "2026-05-02T13:46:02.000Z" + "lastOrderCreatedAt": "2026-05-02T15:00:47.000Z" } diff --git a/apps/backend/src/config/runtime.js b/apps/backend/src/config/runtime.js index ae363315..9e8f7caf 100644 --- a/apps/backend/src/config/runtime.js +++ b/apps/backend/src/config/runtime.js @@ -295,6 +295,56 @@ function applyEnvOverrides(baseConfig) { nextConfig.platforms.cloudtentacles.permissionPath = cloudtentaclesPermissionPath } + const cloudtentaclesCategoriesPath = String(process.env.CLOUDTENTACLES_CATEGORIES_PATH || '').trim() + if (cloudtentaclesCategoriesPath) { + nextConfig.platforms.cloudtentacles.categoriesPath = cloudtentaclesCategoriesPath + } + + const cloudtentaclesSkuListPath = String(process.env.CLOUDTENTACLES_SKU_LIST_PATH || '').trim() + if (cloudtentaclesSkuListPath) { + nextConfig.platforms.cloudtentacles.skuListPath = cloudtentaclesSkuListPath + } + + const cloudtentaclesSkuBuyPath = String(process.env.CLOUDTENTACLES_SKU_BUY_PATH || '').trim() + if (cloudtentaclesSkuBuyPath) { + nextConfig.platforms.cloudtentacles.skuBuyPath = cloudtentaclesSkuBuyPath + } + + const cloudtentaclesKnapsackPath = String(process.env.CLOUDTENTACLES_KNAPSACK_PATH || '').trim() + if (cloudtentaclesKnapsackPath) { + nextConfig.platforms.cloudtentacles.knapsackPath = cloudtentaclesKnapsackPath + } + + const cloudtentaclesVnListPath = String(process.env.CLOUDTENTACLES_VN_LIST_PATH || '').trim() + if (cloudtentaclesVnListPath) { + nextConfig.platforms.cloudtentacles.vnListPath = cloudtentaclesVnListPath + } + + const cloudtentaclesVnAppointPath = String(process.env.CLOUDTENTACLES_VN_APPOINT_PATH || '').trim() + if (cloudtentaclesVnAppointPath) { + nextConfig.platforms.cloudtentacles.vnAppointPath = cloudtentaclesVnAppointPath + } + + const cloudtentaclesVnGenerateLoginCodePath = String(process.env.CLOUDTENTACLES_VN_GENERATE_LOGIN_CODE_PATH || '').trim() + if (cloudtentaclesVnGenerateLoginCodePath) { + nextConfig.platforms.cloudtentacles.vnGenerateLoginCodePath = cloudtentaclesVnGenerateLoginCodePath + } + + const cloudtentaclesVnVerifCodePath = String(process.env.CLOUDTENTACLES_VN_VERIF_CODE_PATH || '').trim() + if (cloudtentaclesVnVerifCodePath) { + nextConfig.platforms.cloudtentacles.vnVerifCodePath = cloudtentaclesVnVerifCodePath + } + + const cloudtentaclesVnVerifyLoginCodePath = String(process.env.CLOUDTENTACLES_VN_VERIFY_LOGIN_CODE_PATH || '').trim() + if (cloudtentaclesVnVerifyLoginCodePath) { + nextConfig.platforms.cloudtentacles.vnVerifyLoginCodePath = cloudtentaclesVnVerifyLoginCodePath + } + + const cloudtentaclesVnBindUrlPath = String(process.env.CLOUDTENTACLES_VN_BIND_URL_PATH || '').trim() + if (cloudtentaclesVnBindUrlPath) { + nextConfig.platforms.cloudtentacles.vnBindUrlPath = cloudtentaclesVnBindUrlPath + } + const cloudtentaclesPublicKeyPem = String(process.env.CLOUDTENTACLES_PUBLIC_KEY_PEM || '').trim() if (cloudtentaclesPublicKeyPem) { nextConfig.platforms.cloudtentacles.publicKeyPem = cloudtentaclesPublicKeyPem diff --git a/apps/backend/src/routes/admin/platform-config.js b/apps/backend/src/routes/admin/platform-config.js index e9bc915f..b261c9e1 100644 --- a/apps/backend/src/routes/admin/platform-config.js +++ b/apps/backend/src/routes/admin/platform-config.js @@ -3,10 +3,20 @@ import { Router } from 'express' import { + appointAdminCloudtentaclesVirtualNumber, + buyAdminCloudtentaclesSku, + fetchAdminCloudtentaclesVirtualNumberCode, + generateAdminCloudtentaclesLoginCode, + getAdminCloudtentaclesAsset, + getAdminCloudtentaclesBindUrl, + getAdminCloudtentaclesCategories, + getAdminCloudtentaclesKnapsack, getAdminCloudtentaclesSourceConfig, + getAdminCloudtentaclesSkuList, getAdminAgisoShopConfigs, getAdminFulfillmentBindingConfigs, getAdminKhhaoSourceConfig, + listAdminCloudtentaclesVirtualNumbers, lookupAdminFulfillmentBindingOrder, queryAdminKhhaoOrders, sendAdminCloudtentaclesSmsCode, @@ -17,6 +27,7 @@ import { updateAdminAgisoShopConfigs, updateAdminKhhaoSourceConfig, updateAdminFulfillmentBindingConfigs, + verifyAdminCloudtentaclesLoginCode, validateAdminCloudtentaclesSession, } from '../../services/admin/admin-platform-config-service.js' import { createJsonHandler, requireAdminRoles } from './shared.js' @@ -31,6 +42,9 @@ import { createJsonHandler, requireAdminRoles } from './shared.js' /** @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-route-inputs.js').AdminCloudtentaclesCatalogQueryRouteBody} AdminCloudtentaclesCatalogQueryRouteBody */ +/** @typedef {import('../../types/admin-route-inputs.js').AdminCloudtentaclesSkuBuyRouteBody} AdminCloudtentaclesSkuBuyRouteBody */ +/** @typedef {import('../../types/admin-route-inputs.js').AdminCloudtentaclesVirtualNumberRouteBody} AdminCloudtentaclesVirtualNumberRouteBody */ /** @typedef {import('../../types/admin-write-models.js').AdminAgisoShopConfigSaveResponse} AdminAgisoShopConfigSaveResponse */ const router = Router() @@ -268,6 +282,105 @@ router.post('/platform-config/cloudtentacles/validate-session', createJsonHandle }, )) +router.post('/platform-config/cloudtentacles/asset', createJsonHandler( + (req) => getAdminCloudtentaclesAsset(/** @type {AdminCloudtentaclesCatalogQueryRouteBody} */ (req.body)), + { + successMessage: 'cloudtentacles 余额查询成功', + errorMessage: 'cloudtentacles 余额查询失败', + scope: '[admin/platform-config/cloudtentacles/asset]', + }, +)) + +router.post('/platform-config/cloudtentacles/categories', createJsonHandler( + (req) => getAdminCloudtentaclesCategories(/** @type {AdminCloudtentaclesCatalogQueryRouteBody} */ (req.body)), + { + successMessage: 'cloudtentacles 分类查询成功', + errorMessage: 'cloudtentacles 分类查询失败', + scope: '[admin/platform-config/cloudtentacles/categories]', + }, +)) + +router.post('/platform-config/cloudtentacles/sku/list', createJsonHandler( + (req) => getAdminCloudtentaclesSkuList(/** @type {AdminCloudtentaclesCatalogQueryRouteBody} */ (req.body)), + { + successMessage: 'cloudtentacles SKU 列表查询成功', + errorMessage: 'cloudtentacles SKU 列表查询失败', + scope: '[admin/platform-config/cloudtentacles/sku/list]', + }, +)) + +router.post('/platform-config/cloudtentacles/sku/buy', createJsonHandler( + (req) => buyAdminCloudtentaclesSku(/** @type {AdminCloudtentaclesSkuBuyRouteBody} */ (req.body)), + { + successMessage: 'cloudtentacles SKU 购买成功', + errorMessage: 'cloudtentacles SKU 购买失败', + scope: '[admin/platform-config/cloudtentacles/sku/buy]', + }, +)) + +router.post('/platform-config/cloudtentacles/knapsack', createJsonHandler( + (req) => getAdminCloudtentaclesKnapsack(/** @type {AdminCloudtentaclesCatalogQueryRouteBody} */ (req.body)), + { + successMessage: 'cloudtentacles 背包查询成功', + errorMessage: 'cloudtentacles 背包查询失败', + scope: '[admin/platform-config/cloudtentacles/knapsack]', + }, +)) + +router.post('/platform-config/cloudtentacles/vn/list', createJsonHandler( + (req) => listAdminCloudtentaclesVirtualNumbers(/** @type {AdminCloudtentaclesVirtualNumberRouteBody} */ (req.body)), + { + successMessage: 'cloudtentacles 虚拟号列表查询成功', + errorMessage: 'cloudtentacles 虚拟号列表查询失败', + scope: '[admin/platform-config/cloudtentacles/vn/list]', + }, +)) + +router.post('/platform-config/cloudtentacles/vn/appoint', createJsonHandler( + (req) => appointAdminCloudtentaclesVirtualNumber(/** @type {AdminCloudtentaclesVirtualNumberRouteBody} */ (req.body)), + { + successMessage: 'cloudtentacles 虚拟号申请成功', + errorMessage: 'cloudtentacles 虚拟号申请失败', + scope: '[admin/platform-config/cloudtentacles/vn/appoint]', + }, +)) + +router.post('/platform-config/cloudtentacles/vn/generate-login-code', createJsonHandler( + (req) => generateAdminCloudtentaclesLoginCode(/** @type {AdminCloudtentaclesVirtualNumberRouteBody} */ (req.body)), + { + successMessage: 'cloudtentacles 登录码生成成功', + errorMessage: 'cloudtentacles 登录码生成失败', + scope: '[admin/platform-config/cloudtentacles/vn/generate-login-code]', + }, +)) + +router.post('/platform-config/cloudtentacles/vn/fetch-code', createJsonHandler( + (req) => fetchAdminCloudtentaclesVirtualNumberCode(/** @type {AdminCloudtentaclesVirtualNumberRouteBody} */ (req.body)), + { + successMessage: 'cloudtentacles 验证码获取成功', + errorMessage: 'cloudtentacles 验证码获取失败', + scope: '[admin/platform-config/cloudtentacles/vn/fetch-code]', + }, +)) + +router.post('/platform-config/cloudtentacles/vn/verify-code', createJsonHandler( + (req) => verifyAdminCloudtentaclesLoginCode(/** @type {AdminCloudtentaclesVirtualNumberRouteBody} */ (req.body)), + { + successMessage: 'cloudtentacles 登录码校验成功', + errorMessage: 'cloudtentacles 登录码校验失败', + scope: '[admin/platform-config/cloudtentacles/vn/verify-code]', + }, +)) + +router.post('/platform-config/cloudtentacles/vn/bind-url', createJsonHandler( + (req) => getAdminCloudtentaclesBindUrl(/** @type {AdminCloudtentaclesVirtualNumberRouteBody} */ (req.body)), + { + successMessage: 'cloudtentacles 兑换链接获取成功', + errorMessage: 'cloudtentacles 兑换链接获取失败', + scope: '[admin/platform-config/cloudtentacles/vn/bind-url]', + }, +)) + router.get('/platform-config/fulfillment-bindings', createJsonHandler( () => getAdminFulfillmentBindingConfigs(), { diff --git a/apps/backend/src/services/admin/admin-platform-config-service.js b/apps/backend/src/services/admin/admin-platform-config-service.js index d72b0d03..45c2e1bc 100644 --- a/apps/backend/src/services/admin/admin-platform-config-service.js +++ b/apps/backend/src/services/admin/admin-platform-config-service.js @@ -20,11 +20,32 @@ import { getCloudtentaclesSourcesFilePath, saveCloudtentaclesSourceConfig, } from '../platforms/cloudtentacles/source-config-service.js' +import { + clearCloudtentaclesSessionState, + getCloudtentaclesSessionFilePath, + getCloudtentaclesSessionState, + saveCloudtentaclesSessionState, +} from '../platforms/cloudtentacles/session-state-service.js' import { loginCloudtentaclesSession, sendCloudtentaclesSmsCode, validateCloudtentaclesSession, } from '../platforms/cloudtentacles/session-service.js' +import { + buyCloudtentaclesSku, + getCloudtentaclesAsset, + getCloudtentaclesCategories, + listCloudtentaclesSku, +} from '../platforms/cloudtentacles/catalog-service.js' +import { getCloudtentaclesKnapsack } from '../platforms/cloudtentacles/knapsack-service.js' +import { + appointCloudtentaclesVirtualNumber, + fetchCloudtentaclesVirtualNumberCode, + generateCloudtentaclesLoginCode, + getCloudtentaclesBindUrl, + listCloudtentaclesVirtualNumbers, + verifyCloudtentaclesLoginCode, +} from '../platforms/cloudtentacles/virtual-number-service.js' import { normalizeAgisoMessageTemplate } from '../platforms/agiso/xianyu/message-service.js' import { getOrderFulfillmentBindingConfigs, @@ -46,6 +67,9 @@ import { resolveDisplayShopName } from './admin-read-shared-helpers.js' /** @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').AdminCloudtentaclesCatalogQueryInput} AdminCloudtentaclesCatalogQueryInput */ +/** @typedef {import('../../types/admin-write-inputs.js').AdminCloudtentaclesSkuBuyInput} AdminCloudtentaclesSkuBuyInput */ +/** @typedef {import('../../types/admin-write-inputs.js').AdminCloudtentaclesVirtualNumberInput} AdminCloudtentaclesVirtualNumberInput */ /** @typedef {import('../../types/admin-write-inputs.js').AdminFulfillmentBindingConfigSaveInput} AdminFulfillmentBindingConfigSaveInput */ /** @typedef {import('../../types/admin-write-inputs.js').AdminFulfillmentBindingLookupInput} AdminFulfillmentBindingLookupInput */ @@ -352,9 +376,11 @@ export async function syncAdminKhhaoOrders(payload = /** @type {AdminKhhaoOrderQ export function getAdminCloudtentaclesSourceConfig() { const config = getCloudtentaclesSourceConfig() + const session = getCloudtentaclesSessionState() return { filePath: getCloudtentaclesSourcesFilePath(), + sessionFilePath: getCloudtentaclesSessionFilePath(), source: { enabled: config.enabled !== false, baseUrl: String(config.baseUrl || '').trim(), @@ -364,11 +390,13 @@ export function getAdminCloudtentaclesSourceConfig() { deviceId: String(config.deviceId || '-').trim() || '-', deviceType: Number(config.deviceType || 0), }, + session: mapAdminCloudtentaclesSession(session), } } /** @param {AdminCloudtentaclesSourceConfigInput} [payload] */ export function updateAdminCloudtentaclesSourceConfig(payload = /** @type {AdminCloudtentaclesSourceConfigInput} */ ({})) { + const current = getCloudtentaclesSourceConfig() const saved = saveCloudtentaclesSourceConfig({ enabled: payload.enabled !== false, baseUrl: String(payload.baseUrl || '').trim() || 'https://123.207.217.176', @@ -378,9 +406,12 @@ export function updateAdminCloudtentaclesSourceConfig(payload = /** @type {Admin deviceId: String(payload.deviceId || '-').trim() || '-', deviceType: Number(payload.deviceType || 0), }) + const shouldClearSession = hasCloudtentaclesCredentialContextChanged(current, saved) + const session = shouldClearSession ? clearCloudtentaclesSessionState() : getCloudtentaclesSessionState() return { filePath: getCloudtentaclesSourcesFilePath(), + sessionFilePath: getCloudtentaclesSessionFilePath(), source: { enabled: saved.enabled !== false, baseUrl: String(saved.baseUrl || '').trim(), @@ -390,6 +421,7 @@ export function updateAdminCloudtentaclesSourceConfig(payload = /** @type {Admin deviceId: String(saved.deviceId || '-').trim() || '-', deviceType: Number(saved.deviceType || 0), }, + session: mapAdminCloudtentaclesSession(session), } } @@ -422,6 +454,15 @@ export async function testAdminCloudtentaclesLogin(payload = /** @type {AdminClo deviceId: pickFirstNonEmpty([payload.deviceId, savedSource.deviceId, '-']), deviceType: payload.deviceType ?? savedSource.deviceType, }) + const savedSession = saveCloudtentaclesSessionState({ + token: session.token, + baseUrl: session.baseUrl, + username: session.username, + phone: session.phone, + loggedInAt: session.loggedInAt, + deviceId: pickFirstNonEmpty([payload.deviceId, savedSource.deviceId, '-']), + deviceType: payload.deviceType ?? savedSource.deviceType, + }) return { baseUrl: session.baseUrl, @@ -434,6 +475,7 @@ export async function testAdminCloudtentaclesLogin(payload = /** @type {AdminClo tokenMasked: maskSecret(session.token), permissionCount: session.permissions.length, permissions: session.permissions, + persisted: Boolean(savedSession.token), }, userInfo: session.userInfo, asset: session.asset, @@ -443,12 +485,22 @@ export async function testAdminCloudtentaclesLogin(payload = /** @type {AdminClo /** @param {AdminCloudtentaclesValidateSessionInput} [payload] */ export async function validateAdminCloudtentaclesSession(payload = /** @type {AdminCloudtentaclesValidateSessionInput} */ ({})) { const savedSource = getCloudtentaclesSourceConfig() + const persistedSession = getCloudtentaclesSessionState() const session = await validateCloudtentaclesSession({ baseUrl: pickFirstNonEmpty([payload.baseUrl, savedSource.baseUrl, 'https://123.207.217.176']), - token: String(payload.token || '').trim(), + token: pickFirstNonEmpty([payload.token, persistedSession.token]), deviceId: pickFirstNonEmpty([payload.deviceId, savedSource.deviceId, '-']), deviceType: payload.deviceType ?? savedSource.deviceType, }) + const savedSession = saveCloudtentaclesSessionState({ + token: session.token, + baseUrl: session.baseUrl, + username: pickFirstNonEmpty([persistedSession.username, savedSource.username]), + phone: pickFirstNonEmpty([persistedSession.phone, savedSource.phone]), + loggedInAt: session.loggedInAt, + deviceId: pickFirstNonEmpty([payload.deviceId, savedSource.deviceId, persistedSession.deviceId, '-']), + deviceType: payload.deviceType ?? savedSource.deviceType ?? persistedSession.deviceType, + }) return { baseUrl: session.baseUrl, @@ -457,12 +509,106 @@ export async function validateAdminCloudtentaclesSession(payload = /** @type {Ad tokenMasked: maskSecret(session.token), permissionCount: session.permissions.length, permissions: session.permissions, + persisted: Boolean(savedSession.token), }, userInfo: session.userInfo, asset: session.asset, } } +/** @param {AdminCloudtentaclesCatalogQueryInput} [payload] */ +export async function getAdminCloudtentaclesAsset(payload = /** @type {AdminCloudtentaclesCatalogQueryInput} */ ({})) { + const context = resolveCloudtentaclesAdminContext(payload) + return getCloudtentaclesAsset(context) +} + +/** @param {AdminCloudtentaclesCatalogQueryInput} [payload] */ +export async function getAdminCloudtentaclesCategories(payload = /** @type {AdminCloudtentaclesCatalogQueryInput} */ ({})) { + const context = resolveCloudtentaclesAdminContext(payload) + return getCloudtentaclesCategories(context) +} + +/** @param {AdminCloudtentaclesCatalogQueryInput} [payload] */ +export async function getAdminCloudtentaclesSkuList(payload = /** @type {AdminCloudtentaclesCatalogQueryInput} */ ({})) { + const context = resolveCloudtentaclesAdminContext(payload) + return listCloudtentaclesSku(context) +} + +/** @param {AdminCloudtentaclesSkuBuyInput} [payload] */ +export async function buyAdminCloudtentaclesSku(payload = /** @type {AdminCloudtentaclesSkuBuyInput} */ ({})) { + const context = resolveCloudtentaclesAdminContext(payload) + return buyCloudtentaclesSku({ + ...context, + id: payload.id, + count: payload.count, + }) +} + +/** @param {AdminCloudtentaclesCatalogQueryInput} [payload] */ +export async function getAdminCloudtentaclesKnapsack(payload = /** @type {AdminCloudtentaclesCatalogQueryInput} */ ({})) { + const context = resolveCloudtentaclesAdminContext(payload) + return getCloudtentaclesKnapsack(context) +} + +/** @param {AdminCloudtentaclesVirtualNumberInput} [payload] */ +export async function listAdminCloudtentaclesVirtualNumbers(payload = /** @type {AdminCloudtentaclesVirtualNumberInput} */ ({})) { + const context = resolveCloudtentaclesAdminContext(payload) + return listCloudtentaclesVirtualNumbers({ + ...context, + key: payload.key, + }) +} + +/** @param {AdminCloudtentaclesVirtualNumberInput} [payload] */ +export async function appointAdminCloudtentaclesVirtualNumber(payload = /** @type {AdminCloudtentaclesVirtualNumberInput} */ ({})) { + const context = resolveCloudtentaclesAdminContext(payload) + return appointCloudtentaclesVirtualNumber({ + ...context, + key: payload.key, + }) +} + +/** @param {AdminCloudtentaclesVirtualNumberInput} [payload] */ +export async function generateAdminCloudtentaclesLoginCode(payload = /** @type {AdminCloudtentaclesVirtualNumberInput} */ ({})) { + const context = resolveCloudtentaclesAdminContext(payload) + return generateCloudtentaclesLoginCode({ + ...context, + key: payload.key, + id: payload.id, + }) +} + +/** @param {AdminCloudtentaclesVirtualNumberInput} [payload] */ +export async function fetchAdminCloudtentaclesVirtualNumberCode(payload = /** @type {AdminCloudtentaclesVirtualNumberInput} */ ({})) { + const context = resolveCloudtentaclesAdminContext(payload) + return fetchCloudtentaclesVirtualNumberCode({ + ...context, + key: payload.key, + phone: payload.phone, + }) +} + +/** @param {AdminCloudtentaclesVirtualNumberInput} [payload] */ +export async function verifyAdminCloudtentaclesLoginCode(payload = /** @type {AdminCloudtentaclesVirtualNumberInput} */ ({})) { + const context = resolveCloudtentaclesAdminContext(payload) + return verifyCloudtentaclesLoginCode({ + ...context, + key: payload.key, + id: payload.id, + code: payload.code, + }) +} + +/** @param {AdminCloudtentaclesVirtualNumberInput} [payload] */ +export async function getAdminCloudtentaclesBindUrl(payload = /** @type {AdminCloudtentaclesVirtualNumberInput} */ ({})) { + const context = resolveCloudtentaclesAdminContext(payload) + return getCloudtentaclesBindUrl({ + ...context, + key: payload.key, + id: payload.id, + }) +} + export async function getAdminFulfillmentBindingConfigs() { const bindings = getOrderFulfillmentBindingConfigs() const rowsResult = await query( @@ -825,6 +971,42 @@ function maskPhone(value) { return `${normalized.slice(0, 3)}****${normalized.slice(-4)}` } +function resolveCloudtentaclesAdminContext(payload = {}) { + const savedSource = getCloudtentaclesSourceConfig() + const persistedSession = getCloudtentaclesSessionState() + + return { + baseUrl: pickFirstNonEmpty([payload.baseUrl, savedSource.baseUrl, 'https://123.207.217.176']), + token: pickFirstNonEmpty([payload.token, persistedSession.token]), + deviceId: pickFirstNonEmpty([payload.deviceId, savedSource.deviceId, persistedSession.deviceId, '-']), + deviceType: payload.deviceType ?? savedSource.deviceType ?? persistedSession.deviceType, + } +} + +function hasCloudtentaclesCredentialContextChanged(current, next) { + return ( + String(current.baseUrl || '').trim() !== String(next.baseUrl || '').trim() + || String(current.username || '').trim() !== String(next.username || '').trim() + || String(current.phone || '').trim() !== String(next.phone || '').trim() + || String(current.deviceId || '').trim() !== String(next.deviceId || '').trim() + || Number(current.deviceType || 0) !== Number(next.deviceType || 0) + ) +} + +function mapAdminCloudtentaclesSession(session = {}) { + return { + token: String(session.token || '').trim(), + tokenMasked: maskSecret(session.token), + baseUrl: String(session.baseUrl || '').trim(), + username: String(session.username || '').trim(), + phoneMasked: maskPhone(session.phone), + loggedInAt: String(session.loggedInAt || '').trim(), + deviceId: String(session.deviceId || '-').trim() || '-', + deviceType: Number(session.deviceType || 0), + hasToken: Boolean(String(session.token || '').trim()), + } +} + function isPlainObject(value) { return Boolean(value) && typeof value === 'object' && !Array.isArray(value) } diff --git a/apps/backend/src/services/platforms/cloudtentacles/catalog-service.js b/apps/backend/src/services/platforms/cloudtentacles/catalog-service.js new file mode 100644 index 00000000..f41971fb --- /dev/null +++ b/apps/backend/src/services/platforms/cloudtentacles/catalog-service.js @@ -0,0 +1,174 @@ +// @ts-check + +import { createHttpError } from '../../../utils/http.js' +import { cloudtentaclesRequest } from './http-client.js' +import { resolveCloudtentaclesConfig } from './shared.js' + +export async function getCloudtentaclesAsset(payload = {}) { + const token = requireToken(payload.token, 'cloudtentacles 余额查询缺少 token', 'cloudtentacles_asset_missing_token') + const config = resolveCloudtentaclesConfig(payload) + + const result = await cloudtentaclesRequest(config.assetPath, { + ...config, + method: 'GET', + token, + contentType: 'application/json', + businessErrorStatusCode: 401, + businessErrorCode: 'cloudtentacles_asset_failed', + }) + + return { + baseUrl: config.baseUrl, + asset: Number(result.payload?.data || 0), + raw: result.payload?.data ?? null, + } +} + +export async function getCloudtentaclesCategories(payload = {}) { + const token = requireToken(payload.token, 'cloudtentacles 分类查询缺少 token', 'cloudtentacles_categories_missing_token') + const config = resolveCloudtentaclesConfig(payload) + + const result = await cloudtentaclesRequest(config.categoriesPath, { + ...config, + method: 'GET', + token, + contentType: 'application/json', + businessErrorStatusCode: 401, + businessErrorCode: 'cloudtentacles_categories_failed', + }) + + const items = Array.isArray(result.payload?.data) ? result.payload.data.map(mapCategoryItem) : [] + + return { + baseUrl: config.baseUrl, + itemCount: items.length, + items, + rawItems: Array.isArray(result.payload?.data) ? result.payload.data : [], + } +} + +export async function listCloudtentaclesSku(payload = {}) { + const token = requireToken(payload.token, 'cloudtentacles SKU 列表查询缺少 token', 'cloudtentacles_sku_list_missing_token') + const config = resolveCloudtentaclesConfig(payload) + + const result = await cloudtentaclesRequest(config.skuListPath, { + ...config, + method: 'GET', + token, + contentType: 'application/json', + businessErrorStatusCode: 401, + businessErrorCode: 'cloudtentacles_sku_list_failed', + }) + + const items = Array.isArray(result.payload?.data) ? result.payload.data.map(mapSkuItem) : [] + + return { + baseUrl: config.baseUrl, + itemCount: items.length, + items, + rawItems: Array.isArray(result.payload?.data) ? result.payload.data : [], + } +} + +export async function buyCloudtentaclesSku(payload = {}) { + const token = requireToken(payload.token, 'cloudtentacles 购买 SKU 缺少 token', 'cloudtentacles_sku_buy_missing_token') + const skuId = requireId(payload.id, 'cloudtentacles 购买 SKU 缺少商品 id', 'cloudtentacles_sku_buy_missing_id') + const count = requireCount(payload.count) + const config = resolveCloudtentaclesConfig(payload) + + const result = await cloudtentaclesRequest(config.skuBuyPath, { + ...config, + method: 'POST', + token, + body: { + id: skuId, + count, + }, + businessErrorStatusCode: 401, + businessErrorCode: 'cloudtentacles_sku_buy_failed', + }) + + return { + baseUrl: config.baseUrl, + id: skuId, + count, + responseMessage: String(result.payload?.message || result.payload?.msg || 'success'), + } +} + +function mapCategoryItem(item) { + const source = isPlainObject(item) ? item : {} + + return { + id: Number(source.id || 0), + name: String(source.name || '').trim(), + supplierTag: String(source.supplier_tag || '').trim(), + description: String(source.description || '').trim(), + notes: String(source.notes || '').trim(), + expire: Number(source.expire || 0), + sendType: Number(source.send_type || 0), + exchangeUrl: String(source.exchange_url || '').trim(), + createdAt: String(source.c_time || '').trim(), + updatedAt: String(source.l_update_time || '').trim(), + raw: source, + } +} + +function mapSkuItem(item) { + const source = isPlainObject(item) ? item : {} + + return { + id: Number(source.id || 0), + categoriesId: Number(source.categories_id || 0), + name: String(source.name || '').trim(), + description: String(source.desc || '').trim(), + image: String(source.image || '').trim(), + inventory: Number(source.inventory || 0), + price: Number(source.price || 0), + listingTime: String(source.listing_time || '').trim(), + delistingTime: String(source.delisting_time || '').trim(), + buyLimitMin: Number(source.buy_limit_min || 0), + buyLimitMax: Number(source.buy_limit_max || 0), + raw: source, + } +} + +function requireToken(value, message, errorCode) { + const token = String(value || '').trim() + if (!token) { + throw createHttpError(message, { + statusCode: 400, + errorCode, + }) + } + + return token +} + +function requireId(value, message, errorCode) { + const id = Number(value) + if (!Number.isInteger(id) || id <= 0) { + throw createHttpError(message, { + statusCode: 400, + errorCode, + }) + } + + return id +} + +function requireCount(value) { + const count = Number(value) + if (!Number.isInteger(count) || count <= 0) { + throw createHttpError('cloudtentacles 购买数量无效', { + statusCode: 400, + errorCode: 'cloudtentacles_sku_buy_invalid_count', + }) + } + + return count +} + +function isPlainObject(value) { + return Boolean(value) && typeof value === 'object' && !Array.isArray(value) +} diff --git a/apps/backend/src/services/platforms/cloudtentacles/knapsack-service.js b/apps/backend/src/services/platforms/cloudtentacles/knapsack-service.js new file mode 100644 index 00000000..c417045f --- /dev/null +++ b/apps/backend/src/services/platforms/cloudtentacles/knapsack-service.js @@ -0,0 +1,53 @@ +// @ts-check + +import { createHttpError } from '../../../utils/http.js' +import { cloudtentaclesRequest } from './http-client.js' +import { resolveCloudtentaclesConfig } from './shared.js' + +export async function getCloudtentaclesKnapsack(payload = {}) { + const token = String(payload.token || '').trim() + if (!token) { + throw createHttpError('cloudtentacles 背包查询缺少 token', { + statusCode: 400, + errorCode: 'cloudtentacles_knapsack_missing_token', + }) + } + + const config = resolveCloudtentaclesConfig(payload) + const result = await cloudtentaclesRequest(config.knapsackPath, { + ...config, + method: 'GET', + token, + contentType: 'application/json', + businessErrorStatusCode: 401, + businessErrorCode: 'cloudtentacles_knapsack_failed', + }) + + const items = Array.isArray(result.payload?.data) ? result.payload.data.map(mapKnapsackItem) : [] + + return { + baseUrl: config.baseUrl, + itemCount: items.length, + items, + rawItems: Array.isArray(result.payload?.data) ? result.payload.data : [], + } +} + +function mapKnapsackItem(item) { + const source = isPlainObject(item) ? item : {} + + return { + id: Number(source.id || 0), + name: String(source.name || '').trim(), + count: Number(source.count || 0), + image: String(source.image || '').trim(), + categoriesId: Number(source.categories_id || 0), + listingTime: String(source.listing_time || '').trim(), + delistingTime: String(source.delisting_time || '').trim(), + raw: source, + } +} + +function isPlainObject(value) { + return Boolean(value) && typeof value === 'object' && !Array.isArray(value) +} diff --git a/apps/backend/src/services/platforms/cloudtentacles/session-state-service.js b/apps/backend/src/services/platforms/cloudtentacles/session-state-service.js new file mode 100644 index 00000000..5a4c96e9 --- /dev/null +++ b/apps/backend/src/services/platforms/cloudtentacles/session-state-service.js @@ -0,0 +1,77 @@ +// @ts-check + +import fs from 'node:fs' +import path from 'node:path' + +import { PROJECT_ROOT } from '../../../config/runtime.js' + +const CLOUDTENTACLES_SESSION_FILE_PATH = path.join(PROJECT_ROOT, 'data', 'cloudtentacles-session.json') + +export function getCloudtentaclesSessionFilePath() { + return CLOUDTENTACLES_SESSION_FILE_PATH +} + +export function getCloudtentaclesSessionState() { + return loadCloudtentaclesSessionStateFromFile() +} + +export function saveCloudtentaclesSessionState(rawValue) { + const normalized = normalizeCloudtentaclesSessionState(rawValue) + fs.mkdirSync(path.dirname(CLOUDTENTACLES_SESSION_FILE_PATH), { recursive: true }) + fs.writeFileSync(CLOUDTENTACLES_SESSION_FILE_PATH, `${JSON.stringify(normalized, null, 2)}\n`, 'utf8') + return normalized +} + +export function clearCloudtentaclesSessionState() { + const cleared = createDefaultCloudtentaclesSessionState() + saveCloudtentaclesSessionState(cleared) + return cleared +} + +function loadCloudtentaclesSessionStateFromFile() { + if (!fs.existsSync(CLOUDTENTACLES_SESSION_FILE_PATH)) { + return createDefaultCloudtentaclesSessionState() + } + + try { + const rawText = fs.readFileSync(CLOUDTENTACLES_SESSION_FILE_PATH, 'utf8') + return normalizeCloudtentaclesSessionState(JSON.parse(rawText)) + } catch { + return createDefaultCloudtentaclesSessionState() + } +} + +function normalizeCloudtentaclesSessionState(rawValue) { + const source = isPlainObject(rawValue) ? rawValue : {} + + return { + token: String(source.token || '').trim(), + baseUrl: String(source.baseUrl || '').trim(), + username: String(source.username || '').trim(), + phone: String(source.phone || '').trim(), + loggedInAt: String(source.loggedInAt || '').trim(), + deviceId: String(source.deviceId || '-').trim() || '-', + deviceType: normalizeInteger(source.deviceType, 0), + } +} + +function createDefaultCloudtentaclesSessionState() { + return { + token: '', + baseUrl: '', + username: '', + phone: '', + loggedInAt: '', + 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]' +} diff --git a/apps/backend/src/services/platforms/cloudtentacles/shared.js b/apps/backend/src/services/platforms/cloudtentacles/shared.js index b0d45e75..e21c2f91 100644 --- a/apps/backend/src/services/platforms/cloudtentacles/shared.js +++ b/apps/backend/src/services/platforms/cloudtentacles/shared.js @@ -14,6 +14,16 @@ export function resolveCloudtentaclesConfig(overrides = {}) { userInfoPath: '/user/info', assetPath: '/user/get_asset', permissionPath: '/user/get_permission', + categoriesPath: '/categories/get', + skuListPath: '/sku/list', + skuBuyPath: '/sku/buy', + knapsackPath: '/user/get_knapsack', + vnListPath: '/vn/list', + vnAppointPath: '/vn/appoint', + vnGenerateLoginCodePath: '/vn/generate_login_code', + vnVerifCodePath: '/public/vn_verif_code', + vnVerifyLoginCodePath: '/vn/verify_login_code', + vnBindUrlPath: '/vn/bind_url', publicKeyPem: '', clientSource: 'ct-client', deviceId: '-', @@ -28,6 +38,22 @@ export function resolveCloudtentaclesConfig(overrides = {}) { 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'), + categoriesPath: normalizePath(overrides.categoriesPath || baseConfig.categoriesPath, '/categories/get'), + skuListPath: normalizePath(overrides.skuListPath || baseConfig.skuListPath, '/sku/list'), + skuBuyPath: normalizePath(overrides.skuBuyPath || baseConfig.skuBuyPath, '/sku/buy'), + knapsackPath: normalizePath(overrides.knapsackPath || baseConfig.knapsackPath, '/user/get_knapsack'), + vnListPath: normalizePath(overrides.vnListPath || baseConfig.vnListPath, '/vn/list'), + vnAppointPath: normalizePath(overrides.vnAppointPath || baseConfig.vnAppointPath, '/vn/appoint'), + vnGenerateLoginCodePath: normalizePath( + overrides.vnGenerateLoginCodePath || baseConfig.vnGenerateLoginCodePath, + '/vn/generate_login_code', + ), + vnVerifCodePath: normalizePath(overrides.vnVerifCodePath || baseConfig.vnVerifCodePath, '/public/vn_verif_code'), + vnVerifyLoginCodePath: normalizePath( + overrides.vnVerifyLoginCodePath || baseConfig.vnVerifyLoginCodePath, + '/vn/verify_login_code', + ), + vnBindUrlPath: normalizePath(overrides.vnBindUrlPath || baseConfig.vnBindUrlPath, '/vn/bind_url'), publicKeyPem: normalizePem(overrides.publicKeyPem || baseConfig.publicKeyPem), clientSource: String(overrides.clientSource || baseConfig.clientSource || 'ct-client').trim() || 'ct-client', deviceId: String(overrides.deviceId ?? baseConfig.deviceId ?? '-').trim() || '-', diff --git a/apps/backend/src/services/platforms/cloudtentacles/virtual-number-service.js b/apps/backend/src/services/platforms/cloudtentacles/virtual-number-service.js new file mode 100644 index 00000000..8152e10a --- /dev/null +++ b/apps/backend/src/services/platforms/cloudtentacles/virtual-number-service.js @@ -0,0 +1,222 @@ +// @ts-check + +import { createHttpError } from '../../../utils/http.js' +import { cloudtentaclesRequest } from './http-client.js' +import { resolveCloudtentaclesConfig } from './shared.js' + +export async function listCloudtentaclesVirtualNumbers(payload = {}) { + const token = requireToken(payload.token, 'cloudtentacles 虚拟号列表缺少 token', 'cloudtentacles_vn_list_missing_token') + const key = requireKey(payload.key, 'cloudtentacles 虚拟号列表缺少 key', 'cloudtentacles_vn_list_missing_key') + const config = resolveCloudtentaclesConfig(payload) + + const result = await cloudtentaclesRequest(config.vnListPath, { + ...config, + method: 'POST', + token, + body: { key }, + businessErrorStatusCode: 401, + businessErrorCode: 'cloudtentacles_vn_list_failed', + }) + + const items = Array.isArray(result.payload?.data) ? result.payload.data.map(mapVirtualNumberItem) : [] + + return { + baseUrl: config.baseUrl, + key, + itemCount: items.length, + items, + rawItems: Array.isArray(result.payload?.data) ? result.payload.data : [], + } +} + +export async function appointCloudtentaclesVirtualNumber(payload = {}) { + const token = requireToken(payload.token, 'cloudtentacles 申请虚拟号缺少 token', 'cloudtentacles_vn_appoint_missing_token') + const key = requireKey(payload.key, 'cloudtentacles 申请虚拟号缺少 key', 'cloudtentacles_vn_appoint_missing_key') + const config = resolveCloudtentaclesConfig(payload) + + const result = await cloudtentaclesRequest(config.vnAppointPath, { + ...config, + method: 'POST', + token, + body: { key }, + businessErrorStatusCode: 401, + businessErrorCode: 'cloudtentacles_vn_appoint_failed', + }) + + return { + baseUrl: config.baseUrl, + key, + item: mapVirtualNumberItem(result.payload?.data), + rawItem: isPlainObject(result.payload?.data) ? result.payload.data : {}, + } +} + +export async function generateCloudtentaclesLoginCode(payload = {}) { + const token = requireToken(payload.token, 'cloudtentacles 生成登录码缺少 token', 'cloudtentacles_vn_generate_code_missing_token') + const key = requireKey(payload.key, 'cloudtentacles 生成登录码缺少 key', 'cloudtentacles_vn_generate_code_missing_key') + const id = requireId(payload.id, 'cloudtentacles 生成登录码缺少 id', 'cloudtentacles_vn_generate_code_missing_id') + const config = resolveCloudtentaclesConfig(payload) + + const result = await cloudtentaclesRequest(config.vnGenerateLoginCodePath, { + ...config, + method: 'POST', + token, + body: { key, id }, + businessErrorStatusCode: 401, + businessErrorCode: 'cloudtentacles_vn_generate_code_failed', + }) + + return { + baseUrl: config.baseUrl, + key, + id, + responseMessage: String(result.payload?.message || result.payload?.msg || 'success'), + } +} + +export async function fetchCloudtentaclesVirtualNumberCode(payload = {}) { + const token = requireToken(payload.token, 'cloudtentacles 获取验证码缺少 token', 'cloudtentacles_vn_verif_code_missing_token') + const key = requireKey(payload.key, 'cloudtentacles 获取验证码缺少 key', 'cloudtentacles_vn_verif_code_missing_key') + const phone = String(payload.phone || '').trim() + if (!phone) { + throw createHttpError('cloudtentacles 获取验证码缺少手机号', { + statusCode: 400, + errorCode: 'cloudtentacles_vn_verif_code_missing_phone', + }) + } + + const config = resolveCloudtentaclesConfig(payload) + const result = await cloudtentaclesRequest(config.vnVerifCodePath, { + ...config, + method: 'POST', + token, + body: { key, phone }, + businessErrorStatusCode: 401, + businessErrorCode: 'cloudtentacles_vn_verif_code_failed', + }) + + return { + baseUrl: config.baseUrl, + key, + phone, + code: String(result.payload?.data || '').trim(), + responseMessage: String(result.payload?.message || result.payload?.msg || 'success'), + } +} + +export async function verifyCloudtentaclesLoginCode(payload = {}) { + const token = requireToken(payload.token, 'cloudtentacles 校验登录码缺少 token', 'cloudtentacles_vn_verify_code_missing_token') + const key = requireKey(payload.key, 'cloudtentacles 校验登录码缺少 key', 'cloudtentacles_vn_verify_code_missing_key') + const id = requireId(payload.id, 'cloudtentacles 校验登录码缺少 id', 'cloudtentacles_vn_verify_code_missing_id') + const code = String(payload.code || '').trim() + if (!code) { + throw createHttpError('cloudtentacles 校验登录码缺少验证码', { + statusCode: 400, + errorCode: 'cloudtentacles_vn_verify_code_missing_code', + }) + } + + const config = resolveCloudtentaclesConfig(payload) + const result = await cloudtentaclesRequest(config.vnVerifyLoginCodePath, { + ...config, + method: 'POST', + token, + body: { key, id, code }, + businessErrorStatusCode: 401, + businessErrorCode: 'cloudtentacles_vn_verify_code_failed', + }) + + return { + baseUrl: config.baseUrl, + key, + id, + code, + responseMessage: String(result.payload?.message || result.payload?.msg || 'success'), + } +} + +export async function getCloudtentaclesBindUrl(payload = {}) { + const token = requireToken(payload.token, 'cloudtentacles 获取兑换链接缺少 token', 'cloudtentacles_vn_bind_url_missing_token') + const key = requireKey(payload.key, 'cloudtentacles 获取兑换链接缺少 key', 'cloudtentacles_vn_bind_url_missing_key') + const id = requireId(payload.id, 'cloudtentacles 获取兑换链接缺少 id', 'cloudtentacles_vn_bind_url_missing_id') + const config = resolveCloudtentaclesConfig(payload) + + const result = await cloudtentaclesRequest(config.vnBindUrlPath, { + ...config, + method: 'POST', + token, + body: { id, key }, + businessErrorStatusCode: 401, + businessErrorCode: 'cloudtentacles_vn_bind_url_failed', + }) + + return { + baseUrl: config.baseUrl, + key, + id, + bindUrl: String(result.payload?.data || '').trim(), + responseMessage: String(result.payload?.message || result.payload?.msg || 'success'), + } +} + +function mapVirtualNumberItem(item) { + const source = isPlainObject(item) ? item : {} + const bindInfoText = String(source.bind_info || '').trim() + + return { + id: Number(source.id || 0), + phone: String(source.phone || '').trim(), + status: Number(source.status || 0), + countTime: Number(source.count_time || 0), + bindInfo: tryParseJson(bindInfoText) || bindInfoText || null, + raw: source, + } +} + +function requireToken(value, message, errorCode) { + const token = String(value || '').trim() + if (!token) { + throw createHttpError(message, { + statusCode: 400, + errorCode, + }) + } + + return token +} + +function requireKey(value, message, errorCode) { + const key = String(value || '').trim() + if (!key) { + throw createHttpError(message, { + statusCode: 400, + errorCode, + }) + } + + return key +} + +function requireId(value, message, errorCode) { + const id = Number(value) + if (!Number.isInteger(id) || id <= 0) { + throw createHttpError(message, { + statusCode: 400, + errorCode, + }) + } + + return id +} + +function tryParseJson(text) { + try { + return JSON.parse(text) + } catch { + return null + } +} + +function isPlainObject(value) { + return Boolean(value) && typeof value === 'object' && !Array.isArray(value) +} diff --git a/apps/backend/src/types/admin-route-inputs.js b/apps/backend/src/types/admin-route-inputs.js index 4aa31ab4..c66d9419 100644 --- a/apps/backend/src/types/admin-route-inputs.js +++ b/apps/backend/src/types/admin-route-inputs.js @@ -62,6 +62,18 @@ export {} * @typedef {import('./admin-write-inputs.js').AdminCloudtentaclesValidateSessionInput} AdminCloudtentaclesValidateSessionRouteBody */ +/** + * @typedef {import('./admin-write-inputs.js').AdminCloudtentaclesCatalogQueryInput} AdminCloudtentaclesCatalogQueryRouteBody + */ + +/** + * @typedef {import('./admin-write-inputs.js').AdminCloudtentaclesSkuBuyInput} AdminCloudtentaclesSkuBuyRouteBody + */ + +/** + * @typedef {import('./admin-write-inputs.js').AdminCloudtentaclesVirtualNumberInput} AdminCloudtentaclesVirtualNumberRouteBody + */ + /** * @typedef {import('./admin-write-inputs.js').AdminFulfillmentBindingConfigSaveInput} AdminFulfillmentBindingConfigRouteBody */ diff --git a/apps/backend/src/types/admin-write-inputs.js b/apps/backend/src/types/admin-write-inputs.js index 729f0142..7c69371f 100644 --- a/apps/backend/src/types/admin-write-inputs.js +++ b/apps/backend/src/types/admin-write-inputs.js @@ -147,6 +147,39 @@ export {} * }} AdminCloudtentaclesValidateSessionInput */ +/** + * @typedef {{ + * baseUrl?: string + * token?: string + * deviceId?: string + * deviceType?: number | string + * }} AdminCloudtentaclesCatalogQueryInput + */ + +/** + * @typedef {{ + * baseUrl?: string + * token?: string + * deviceId?: string + * deviceType?: number | string + * id?: number | string + * count?: number | string + * }} AdminCloudtentaclesSkuBuyInput + */ + +/** + * @typedef {{ + * baseUrl?: string + * token?: string + * deviceId?: string + * deviceType?: number | string + * key?: string + * id?: number | string + * phone?: string + * code?: string + * }} AdminCloudtentaclesVirtualNumberInput + */ + /** * @typedef {{ * provider?: string diff --git a/apps/backend/src/types/runtime-config.js b/apps/backend/src/types/runtime-config.js index 424d99d8..38290841 100644 --- a/apps/backend/src/types/runtime-config.js +++ b/apps/backend/src/types/runtime-config.js @@ -104,6 +104,16 @@ export {} * userInfoPath: string * assetPath: string * permissionPath: string + * categoriesPath: string + * skuListPath: string + * skuBuyPath: string + * knapsackPath: string + * vnListPath: string + * vnAppointPath: string + * vnGenerateLoginCodePath: string + * vnVerifCodePath: string + * vnVerifyLoginCodePath: string + * vnBindUrlPath: string * publicKeyPem: string * clientSource: string * deviceId: string diff --git a/apps/frontend/src/services/admin.ts b/apps/frontend/src/services/admin.ts index 6693161e..6b5e897e 100644 --- a/apps/frontend/src/services/admin.ts +++ b/apps/frontend/src/services/admin.ts @@ -5,6 +5,7 @@ import type { AdminAgisoShopConfigItem, AdminAuditLogItem, AdminCloudtentaclesLoginTestResult, + AdminCloudtentaclesPersistedSession, AdminCloudtentaclesSendSmsResult, AdminCloudtentaclesSourceConfig, AdminCloudtentaclesValidateSessionResult, @@ -152,14 +153,18 @@ export function syncAdminKhhaoOrders(payload: { export function fetchAdminCloudtentaclesSourceConfig() { return apiGet<{ filePath: string + sessionFilePath: string source: AdminCloudtentaclesSourceConfig + session: AdminCloudtentaclesPersistedSession }>('/api/v1/admin/platform-config/cloudtentacles-source') } export function saveAdminCloudtentaclesSourceConfig(payload: AdminCloudtentaclesSourceConfig) { return apiPost<{ filePath: string + sessionFilePath: string source: AdminCloudtentaclesSourceConfig + session: AdminCloudtentaclesPersistedSession }>('/api/v1/admin/platform-config/cloudtentacles-source', payload as unknown as Record) } @@ -194,6 +199,118 @@ export function validateAdminCloudtentaclesSession(payload: { return apiPost('/api/v1/admin/platform-config/cloudtentacles/validate-session', payload) } +export function fetchAdminCloudtentaclesAsset(payload: { + baseUrl?: string + token?: string + deviceId?: string + deviceType?: number +}) { + return apiPost>('/api/v1/admin/platform-config/cloudtentacles/asset', payload) +} + +export function fetchAdminCloudtentaclesCategories(payload: { + baseUrl?: string + token?: string + deviceId?: string + deviceType?: number +}) { + return apiPost>('/api/v1/admin/platform-config/cloudtentacles/categories', payload) +} + +export function fetchAdminCloudtentaclesSkuList(payload: { + baseUrl?: string + token?: string + deviceId?: string + deviceType?: number +}) { + return apiPost>('/api/v1/admin/platform-config/cloudtentacles/sku/list', payload) +} + +export function buyAdminCloudtentaclesSku(payload: { + baseUrl?: string + token?: string + deviceId?: string + deviceType?: number + id?: number + count?: number +}) { + return apiPost>('/api/v1/admin/platform-config/cloudtentacles/sku/buy', payload) +} + +export function fetchAdminCloudtentaclesKnapsack(payload: { + baseUrl?: string + token?: string + deviceId?: string + deviceType?: number +}) { + return apiPost>('/api/v1/admin/platform-config/cloudtentacles/knapsack', payload) +} + +export function fetchAdminCloudtentaclesVnList(payload: { + baseUrl?: string + token?: string + deviceId?: string + deviceType?: number + key?: string +}) { + return apiPost>('/api/v1/admin/platform-config/cloudtentacles/vn/list', payload) +} + +export function appointAdminCloudtentaclesVn(payload: { + baseUrl?: string + token?: string + deviceId?: string + deviceType?: number + key?: string +}) { + return apiPost>('/api/v1/admin/platform-config/cloudtentacles/vn/appoint', payload) +} + +export function generateAdminCloudtentaclesVnLoginCode(payload: { + baseUrl?: string + token?: string + deviceId?: string + deviceType?: number + key?: string + id?: number +}) { + return apiPost>('/api/v1/admin/platform-config/cloudtentacles/vn/generate-login-code', payload) +} + +export function fetchAdminCloudtentaclesVnCode(payload: { + baseUrl?: string + token?: string + deviceId?: string + deviceType?: number + key?: string + phone?: string +}) { + return apiPost>('/api/v1/admin/platform-config/cloudtentacles/vn/fetch-code', payload) +} + +export function verifyAdminCloudtentaclesVnCode(payload: { + baseUrl?: string + token?: string + deviceId?: string + deviceType?: number + key?: string + id?: number + code?: string +}) { + return apiPost>('/api/v1/admin/platform-config/cloudtentacles/vn/verify-code', payload) +} + +export function fetchAdminCloudtentaclesBindUrl(payload: { + baseUrl?: string + token?: string + deviceId?: string + deviceType?: number + key?: string + id?: number +}) { + return apiPost>('/api/v1/admin/platform-config/cloudtentacles/vn/bind-url', payload) +} + export function fetchAdminFulfillmentBindingConfigs() { return apiGet<{ filePath: string diff --git a/apps/frontend/src/types/admin.ts b/apps/frontend/src/types/admin.ts index cb9a9d9d..ff4624f1 100644 --- a/apps/frontend/src/types/admin.ts +++ b/apps/frontend/src/types/admin.ts @@ -185,6 +185,18 @@ export interface AdminCloudtentaclesSourceConfig { deviceType: number } +export interface AdminCloudtentaclesPersistedSession { + token: string + tokenMasked: string + baseUrl: string + username: string + phoneMasked: string + loggedInAt: string + deviceId: string + deviceType: number + hasToken: boolean +} + export interface AdminCloudtentaclesSendSmsResult { baseUrl: string username: string @@ -198,6 +210,7 @@ export interface AdminCloudtentaclesSessionSummary { tokenMasked: string permissionCount: number permissions: string[] + persisted?: boolean } export interface AdminCloudtentaclesLoginTestResult { diff --git a/apps/frontend/src/views/admin/AdminPlatformShopsView.vue b/apps/frontend/src/views/admin/AdminPlatformShopsView.vue index 7dd55943..756fd8b0 100644 --- a/apps/frontend/src/views/admin/AdminPlatformShopsView.vue +++ b/apps/frontend/src/views/admin/AdminPlatformShopsView.vue @@ -3,9 +3,19 @@ import { computed, onMounted, ref } from 'vue' import { showError, showSuccess } from '@/lib/feedback' import { + appointAdminCloudtentaclesVn, + buyAdminCloudtentaclesSku, + fetchAdminCloudtentaclesAsset, + fetchAdminCloudtentaclesBindUrl, + fetchAdminCloudtentaclesCategories, + fetchAdminCloudtentaclesKnapsack, fetchAdminAgisoShopConfigs, fetchAdminCloudtentaclesSourceConfig, + fetchAdminCloudtentaclesSkuList, + fetchAdminCloudtentaclesVnCode, + fetchAdminCloudtentaclesVnList, fetchAdminKhhaoSourceConfig, + generateAdminCloudtentaclesVnLoginCode, queryAdminKhhaoOrders, saveAdminCloudtentaclesSourceConfig, saveAdminAgisoShopConfigs, @@ -15,12 +25,14 @@ import { testAdminCloudtentaclesLogin, testAdminKhhaoLogin, validateAdminCloudtentaclesSession, + verifyAdminCloudtentaclesVnCode, } from '@/services/admin' import type { AdminAgisoMessagingDefaults, AdminAgisoObservedShopItem, AdminAgisoShopConfigItem, AdminCloudtentaclesLoginTestResult, + AdminCloudtentaclesPersistedSession, AdminCloudtentaclesSendSmsResult, AdminCloudtentaclesSourceConfig, AdminCloudtentaclesValidateSessionResult, @@ -50,6 +62,7 @@ type EditableShop = { } const loading = ref(true) +const isDevMode = import.meta.env.DEV const saving = ref(false) const khhaoSaving = ref(false) const cloudtentaclesSaving = ref(false) @@ -60,6 +73,7 @@ const agisoFilePath = ref('') const khhaoFilePath = ref('') const khhaoStateFilePath = ref('') const cloudtentaclesFilePath = ref('') +const cloudtentaclesSessionFilePath = ref('') const defaults = ref(createEmptyDefaults()) const shops = ref([]) const observedShops = ref([]) @@ -92,6 +106,10 @@ const cloudtentaclesForm = ref({ phone: '', code: '', token: '', + vnKey: '1', + vnId: 0, + skuId: 0, + skuCount: 1, deviceId: '-', deviceType: 0, }) @@ -102,6 +120,9 @@ const cloudtentaclesResultError = ref('') const cloudtentaclesSmsResult = ref(null) const cloudtentaclesLoginResult = ref(null) const cloudtentaclesValidateResult = ref(null) +const cloudtentaclesDebugLoading = ref(false) +const cloudtentaclesDebugResult = ref('') +const cloudtentaclesPersistedSession = ref(null) const agisoStats = computed(() => ({ configuredShopCount: shops.value.length, @@ -203,13 +224,19 @@ async function loadConfigs() { } cloudtentaclesFilePath.value = cloudtentaclesResponse.data.filePath + cloudtentaclesSessionFilePath.value = cloudtentaclesResponse.data.sessionFilePath + cloudtentaclesPersistedSession.value = cloudtentaclesResponse.data.session 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: '', + token: cloudtentaclesResponse.data.session?.token || '', + vnKey: '1', + vnId: 0, + skuId: 0, + skuCount: 1, deviceId: cloudtentaclesResponse.data.source.deviceId || '-', deviceType: cloudtentaclesResponse.data.source.deviceType || 0, } @@ -492,6 +519,9 @@ async function handleCloudtentaclesSaveSource() { try { const response = await saveAdminCloudtentaclesSourceConfig(buildCloudtentaclesConfigPayload()) cloudtentaclesFilePath.value = response.data.filePath + cloudtentaclesSessionFilePath.value = response.data.sessionFilePath + cloudtentaclesPersistedSession.value = response.data.session + cloudtentaclesForm.value.token = response.data.session?.token || '' showSuccess('cloudtentacles 履约平台配置已保存') } catch (error) { cloudtentaclesResultError.value = error instanceof Error ? error.message : 'cloudtentacles 配置保存失败' @@ -533,6 +563,17 @@ async function handleCloudtentaclesTestLogin() { const response = await testAdminCloudtentaclesLogin(buildCloudtentaclesLoginPayload()) cloudtentaclesLoginResult.value = response.data cloudtentaclesForm.value.token = response.data.token || cloudtentaclesForm.value.token + cloudtentaclesPersistedSession.value = { + token: response.data.token || '', + tokenMasked: response.data.session.tokenMasked, + baseUrl: response.data.baseUrl, + username: response.data.username, + phoneMasked: response.data.phoneMasked, + loggedInAt: response.data.loggedInAt, + deviceId: cloudtentaclesForm.value.deviceId.trim() || '-', + deviceType: Number(cloudtentaclesForm.value.deviceType || 0), + hasToken: Boolean(response.data.token), + } showSuccess('cloudtentacles 登录测试成功') } catch (error) { cloudtentaclesResultError.value = error instanceof Error ? error.message : 'cloudtentacles 登录测试失败' @@ -560,6 +601,17 @@ async function handleCloudtentaclesValidateSession() { deviceType: Number(cloudtentaclesForm.value.deviceType || 0), }) cloudtentaclesValidateResult.value = response.data + cloudtentaclesPersistedSession.value = { + token, + tokenMasked: response.data.session.tokenMasked, + baseUrl: response.data.baseUrl, + username: cloudtentaclesPersistedSession.value?.username || cloudtentaclesForm.value.username.trim(), + phoneMasked: cloudtentaclesPersistedSession.value?.phoneMasked || '', + loggedInAt: response.data.loggedInAt, + deviceId: cloudtentaclesForm.value.deviceId.trim() || '-', + deviceType: Number(cloudtentaclesForm.value.deviceType || 0), + hasToken: Boolean(token), + } showSuccess('cloudtentacles 会话校验成功') } catch (error) { cloudtentaclesResultError.value = error instanceof Error ? error.message : 'cloudtentacles 会话校验失败' @@ -569,6 +621,142 @@ async function handleCloudtentaclesValidateSession() { } } +function buildCloudtentaclesTokenPayload() { + return { + baseUrl: cloudtentaclesForm.value.baseUrl.trim() || 'https://123.207.217.176', + token: cloudtentaclesForm.value.token.trim(), + deviceId: cloudtentaclesForm.value.deviceId.trim() || '-', + deviceType: Number(cloudtentaclesForm.value.deviceType || 0), + } +} + +function ensureCloudtentaclesToken() { + if (!cloudtentaclesForm.value.token.trim()) { + cloudtentaclesResultError.value = '请先登录或手动填写 token' + return false + } + return true +} + +function renderCloudtentaclesDebugResult(label: string, data: unknown) { + cloudtentaclesDebugResult.value = `${label}\n${JSON.stringify(data, null, 2)}` +} + +async function runCloudtentaclesDebugAction(label: string, runner: () => Promise<{ data: unknown }>) { + cloudtentaclesDebugLoading.value = true + cloudtentaclesResultError.value = '' + + try { + const response = await runner() + renderCloudtentaclesDebugResult(label, response.data) + showSuccess(`${label}成功`) + } catch (error) { + cloudtentaclesResultError.value = error instanceof Error ? error.message : `${label}失败` + showError(cloudtentaclesResultError.value) + } finally { + cloudtentaclesDebugLoading.value = false + } +} + +async function handleCloudtentaclesFetchAsset() { + if (!ensureCloudtentaclesToken()) return + await runCloudtentaclesDebugAction('查询余额', () => fetchAdminCloudtentaclesAsset(buildCloudtentaclesTokenPayload())) +} + +async function handleCloudtentaclesFetchCategories() { + if (!ensureCloudtentaclesToken()) return + await runCloudtentaclesDebugAction('查询分类', () => fetchAdminCloudtentaclesCategories(buildCloudtentaclesTokenPayload())) +} + +async function handleCloudtentaclesFetchSkuList() { + if (!ensureCloudtentaclesToken()) return + await runCloudtentaclesDebugAction('查询SKU列表', () => fetchAdminCloudtentaclesSkuList(buildCloudtentaclesTokenPayload())) +} + +async function handleCloudtentaclesBuySku() { + if (!ensureCloudtentaclesToken()) return + await runCloudtentaclesDebugAction('购买SKU', () => buyAdminCloudtentaclesSku({ + ...buildCloudtentaclesTokenPayload(), + id: Number(cloudtentaclesForm.value.skuId || 0), + count: Number(cloudtentaclesForm.value.skuCount || 1), + })) +} + +async function handleCloudtentaclesFetchKnapsack() { + if (!ensureCloudtentaclesToken()) return + await runCloudtentaclesDebugAction('查询背包', () => fetchAdminCloudtentaclesKnapsack(buildCloudtentaclesTokenPayload())) +} + +async function handleCloudtentaclesFetchVnList() { + if (!ensureCloudtentaclesToken()) return + await runCloudtentaclesDebugAction('查询虚拟号列表', () => fetchAdminCloudtentaclesVnList({ + ...buildCloudtentaclesTokenPayload(), + key: cloudtentaclesForm.value.vnKey.trim(), + })) +} + +async function handleCloudtentaclesAppointVn() { + if (!ensureCloudtentaclesToken()) return + await runCloudtentaclesDebugAction('申请虚拟号', async () => { + const response = await appointAdminCloudtentaclesVn({ + ...buildCloudtentaclesTokenPayload(), + key: cloudtentaclesForm.value.vnKey.trim(), + }) + const item = (response.data as { item?: { id?: number, phone?: string } }).item + if (item?.id) { + cloudtentaclesForm.value.vnId = Number(item.id) + } + if (item?.phone) { + cloudtentaclesForm.value.phone = String(item.phone) + } + return response + }) +} + +async function handleCloudtentaclesGenerateVnLoginCode() { + if (!ensureCloudtentaclesToken()) return + await runCloudtentaclesDebugAction('生成登录码', () => generateAdminCloudtentaclesVnLoginCode({ + ...buildCloudtentaclesTokenPayload(), + key: cloudtentaclesForm.value.vnKey.trim(), + id: Number(cloudtentaclesForm.value.vnId || 0), + })) +} + +async function handleCloudtentaclesFetchVnCode() { + if (!ensureCloudtentaclesToken()) return + await runCloudtentaclesDebugAction('获取验证码', async () => { + const response = await fetchAdminCloudtentaclesVnCode({ + ...buildCloudtentaclesTokenPayload(), + key: cloudtentaclesForm.value.vnKey.trim(), + phone: cloudtentaclesForm.value.phone.trim(), + }) + const code = (response.data as { code?: string }).code + if (code) { + cloudtentaclesForm.value.code = String(code) + } + return response + }) +} + +async function handleCloudtentaclesVerifyVnCode() { + if (!ensureCloudtentaclesToken()) return + await runCloudtentaclesDebugAction('校验验证码', () => verifyAdminCloudtentaclesVnCode({ + ...buildCloudtentaclesTokenPayload(), + key: cloudtentaclesForm.value.vnKey.trim(), + id: Number(cloudtentaclesForm.value.vnId || 0), + code: cloudtentaclesForm.value.code.trim(), + })) +} + +async function handleCloudtentaclesFetchBindUrl() { + if (!ensureCloudtentaclesToken()) return + await runCloudtentaclesDebugAction('获取兑换链接', () => fetchAdminCloudtentaclesBindUrl({ + ...buildCloudtentaclesTokenPayload(), + key: cloudtentaclesForm.value.vnKey.trim(), + id: Number(cloudtentaclesForm.value.vnId || 0), + })) +} + onMounted(loadConfigs) @@ -631,6 +819,7 @@ onMounted(loadConfigs) type="button" :class="['platform-overview-card', { 'is-active': activePlatform === 'cloudtentacles' }]" @click="switchPlatform('cloudtentacles')" + v-if="isDevMode" >
cloudtentacles @@ -664,6 +853,7 @@ onMounted(loadConfigs) type="button" :class="['switch-chip', { 'is-active': activePlatform === 'cloudtentacles' }]" @click="switchPlatform('cloudtentacles')" + v-if="isDevMode" > cloudtentacles 履约平台 @@ -1047,16 +1237,26 @@ onMounted(loadConfigs) -