Files
order_site/apps/backend/src/services/admin/admin-platform-config-service.js
T

848 lines
33 KiB
JavaScript

// @ts-check
import { query } from '../../db/client.js'
import {
getAgisoMessagingDefaults,
getAgisoShopConfig,
getAgisoShopConfigMap,
getAgisoShopsFilePath,
saveAgisoMessagingConfig,
} from '../platforms/agiso/shop-config-service.js'
import { enrichAgisoXianyuTradeOrder } from '../platforms/agiso/xianyu/order-detail-service.js'
import { mapKhhaoOrderPreviewList } from '../platforms/khhao/order-mapper-service.js'
import { queryKhhaoOrderList } from '../platforms/khhao/order-query-service.js'
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 {
consumeKuaishouEticket,
queryKuaishouEticketConsumeDetail,
} from '../platforms/kuaishou-eticket/consume-service.js'
import {
findKuaishouEticketShopConfig,
getFirstAvailableKuaishouEticketShop,
getKuaishouEticketSourceConfig,
getKuaishouEticketSourceFilePath,
listKuaishouEticketShopConfigs,
resolveKuaishouEticketShopConfig,
saveKuaishouEticketSourceConfig,
} from '../platforms/kuaishou-eticket/source-config-service.js'
import { queryKuaishouEticketStableInfo } from '../platforms/kuaishou-eticket/info-service.js'
import {
getCloudtentaclesSourceConfig,
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,
useCloudtentaclesSku,
} from '../platforms/cloudtentacles/catalog-service.js'
import { getCloudtentaclesKnapsack } from '../platforms/cloudtentacles/knapsack-service.js'
import {
appointCloudtentaclesVirtualNumber,
backCloudtentaclesVirtualNumber,
fetchCloudtentaclesVirtualNumberCode,
generateCloudtentaclesLoginCode,
getCloudtentaclesBindUrl,
listCloudtentaclesVirtualNumbers,
verifyCloudtentaclesLoginCode,
} from '../platforms/cloudtentacles/virtual-number-service.js'
import { runCloudtentaclesFullDebugFlow } from '../platforms/cloudtentacles/debug-flow-service.js'
import {
getOrderFulfillmentBindingConfigs,
getOrderFulfillmentBindingsFilePath,
saveOrderFulfillmentBindingConfigs,
} from '../order/fulfillment-binding-config-service.js'
import {
getKuaishouCloudFulfillmentConfig,
getKuaishouCloudFulfillmentFilePath,
saveKuaishouCloudFulfillmentConfig,
} from '../order/kuaishou-cloud-fulfillment-config-service.js'
import { getFulfillmentProfileByKey } from '../../repositories/fulfillment-profile-repo.js'
import { createHttpError } from '../../utils/http.js'
import { syncConfiguredFulfillmentBindings } from '../bootstrap/fulfillment-bootstrap-service.js'
import { resolveDisplayShopName } from './admin-read-shared-helpers.js'
import {
hasCloudtentaclesCredentialContextChanged,
isPlainObject,
pickFirstNonEmpty,
resolveCloudtentaclesAdminContext,
resolveKuaishouEticketAdminContext,
} from './admin-platform-config-context.js'
import {
buildFulfillmentLookupResult,
normalizeFulfillmentLookupPayload,
resolveFulfillmentLookupDetail,
} from './admin-platform-config-fulfillment.js'
import { listAdminObservedProducts } from './admin-platform-config-observed-products.js'
import {
assertAdminFulfillmentBindingsInput,
buildAdminFulfillmentBindingUniqueKey,
normalizeAdminFulfillmentBindingItem,
} from './admin-platform-config-validation.js'
import {
applyOptionalStringField,
mapAdminAgisoMessagingDefaults,
mapAdminAgisoShopConfigItem,
mapAdminFulfillmentBindingConfigItem,
} from './admin-platform-config-domain.js'
import {
mapAdminCloudtentaclesSession,
mapAdminCloudtentaclesSourceConfig,
mapAdminKhhaoSourceConfig,
mapAdminKhhaoSyncState,
mapAdminKuaishouCloudFulfillmentSource,
mapAdminKuaishouEticketSourceConfig,
maskPhone,
maskSecret,
} from './admin-platform-config-mappers.js'
/** @typedef {import('../../types/admin-write-inputs.js').AdminAgisoShopConfigSaveInput} AdminAgisoShopConfigSaveInput */
/** @typedef {import('../../types/admin-write-inputs.js').AdminKhhaoSourceConfigInput} AdminKhhaoSourceConfigInput */
/** @typedef {import('../../types/admin-write-inputs.js').AdminKuaishouEticketSourceConfigInput} AdminKuaishouEticketSourceConfigInput */
/** @typedef {import('../../types/admin-write-inputs.js').AdminKuaishouEticketShopInfoInput} AdminKuaishouEticketShopInfoInput */
/** @typedef {import('../../types/admin-write-inputs.js').AdminKuaishouEticketDetailQueryInput} AdminKuaishouEticketDetailQueryInput */
/** @typedef {import('../../types/admin-write-inputs.js').AdminKuaishouEticketConsumeInput} AdminKuaishouEticketConsumeInput */
/** @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').AdminKuaishouCloudFulfillmentConfigInput} AdminKuaishouCloudFulfillmentConfigInput */
/** @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').AdminCloudtentaclesSkuUseInput} AdminCloudtentaclesSkuUseInput */
/** @typedef {import('../../types/admin-write-inputs.js').AdminCloudtentaclesVirtualNumberInput} AdminCloudtentaclesVirtualNumberInput */
/** @typedef {import('../../types/admin-write-inputs.js').AdminCloudtentaclesFullFlowInput} AdminCloudtentaclesFullFlowInput */
/** @typedef {import('../../types/admin-write-inputs.js').AdminFulfillmentBindingConfigSaveInput} AdminFulfillmentBindingConfigSaveInput */
/** @typedef {import('../../types/admin-write-inputs.js').AdminFulfillmentBindingLookupInput} AdminFulfillmentBindingLookupInput */
export async function getAdminAgisoShopConfigs() {
const configMap = getAgisoShopConfigMap()
const defaults = getAgisoMessagingDefaults()
const rowsResult = await query(
`
SELECT
shop_id,
MAX(CASE WHEN trim(shop_name) != '' THEN shop_name ELSE '' END) AS detected_shop_name,
MAX(created_at) AS latest_seen_at,
COUNT(*)::int AS webhook_event_count
FROM webhook_events
WHERE provider = 'agiso' AND trim(shop_id) != ''
GROUP BY shop_id
ORDER BY latest_seen_at DESC, shop_id DESC
`,
)
const rows = rowsResult.rows
return {
filePath: getAgisoShopsFilePath(),
defaults: mapAdminAgisoMessagingDefaults(defaults),
shops: Object.entries(configMap)
.sort(([left], [right]) => left.localeCompare(right))
.map(([shopId, config]) => mapAdminAgisoShopConfigItem(shopId, config)),
observedShops: rows.map((row) => ({
shopId: String(row.shop_id || '').trim(),
detectedShopName: String(row.detected_shop_name || '').trim(),
displayShopName: resolveDisplayShopName('agiso', row.shop_id, row.detected_shop_name),
latestSeenAt: row.latest_seen_at || null,
webhookEventCount: Number(row.webhook_event_count || 0),
configured: Boolean(configMap[String(row.shop_id || '').trim()]),
})),
}
}
/** @param {AdminAgisoShopConfigSaveInput} [payload] */
export function updateAdminAgisoShopConfigs(payload = /** @type {AdminAgisoShopConfigSaveInput} */ ({})) {
const rawItems = Array.isArray(payload.shops) ? payload.shops : []
const currentDefaults = getAgisoMessagingDefaults()
const currentMap = getAgisoShopConfigMap()
const nextDefaults = { ...currentDefaults }
const nextMap = {}
applyOptionalStringField(nextDefaults, 'messageTemplate', payload.defaults)
applyOptionalStringField(nextDefaults, 'autoDeliveryMessageTemplate', payload.defaults)
for (const item of rawItems) {
const shopId = String(item?.shopId || '').trim()
if (!shopId) {
continue
}
const current = currentMap[shopId] || {}
const next = { ...current }
const shopName = String(item?.shopName || '').trim()
const accessToken = String(item?.accessToken || '').trim()
const messageTemplate = String(item?.messageTemplate || '').trim()
const autoDeliveryMessageTemplate = String(item?.autoDeliveryMessageTemplate || '').trim()
const appSecret = String(item?.appSecret || '').trim()
const apiVersion = String(item?.apiVersion || '').trim()
const sendMessageEndpoint = String(item?.sendMessageEndpoint || '').trim()
if (shopName) {
next.shopName = shopName
} else {
delete next.shopName
}
if (accessToken) {
next.accessToken = accessToken
}
if (messageTemplate) {
next.messageTemplate = messageTemplate
} else if (typeof item?.messageTemplate === 'string') {
delete next.messageTemplate
}
if (autoDeliveryMessageTemplate) {
next.autoDeliveryMessageTemplate = autoDeliveryMessageTemplate
} else if (typeof item?.autoDeliveryMessageTemplate === 'string') {
delete next.autoDeliveryMessageTemplate
}
if (appSecret) {
next.appSecret = appSecret
}
if (apiVersion) {
next.apiVersion = apiVersion
}
if (sendMessageEndpoint) {
next.sendMessageEndpoint = sendMessageEndpoint
}
if (typeof item?.enabled === 'boolean') {
next.enabled = item.enabled
}
if (!next.accessToken) {
continue
}
nextMap[shopId] = next
}
const saved = saveAgisoMessagingConfig({
defaults: nextDefaults,
shops: nextMap,
})
return {
filePath: getAgisoShopsFilePath(),
defaults: mapAdminAgisoMessagingDefaults(saved.defaults),
shops: Object.entries(saved.shops)
.sort(([left], [right]) => left.localeCompare(right))
.map(([shopId, config]) => mapAdminAgisoShopConfigItem(shopId, config)),
}
}
export function getAdminKhhaoSourceConfig() {
const config = getKhhaoSourceConfig()
const syncState = getKhhaoSyncState()
return {
filePath: getKhhaoSourcesFilePath(),
stateFilePath: getKhhaoSyncStateFilePath(),
source: mapAdminKhhaoSourceConfig(config),
syncState: mapAdminKhhaoSyncState(syncState),
}
}
export function getAdminKuaishouEticketSourceConfig() {
const config = getKuaishouEticketSourceConfig()
return {
filePath: getKuaishouEticketSourceFilePath(),
source: mapAdminKuaishouEticketSourceConfig(config, listKuaishouEticketShopConfigs(config)),
}
}
/** @param {AdminKuaishouEticketSourceConfigInput} [payload] */
export function updateAdminKuaishouEticketSourceConfig(payload = /** @type {AdminKuaishouEticketSourceConfigInput} */ ({})) {
const saved = saveKuaishouEticketSourceConfig({
enabled: payload.enabled !== false,
baseUrl: String(payload.baseUrl || '').trim() || 'https://s.kwaixiaodian.com',
shops: Array.isArray(payload.shops) ? payload.shops : [],
})
return {
filePath: getKuaishouEticketSourceFilePath(),
source: mapAdminKuaishouEticketSourceConfig(saved, listKuaishouEticketShopConfigs(saved)),
}
}
/** @param {AdminKhhaoSourceConfigInput} [payload] */
export function updateAdminKhhaoSourceConfig(payload = /** @type {AdminKhhaoSourceConfigInput} */ ({})) {
const saved = saveKhhaoSourceConfig({
enabled: payload.enabled !== false,
baseUrl: String(payload.baseUrl || '').trim() || 'https://admin.khhao.com',
username: String(payload.username || '').trim(),
password: String(payload.password || '').trim(),
maxCaptchaAttempts: Number(payload.maxCaptchaAttempts || 3) || 3,
autoSync: {
enabled: payload.autoSync?.enabled === true,
intervalMinutes: Number(payload.autoSync?.intervalMinutes || 3) || 3,
pages: Number(payload.autoSync?.pages || 2) || 2,
pageSize: Number(payload.autoSync?.pageSize || 20) || 20,
},
})
const syncState = saved.autoSync?.enabled ? ensureKhhaoSyncBaseline() : getKhhaoSyncState()
return {
filePath: getKhhaoSourcesFilePath(),
stateFilePath: getKhhaoSyncStateFilePath(),
source: mapAdminKhhaoSourceConfig(saved),
syncState: mapAdminKhhaoSyncState(syncState),
}
}
/** @param {AdminKhhaoTestLoginInput} [payload] */
export async function testAdminKhhaoLogin(payload = /** @type {AdminKhhaoTestLoginInput} */ ({})) {
const savedSource = getKhhaoSourceConfig()
const session = await loginKhhaoSession({
baseUrl: String(payload.baseUrl || savedSource.baseUrl || '').trim(),
username: String(payload.username || savedSource.username || '').trim(),
password: String(payload.password || savedSource.password || '').trim(),
maxCaptchaAttempts: payload.maxCaptchaAttempts || savedSource.maxCaptchaAttempts,
includeImageBase64: payload.includeImageBase64,
requestId: `admin-khhao-test-login:${String(payload.username || savedSource.username || '').trim() || 'anonymous'}`,
})
return {
baseUrl: session.baseUrl,
username: session.username,
loggedInAt: session.loggedInAt,
attempt: session.attempt,
responseMessage: session.responseMessage,
captcha: {
recognizedText: session.captchaText,
imageBase64: session.captchaImageBase64,
},
session: {
cookieKeys: Object.keys(session.cookieMap),
cookieCount: Object.keys(session.cookieMap).length,
cookieHeaderMasked: maskSecret(session.cookieHeader),
},
}
}
/** @param {AdminKhhaoOrderQueryInput} [payload] */
export async function queryAdminKhhaoOrders(payload = /** @type {AdminKhhaoOrderQueryInput} */ ({})) {
const savedSource = getKhhaoSourceConfig()
const session = await loginKhhaoSession({
baseUrl: String(payload.baseUrl || savedSource.baseUrl || '').trim(),
username: String(payload.username || savedSource.username || '').trim(),
password: String(payload.password || savedSource.password || '').trim(),
maxCaptchaAttempts: payload.maxCaptchaAttempts || savedSource.maxCaptchaAttempts,
requestId: `admin-khhao-query-orders:${String(payload.username || savedSource.username || '').trim() || 'anonymous'}`,
})
const result = await queryKhhaoOrderList({
baseUrl: String(payload.baseUrl || savedSource.baseUrl || '').trim(),
page: payload.page,
limit: payload.limit,
session,
})
const previews = mapKhhaoOrderPreviewList(result.items)
return {
baseUrl: session.baseUrl,
page: result.page,
limit: result.limit,
total: result.total,
itemCount: result.items.length,
items: previews,
rawItems: result.items,
}
}
/** @param {AdminKhhaoOrderQueryInput} [payload] */
export async function syncAdminKhhaoOrders(payload = /** @type {AdminKhhaoOrderQueryInput} */ ({})) {
const savedSource = getKhhaoSourceConfig()
return syncKhhaoOrders({
...payload,
baseUrl: String(payload.baseUrl || savedSource.baseUrl || '').trim(),
username: String(payload.username || savedSource.username || '').trim(),
password: String(payload.password || savedSource.password || '').trim(),
maxCaptchaAttempts: payload.maxCaptchaAttempts || savedSource.maxCaptchaAttempts,
})
}
/** @param {AdminKuaishouEticketDetailQueryInput} [payload] */
export async function queryAdminKuaishouEticketDetail(payload = /** @type {AdminKuaishouEticketDetailQueryInput} */ ({})) {
const context = resolveKuaishouEticketAdminContext(payload, {
savedSource: getKuaishouEticketSourceConfig(),
findShopConfig: findKuaishouEticketShopConfig,
getFirstAvailableShop: getFirstAvailableKuaishouEticketShop,
})
return queryKuaishouEticketConsumeDetail({
baseUrl: context.baseUrl,
cookie: context.cookie,
eTicketId: String(payload.eTicketId || '').trim(),
})
}
/** @param {AdminKuaishouEticketShopInfoInput} [payload] */
export async function queryAdminKuaishouEticketShopInfo(payload = /** @type {AdminKuaishouEticketShopInfoInput} */ ({})) {
const context = resolveKuaishouEticketAdminContext(payload, {
savedSource: getKuaishouEticketSourceConfig(),
findShopConfig: findKuaishouEticketShopConfig,
getFirstAvailableShop: getFirstAvailableKuaishouEticketShop,
})
return queryKuaishouEticketStableInfo({
baseUrl: context.baseUrl,
cookie: context.cookie,
})
}
/** @param {AdminKuaishouEticketConsumeInput} [payload] */
export async function consumeAdminKuaishouEticket(payload = /** @type {AdminKuaishouEticketConsumeInput} */ ({})) {
const context = resolveKuaishouEticketAdminContext(payload, {
savedSource: getKuaishouEticketSourceConfig(),
findShopConfig: findKuaishouEticketShopConfig,
getFirstAvailableShop: getFirstAvailableKuaishouEticketShop,
})
return consumeKuaishouEticket({
baseUrl: context.baseUrl,
cookie: context.cookie,
eTicketId: String(payload.eTicketId || '').trim(),
oid: String(payload.oid || '').trim(),
formToken: String(payload.formToken || '').trim(),
num: payload.num,
storeId: String(payload.storeId || '').trim(),
})
}
export function getAdminCloudtentaclesSourceConfig() {
const config = getCloudtentaclesSourceConfig()
const session = getCloudtentaclesSessionState()
return {
filePath: getCloudtentaclesSourcesFilePath(),
sessionFilePath: getCloudtentaclesSessionFilePath(),
source: mapAdminCloudtentaclesSourceConfig(config),
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',
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),
})
const shouldClearSession = hasCloudtentaclesCredentialContextChanged(current, saved)
const session = shouldClearSession ? clearCloudtentaclesSessionState() : getCloudtentaclesSessionState()
return {
filePath: getCloudtentaclesSourcesFilePath(),
sessionFilePath: getCloudtentaclesSessionFilePath(),
source: mapAdminCloudtentaclesSourceConfig(saved),
session: mapAdminCloudtentaclesSession(session),
}
}
/** @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,
})
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,
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,
persisted: Boolean(savedSession.token),
},
userInfo: session.userInfo,
asset: session.asset,
}
}
/** @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: 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,
loggedInAt: session.loggedInAt,
session: {
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, {
savedSource: getCloudtentaclesSourceConfig(),
persistedSession: getCloudtentaclesSessionState(),
})
return getCloudtentaclesAsset(context)
}
/** @param {AdminCloudtentaclesCatalogQueryInput} [payload] */
export async function getAdminCloudtentaclesCategories(payload = /** @type {AdminCloudtentaclesCatalogQueryInput} */ ({})) {
const context = resolveCloudtentaclesAdminContext(payload, {
savedSource: getCloudtentaclesSourceConfig(),
persistedSession: getCloudtentaclesSessionState(),
})
return getCloudtentaclesCategories(context)
}
/** @param {AdminCloudtentaclesCatalogQueryInput} [payload] */
export async function getAdminCloudtentaclesSkuList(payload = /** @type {AdminCloudtentaclesCatalogQueryInput} */ ({})) {
const context = resolveCloudtentaclesAdminContext(payload, {
savedSource: getCloudtentaclesSourceConfig(),
persistedSession: getCloudtentaclesSessionState(),
})
return listCloudtentaclesSku(context)
}
/** @param {AdminCloudtentaclesSkuBuyInput} [payload] */
export async function buyAdminCloudtentaclesSku(payload = /** @type {AdminCloudtentaclesSkuBuyInput} */ ({})) {
const context = resolveCloudtentaclesAdminContext(payload, {
savedSource: getCloudtentaclesSourceConfig(),
persistedSession: getCloudtentaclesSessionState(),
})
return buyCloudtentaclesSku({
...context,
id: payload.id,
count: payload.count,
})
}
/** @param {AdminCloudtentaclesSkuUseInput} [payload] */
export async function useAdminCloudtentaclesSku(payload = /** @type {AdminCloudtentaclesSkuUseInput} */ ({})) {
const context = resolveCloudtentaclesAdminContext(payload, {
savedSource: getCloudtentaclesSourceConfig(),
persistedSession: getCloudtentaclesSessionState(),
})
return useCloudtentaclesSku({
...context,
id: payload.id,
virtualNumberId: payload.virtualNumberId,
phone: payload.phone,
})
}
/** @param {AdminCloudtentaclesCatalogQueryInput} [payload] */
export async function getAdminCloudtentaclesKnapsack(payload = /** @type {AdminCloudtentaclesCatalogQueryInput} */ ({})) {
const context = resolveCloudtentaclesAdminContext(payload, {
savedSource: getCloudtentaclesSourceConfig(),
persistedSession: getCloudtentaclesSessionState(),
})
return getCloudtentaclesKnapsack(context)
}
/** @param {AdminCloudtentaclesVirtualNumberInput} [payload] */
export async function listAdminCloudtentaclesVirtualNumbers(payload = /** @type {AdminCloudtentaclesVirtualNumberInput} */ ({})) {
const context = resolveCloudtentaclesAdminContext(payload, {
savedSource: getCloudtentaclesSourceConfig(),
persistedSession: getCloudtentaclesSessionState(),
})
return listCloudtentaclesVirtualNumbers({
...context,
key: payload.key,
})
}
/** @param {AdminCloudtentaclesVirtualNumberInput} [payload] */
export async function appointAdminCloudtentaclesVirtualNumber(payload = /** @type {AdminCloudtentaclesVirtualNumberInput} */ ({})) {
const context = resolveCloudtentaclesAdminContext(payload, {
savedSource: getCloudtentaclesSourceConfig(),
persistedSession: getCloudtentaclesSessionState(),
})
return appointCloudtentaclesVirtualNumber({
...context,
key: payload.key,
})
}
/** @param {AdminCloudtentaclesVirtualNumberInput} [payload] */
export async function generateAdminCloudtentaclesLoginCode(payload = /** @type {AdminCloudtentaclesVirtualNumberInput} */ ({})) {
const context = resolveCloudtentaclesAdminContext(payload, {
savedSource: getCloudtentaclesSourceConfig(),
persistedSession: getCloudtentaclesSessionState(),
})
return generateCloudtentaclesLoginCode({
...context,
key: payload.key,
id: payload.id,
})
}
/** @param {AdminCloudtentaclesVirtualNumberInput} [payload] */
export async function fetchAdminCloudtentaclesVirtualNumberCode(payload = /** @type {AdminCloudtentaclesVirtualNumberInput} */ ({})) {
const context = resolveCloudtentaclesAdminContext(payload, {
savedSource: getCloudtentaclesSourceConfig(),
persistedSession: getCloudtentaclesSessionState(),
})
return fetchCloudtentaclesVirtualNumberCode({
...context,
key: payload.key,
phone: payload.phone,
})
}
/** @param {AdminCloudtentaclesVirtualNumberInput} [payload] */
export async function verifyAdminCloudtentaclesLoginCode(payload = /** @type {AdminCloudtentaclesVirtualNumberInput} */ ({})) {
const context = resolveCloudtentaclesAdminContext(payload, {
savedSource: getCloudtentaclesSourceConfig(),
persistedSession: getCloudtentaclesSessionState(),
})
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, {
savedSource: getCloudtentaclesSourceConfig(),
persistedSession: getCloudtentaclesSessionState(),
})
return getCloudtentaclesBindUrl({
...context,
key: payload.key,
id: payload.id,
})
}
/** @param {AdminCloudtentaclesVirtualNumberInput} [payload] */
export async function backAdminCloudtentaclesVirtualNumber(payload = /** @type {AdminCloudtentaclesVirtualNumberInput} */ ({})) {
const context = resolveCloudtentaclesAdminContext(payload, {
savedSource: getCloudtentaclesSourceConfig(),
persistedSession: getCloudtentaclesSessionState(),
})
return backCloudtentaclesVirtualNumber({
...context,
key: payload.key,
id: payload.id,
})
}
/** @param {AdminCloudtentaclesFullFlowInput} [payload] */
export async function runAdminCloudtentaclesFullFlow(payload = /** @type {AdminCloudtentaclesFullFlowInput} */ ({})) {
const context = resolveCloudtentaclesAdminContext(payload, {
savedSource: getCloudtentaclesSourceConfig(),
persistedSession: getCloudtentaclesSessionState(),
})
return runCloudtentaclesFullDebugFlow({
...context,
skuId: payload.skuId,
skuCount: payload.skuCount,
vnKey: payload.vnKey,
})
}
export async function getAdminFulfillmentBindingConfigs() {
const bindings = getOrderFulfillmentBindingConfigs()
return {
filePath: getOrderFulfillmentBindingsFilePath(),
bindings: bindings.map(mapAdminFulfillmentBindingConfigItem),
observedProducts: await listAdminObservedProducts(bindings),
}
}
export function getAdminKuaishouCloudFulfillmentConfig() {
const config = getKuaishouCloudFulfillmentConfig()
return {
filePath: getKuaishouCloudFulfillmentFilePath(),
source: mapAdminKuaishouCloudFulfillmentSource(config),
}
}
/** @param {AdminKuaishouCloudFulfillmentConfigInput} [payload] */
export async function updateAdminKuaishouCloudFulfillmentConfig(
payload = /** @type {AdminKuaishouCloudFulfillmentConfigInput} */ ({}),
) {
const saved = saveKuaishouCloudFulfillmentConfig({
enabled: payload.enabled !== false,
items: Array.isArray(payload.items) ? payload.items : [],
})
await syncConfiguredFulfillmentBindings()
return {
filePath: getKuaishouCloudFulfillmentFilePath(),
source: mapAdminKuaishouCloudFulfillmentSource(saved),
}
}
/** @param {AdminFulfillmentBindingLookupInput} [payload] */
export async function lookupAdminFulfillmentBindingOrder(payload = /** @type {AdminFulfillmentBindingLookupInput} */ ({})) {
const { provider, platform, shopId, platformOrderId } = normalizeFulfillmentLookupPayload(payload)
const detailResult = await enrichAgisoXianyuTradeOrder({
provider,
platform,
shopId,
shopName: '',
platformOrderId,
orderStatus: 'created',
payStatus: 'unpaid',
buyerId: '',
buyerName: '',
receiverContact: '',
totalAmount: 0,
currency: 'CNY',
paidAt: null,
rawPayload: {},
items: [],
}, {
requestId: `admin-fulfillment-lookup:${shopId}:${platformOrderId}`,
})
const { detail } = resolveFulfillmentLookupDetail(detailResult, platformOrderId)
const bindings = getOrderFulfillmentBindingConfigs()
return buildFulfillmentLookupResult({
provider,
platform,
shopId,
platformOrderId,
detail,
detailResult,
bindings,
fallbackShopName: getAgisoShopConfig(shopId)?.shopName,
})
}
/** @param {AdminFulfillmentBindingConfigSaveInput} [payload] */
export async function updateAdminFulfillmentBindingConfigs(
payload = /** @type {AdminFulfillmentBindingConfigSaveInput} */ ({}),
) {
const bindingsInput = Array.isArray(payload.bindings) ? payload.bindings : []
const normalizedBindings = await validateAdminFulfillmentBindingConfigs(bindingsInput)
const saved = saveOrderFulfillmentBindingConfigs(normalizedBindings)
await syncConfiguredFulfillmentBindings()
return {
filePath: getOrderFulfillmentBindingsFilePath(),
bindings: saved.map(mapAdminFulfillmentBindingConfigItem),
}
}
async function validateAdminFulfillmentBindingConfigs(bindings = []) {
assertAdminFulfillmentBindingsInput(bindings)
const seenKeys = new Set()
const kuaishouEticketSource = getKuaishouEticketSourceConfig()
const normalizedBindings = []
for (const [index, rawBinding] of bindings.entries()) {
const normalized = normalizeAdminFulfillmentBindingItem(rawBinding, {
index,
kuaishouEticketSource,
resolveKuaishouEticketShopConfig,
})
const profile = await getFulfillmentProfileByKey(normalized.profileKey)
if (!profile) {
throw createHttpError(`第 ${index + 1} 条规则使用了不存在的履约方式: ${normalized.profileKey}`, {
statusCode: 400,
errorCode: 'admin_fulfillment_bindings_invalid_profile_key',
})
}
const uniqueKey = buildAdminFulfillmentBindingUniqueKey(normalized)
if (seenKeys.has(uniqueKey)) {
throw createHttpError(`第 ${index + 1} 条规则与其它规则重复,请调整匹配条件或内部履约 SKU`, {
statusCode: 409,
errorCode: 'admin_fulfillment_bindings_duplicate_rule',
})
}
seenKeys.add(uniqueKey)
normalizedBindings.push(normalized)
}
return normalizedBindings
}