整理后台平台配置服务目录结构

This commit is contained in:
yml
2026-05-04 15:25:53 +08:00
parent 132dd75efc
commit d629bc96c8
17 changed files with 76 additions and 76 deletions
@@ -0,0 +1,44 @@
// @ts-check
import { getCloudtentaclesSourceConfig } from '../../platforms/cloudtentacles/source-config-service.js'
import { getCloudtentaclesSessionState } from '../../platforms/cloudtentacles/session-state-service.js'
import { pickFirstNonEmpty, resolveCloudtentaclesAdminContext } from './context.js'
const DEFAULT_CLOUDTENTACLES_BASE_URL = 'https://123.207.217.176'
export function resolveAdminCloudtentaclesCredentialPayload(
payload = {},
options = {},
) {
const savedSource = /** @type {Record<string, unknown>} */ (
options.savedSource || getCloudtentaclesSourceConfig()
)
const defaultBaseUrl = String(options.defaultBaseUrl || DEFAULT_CLOUDTENTACLES_BASE_URL).trim()
return {
baseUrl: pickFirstNonEmpty([payload.baseUrl, savedSource.baseUrl, defaultBaseUrl]),
username: pickFirstNonEmpty([payload.username, savedSource.username]),
password: pickFirstNonEmpty([payload.password, savedSource.password]),
phone: pickFirstNonEmpty([payload.phone, savedSource.phone]),
deviceId: pickFirstNonEmpty([payload.deviceId, savedSource.deviceId, '-']),
deviceType: payload.deviceType ?? savedSource.deviceType,
}
}
export function resolveAdminCloudtentaclesSessionPayload(
payload = {},
options = {},
) {
const savedSource = /** @type {Record<string, unknown>} */ (
options.savedSource || getCloudtentaclesSourceConfig()
)
const persistedSession = /** @type {Record<string, unknown>} */ (
options.persistedSession || getCloudtentaclesSessionState()
)
return resolveCloudtentaclesAdminContext(payload, {
savedSource,
persistedSession,
defaultBaseUrl: options.defaultBaseUrl || DEFAULT_CLOUDTENTACLES_BASE_URL,
})
}
@@ -0,0 +1,65 @@
import test from 'node:test'
import assert from 'node:assert/strict'
import {
resolveAdminCloudtentaclesCredentialPayload,
resolveAdminCloudtentaclesSessionPayload,
} from './cloudtentacles.js'
test('resolveAdminCloudtentaclesCredentialPayload merges payload and saved source defaults', () => {
assert.deepEqual(
resolveAdminCloudtentaclesCredentialPayload(
{
baseUrl: ' https://override.example.com ',
phone: ' 13800138000 ',
deviceId: ' custom-device ',
},
{
savedSource: {
baseUrl: 'https://saved.example.com',
username: 'saved-user',
password: 'saved-pass',
phone: '13900000000',
deviceId: 'saved-device',
deviceType: 2,
},
},
),
{
baseUrl: 'https://override.example.com',
username: 'saved-user',
password: 'saved-pass',
phone: '13800138000',
deviceId: 'custom-device',
deviceType: 2,
},
)
})
test('resolveAdminCloudtentaclesSessionPayload merges payload source and persisted session defaults', () => {
assert.deepEqual(
resolveAdminCloudtentaclesSessionPayload(
{
token: ' payload-token ',
},
{
savedSource: {
baseUrl: 'https://saved.example.com',
deviceId: 'saved-device',
deviceType: 3,
},
persistedSession: {
token: 'session-token',
deviceId: 'session-device',
deviceType: 5,
},
},
),
{
baseUrl: 'https://saved.example.com',
token: 'payload-token',
deviceId: 'saved-device',
deviceType: 3,
},
)
})
@@ -0,0 +1,68 @@
// @ts-check
export function pickFirstNonEmpty(values) {
for (const value of values) {
const normalized = String(value || '').trim()
if (normalized) {
return normalized
}
}
return ''
}
export function isPlainObject(value) {
return Boolean(value) && typeof value === 'object' && !Array.isArray(value)
}
export function resolveKuaishouEticketAdminContext(
payload = {},
options = {},
) {
const savedSource = /** @type {Record<string, unknown>} */ (options.savedSource || {})
const findShopConfig = /** @type {(shopId: string, source: Record<string, unknown>) => unknown} */ (
options.findShopConfig || (() => null)
)
const getFirstAvailableShop = /** @type {(source: Record<string, unknown>) => unknown} */ (
options.getFirstAvailableShop || (() => null)
)
const defaultBaseUrl = String(options.defaultBaseUrl || 'https://s.kwaixiaodian.com').trim()
const requestedShopId = String(payload.shopId || '').trim()
const configuredShop = /** @type {Record<string, unknown> | null} */ (
requestedShopId
? findShopConfig(requestedShopId, savedSource)
: getFirstAvailableShop(savedSource)
)
return {
baseUrl: pickFirstNonEmpty([payload.baseUrl, savedSource.baseUrl, defaultBaseUrl]),
cookie: pickFirstNonEmpty([payload.cookie, configuredShop?.cookie]),
shopId: pickFirstNonEmpty([requestedShopId, configuredShop?.shopId]),
shop: configuredShop,
}
}
export function resolveCloudtentaclesAdminContext(
payload = {},
options = {},
) {
const savedSource = /** @type {Record<string, unknown>} */ (options.savedSource || {})
const persistedSession = /** @type {Record<string, unknown>} */ (options.persistedSession || {})
const defaultBaseUrl = String(options.defaultBaseUrl || 'https://123.207.217.176').trim()
return {
baseUrl: pickFirstNonEmpty([payload.baseUrl, savedSource.baseUrl, defaultBaseUrl]),
token: pickFirstNonEmpty([payload.token, persistedSession.token]),
deviceId: pickFirstNonEmpty([payload.deviceId, savedSource.deviceId, persistedSession.deviceId, '-']),
deviceType: payload.deviceType ?? savedSource.deviceType ?? persistedSession.deviceType,
}
}
export 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)
)
}
@@ -0,0 +1,114 @@
import test from 'node:test'
import assert from 'node:assert/strict'
import {
hasCloudtentaclesCredentialContextChanged,
isPlainObject,
pickFirstNonEmpty,
resolveCloudtentaclesAdminContext,
resolveKuaishouEticketAdminContext,
} from './context.js'
test('pickFirstNonEmpty returns first trimmed non-empty string value', () => {
assert.equal(pickFirstNonEmpty(['', ' ', ' value ', 'fallback']), 'value')
assert.equal(pickFirstNonEmpty([null, undefined, 0, '']), '')
})
test('isPlainObject only accepts non-array objects', () => {
assert.equal(isPlainObject({ a: 1 }), true)
assert.equal(isPlainObject([]), false)
assert.equal(isPlainObject(null), false)
assert.equal(isPlainObject('x'), false)
})
test('resolveKuaishouEticketAdminContext prefers explicit payload values', () => {
const context = resolveKuaishouEticketAdminContext(
{
baseUrl: ' https://override.example.com ',
shopId: ' shop-2 ',
cookie: ' cookie-2 ',
},
{
savedSource: { baseUrl: 'https://saved.example.com' },
findShopConfig(requestedShopId) {
assert.equal(requestedShopId, 'shop-2')
return { shopId: 'shop-2', cookie: 'saved-cookie-2' }
},
getFirstAvailableShop() {
throw new Error('should not use first available shop')
},
},
)
assert.deepEqual(context, {
baseUrl: 'https://override.example.com',
cookie: 'cookie-2',
shopId: 'shop-2',
shop: { shopId: 'shop-2', cookie: 'saved-cookie-2' },
})
})
test('resolveKuaishouEticketAdminContext falls back to first available shop', () => {
const context = resolveKuaishouEticketAdminContext(
{},
{
savedSource: { baseUrl: ' https://saved.example.com ' },
getFirstAvailableShop() {
return { shopId: 'shop-1', cookie: 'cookie-1' }
},
},
)
assert.deepEqual(context, {
baseUrl: 'https://saved.example.com',
cookie: 'cookie-1',
shopId: 'shop-1',
shop: { shopId: 'shop-1', cookie: 'cookie-1' },
})
})
test('resolveCloudtentaclesAdminContext merges payload source and persisted session', () => {
const context = resolveCloudtentaclesAdminContext(
{
token: ' token-1 ',
deviceType: 3,
},
{
savedSource: {
baseUrl: ' https://saved.example.com ',
deviceId: ' device-saved ',
deviceType: 1,
},
persistedSession: {
token: 'token-persisted',
deviceId: 'device-persisted',
deviceType: 2,
},
},
)
assert.deepEqual(context, {
baseUrl: 'https://saved.example.com',
token: 'token-1',
deviceId: 'device-saved',
deviceType: 3,
})
})
test('hasCloudtentaclesCredentialContextChanged detects normalized credential changes', () => {
assert.equal(
hasCloudtentaclesCredentialContextChanged(
{ baseUrl: ' https://a ', username: 'u1', phone: '13812345678', deviceId: 'd1', deviceType: 1 },
{ baseUrl: 'https://a', username: 'u1', phone: '13812345678', deviceId: 'd1', deviceType: 1 },
),
false,
)
assert.equal(
hasCloudtentaclesCredentialContextChanged(
{ baseUrl: 'https://a', username: 'u1', phone: '13812345678', deviceId: 'd1', deviceType: 1 },
{ baseUrl: 'https://b', username: 'u1', phone: '13812345678', deviceId: 'd1', deviceType: 1 },
),
true,
)
})
@@ -0,0 +1,136 @@
// @ts-check
import { normalizeAgisoMessageTemplate } from '../../platforms/agiso/xianyu/message-service.js'
import { maskSecret } from './mappers.js'
export function mapAdminAgisoMessagingDefaults(defaults = {}) {
return {
messageTemplate: normalizeAgisoMessageTemplate(String(defaults.messageTemplate || '').trim()),
autoDeliveryMessageTemplate: normalizeAgisoMessageTemplate(
String(defaults.autoDeliveryMessageTemplate || '').trim(),
),
}
}
export function mapAdminAgisoShopConfigItem(shopId, config = {}) {
return {
shopId,
shopName: String(config.shopName || '').trim(),
accessToken: String(config.accessToken || '').trim(),
accessTokenMasked: maskSecret(config.accessToken),
enabled: typeof config.enabled === 'boolean' ? config.enabled : null,
messageTemplate: normalizeAgisoMessageTemplate(String(config.messageTemplate || '').trim()),
autoDeliveryMessageTemplate: normalizeAgisoMessageTemplate(
String(config.autoDeliveryMessageTemplate || '').trim(),
),
appSecretConfigured: Boolean(String(config.appSecret || '').trim()),
apiVersion: String(config.apiVersion || '').trim(),
sendMessageEndpoint: String(config.sendMessageEndpoint || '').trim(),
}
}
export function applyOptionalStringField(target, key, source) {
if (!source || typeof source[key] !== 'string') {
return
}
const value = String(source[key] || '').trim()
if (value) {
target[key] = normalizeAgisoMessageTemplate(value)
return
}
delete target[key]
}
export function mapAdminFulfillmentBindingConfigItem(item) {
const match = item?.match || {}
return {
provider: String(item?.provider || '').trim(),
platform: String(item?.platform || '').trim(),
shopId: String(item?.shopId || '').trim(),
shopName: String(item?.shopName || '').trim(),
khhaoShopId: String(item?.khhaoShopId || '').trim(),
skuCode: String(item?.skuCode || '').trim(),
skuName: String(item?.skuName || '').trim(),
profileKey: String(item?.profileKey || '').trim(),
enabled: item?.enabled !== false,
priority: Number(item?.priority || 100),
config: item?.config || {},
match: {
externalSkuCode: String(match.externalSkuCode || '').trim(),
externalItemId: String(match.externalItemId || '').trim(),
externalSkuName: String(match.externalSkuName || '').trim(),
config: match.config || {},
},
}
}
export function resolveFulfillmentBindingMatchShopId({
provider = '',
platform = '',
shopId = '',
khhaoShopId = '',
} = {}) {
if (String(provider || '').trim() === 'khhao' && String(platform || '').trim() === 'kuaishou') {
return String(khhaoShopId || shopId || '').trim()
}
return String(shopId || '').trim()
}
export function matchesObservedProduct(binding, observed) {
const provider = String(binding?.provider || '').trim()
const platform = String(binding?.platform || '').trim()
const shopId = String(binding?.shopId || '').trim()
const match = binding?.match || {}
const externalSkuCode = String(match.externalSkuCode || '').trim()
const externalItemId = String(match.externalItemId || '').trim()
const externalSkuName = String(match.externalSkuName || '').trim()
if (provider && provider !== String(observed?.provider || '').trim()) {
return false
}
if (platform && platform !== String(observed?.platform || '').trim()) {
return false
}
if (shopId && shopId !== String(observed?.shopId || '').trim()) {
return false
}
return (
(externalSkuCode && externalSkuCode === String(observed?.externalSkuCode || '').trim())
|| (externalItemId && externalItemId === String(observed?.externalItemId || '').trim())
|| (externalSkuName && externalSkuName === String(observed?.externalSkuName || '').trim())
)
}
export function findMatchingObservedBinding(bindings, observed) {
return (Array.isArray(bindings) ? bindings : []).find((binding) => matchesObservedProduct(binding, observed)) || null
}
export function mapAdminObservedProductItem(item, bindings = []) {
const matchedBinding = findMatchingObservedBinding(bindings, item)
return {
provider: String(item?.provider || '').trim(),
platform: String(item?.platform || '').trim(),
shopId: String(item?.shopId || '').trim(),
shopName: String(item?.shopName || '').trim(),
externalItemId: String(item?.externalItemId || '').trim(),
externalSkuCode: String(item?.externalSkuCode || '').trim(),
externalSkuName: String(item?.externalSkuName || '').trim(),
latestSeenAt: item?.latestSeenAt || null,
orderItemCount: Number(item?.orderItemCount || 0),
configured: Boolean(matchedBinding),
matchedBinding: matchedBinding
? {
skuCode: String(matchedBinding.skuCode || '').trim(),
skuName: String(matchedBinding.skuName || '').trim(),
profileKey: String(matchedBinding.profileKey || '').trim(),
}
: null,
}
}
@@ -0,0 +1,197 @@
import test from 'node:test'
import assert from 'node:assert/strict'
import {
applyOptionalStringField,
findMatchingObservedBinding,
mapAdminAgisoMessagingDefaults,
mapAdminAgisoShopConfigItem,
mapAdminFulfillmentBindingConfigItem,
mapAdminObservedProductItem,
matchesObservedProduct,
resolveFulfillmentBindingMatchShopId,
} from './domain.js'
test('mapAdminAgisoMessagingDefaults normalizes escaped newlines', () => {
assert.deepEqual(
mapAdminAgisoMessagingDefaults({
messageTemplate: ' hello\\nworld ',
autoDeliveryMessageTemplate: ' done\\nnow ',
}),
{
messageTemplate: 'hello\nworld',
autoDeliveryMessageTemplate: 'done\nnow',
},
)
})
test('mapAdminAgisoShopConfigItem masks secrets and reports configured flags', () => {
assert.deepEqual(
mapAdminAgisoShopConfigItem('1001', {
shopName: ' 店铺A ',
accessToken: 'abcdef1234567890',
enabled: true,
messageTemplate: ' hi\\nall ',
autoDeliveryMessageTemplate: ' ok ',
appSecret: 'secret-1',
apiVersion: ' v2 ',
sendMessageEndpoint: ' /send ',
}),
{
shopId: '1001',
shopName: '店铺A',
accessToken: 'abcdef1234567890',
accessTokenMasked: 'abcdef****567890',
enabled: true,
messageTemplate: 'hi\nall',
autoDeliveryMessageTemplate: 'ok',
appSecretConfigured: true,
apiVersion: 'v2',
sendMessageEndpoint: '/send',
},
)
})
test('applyOptionalStringField updates and clears normalized template fields', () => {
const target = { messageTemplate: 'old' }
applyOptionalStringField(target, 'messageTemplate', { messageTemplate: ' next\\nline ' })
assert.deepEqual(target, { messageTemplate: 'next\nline' })
applyOptionalStringField(target, 'messageTemplate', { messageTemplate: ' ' })
assert.deepEqual(target, {})
})
test('mapAdminFulfillmentBindingConfigItem normalizes binding shape', () => {
assert.deepEqual(
mapAdminFulfillmentBindingConfigItem({
provider: ' agiso ',
platform: ' xianyu ',
shopId: ' shop-1 ',
skuCode: ' sku-1 ',
priority: '80',
match: {
externalSkuCode: ' ext-1 ',
},
}),
{
provider: 'agiso',
platform: 'xianyu',
shopId: 'shop-1',
shopName: '',
khhaoShopId: '',
skuCode: 'sku-1',
skuName: '',
profileKey: '',
enabled: true,
priority: 80,
config: {},
match: {
externalSkuCode: 'ext-1',
externalItemId: '',
externalSkuName: '',
config: {},
},
},
)
})
test('resolveFulfillmentBindingMatchShopId prefers khhaoShopId for khhao kuaishou bindings', () => {
assert.equal(
resolveFulfillmentBindingMatchShopId({
provider: 'khhao',
platform: 'kuaishou',
shopId: 'shop-a',
khhaoShopId: 'khhao-1',
}),
'khhao-1',
)
assert.equal(
resolveFulfillmentBindingMatchShopId({
provider: 'agiso',
platform: 'xianyu',
shopId: 'shop-a',
khhaoShopId: 'khhao-1',
}),
'shop-a',
)
})
test('matchesObservedProduct honors provider platform shop and external fields', () => {
const binding = {
provider: 'agiso',
platform: 'xianyu',
shopId: 'shop-1',
match: {
externalSkuCode: 'sku-ext-1',
externalItemId: '',
externalSkuName: '',
},
}
assert.equal(
matchesObservedProduct(binding, {
provider: 'agiso',
platform: 'xianyu',
shopId: 'shop-1',
externalSkuCode: 'sku-ext-1',
}),
true,
)
assert.equal(
matchesObservedProduct(binding, {
provider: 'agiso',
platform: 'xianyu',
shopId: 'shop-2',
externalSkuCode: 'sku-ext-1',
}),
false,
)
})
test('mapAdminObservedProductItem attaches matched binding summary', () => {
const bindings = [
{
provider: 'agiso',
platform: 'xianyu',
shopId: 'shop-1',
skuCode: 'sku-1',
skuName: 'SKU 1',
profileKey: 'profile-1',
match: {
externalSkuCode: 'ext-1',
},
},
]
const observed = {
provider: 'agiso',
platform: 'xianyu',
shopId: 'shop-1',
shopName: '店铺1',
externalItemId: '',
externalSkuCode: 'ext-1',
externalSkuName: '礼包1',
latestSeenAt: '2026-05-04T10:00:00.000Z',
orderItemCount: 2,
}
assert.deepEqual(findMatchingObservedBinding(bindings, observed), bindings[0])
assert.deepEqual(mapAdminObservedProductItem(observed, bindings), {
provider: 'agiso',
platform: 'xianyu',
shopId: 'shop-1',
shopName: '店铺1',
externalItemId: '',
externalSkuCode: 'ext-1',
externalSkuName: '礼包1',
latestSeenAt: '2026-05-04T10:00:00.000Z',
orderItemCount: 2,
configured: true,
matchedBinding: {
skuCode: 'sku-1',
skuName: 'SKU 1',
profileKey: 'profile-1',
},
})
})
@@ -0,0 +1,128 @@
// @ts-check
import { createHttpError } from '../../../utils/http.js'
import { formatFenToAmount } from '../../../utils/money.js'
import { isPlainObject, pickFirstNonEmpty } from './context.js'
import { mapAdminObservedProductItem } from './domain.js'
export function normalizeFulfillmentLookupPayload(payload = {}) {
const provider = String(payload.provider || 'agiso').trim() || 'agiso'
const platform = String(payload.platform || 'xianyu').trim() || 'xianyu'
const shopId = String(payload.shopId || '').trim()
const platformOrderId = String(payload.platformOrderId || '').trim()
if (!shopId) {
throw createHttpError('请先填写店铺 ID', {
statusCode: 400,
errorCode: 'admin_fulfillment_lookup_missing_shop_id',
})
}
if (!platformOrderId) {
throw createHttpError('请先填写平台订单号', {
statusCode: 400,
errorCode: 'admin_fulfillment_lookup_missing_platform_order_id',
})
}
if (provider !== 'agiso' || platform !== 'xianyu') {
throw createHttpError('目前仅支持 Agiso 咸鱼订单手动查询', {
statusCode: 400,
errorCode: 'admin_fulfillment_lookup_platform_not_supported',
})
}
return { provider, platform, shopId, platformOrderId }
}
export function resolveFulfillmentLookupDetail(detailResult, platformOrderId) {
const detail = isPlainObject(detailResult?.parsed) ? detailResult.parsed : {}
const items = Array.isArray(detail.items) ? detail.items : []
if (items.length > 0) {
return { detail, items }
}
const detailReason = String(detailResult?.reason || '').trim()
const detailMessage = String(detailResult?.errorMessage || '').trim()
let message = detailMessage
if (!message && detailReason === 'missing_config') {
message = '当前店铺缺少订单详情查询配置,请先检查 accessToken、appSecret 和详情接口地址'
}
if (!message) {
message = `未查询到订单 ${platformOrderId} 的商品明细`
}
throw createHttpError(message, {
statusCode: 404,
errorCode: 'admin_fulfillment_lookup_order_items_not_found',
})
}
export function buildFulfillmentLookupResult({
provider = '',
platform = '',
shopId = '',
platformOrderId = '',
detail = /** @type {Record<string, unknown>} */ ({}),
detailResult = /** @type {Record<string, unknown>} */ ({}),
bindings = [],
fallbackShopName = '',
} = {}) {
const resolvedShopName = pickFirstNonEmpty([detail.shopName, fallbackShopName])
const items = Array.isArray(detail.items) ? detail.items : []
return {
order: {
provider,
platform,
shopId,
shopName: resolvedShopName,
platformOrderId,
buyerName: String(detail.buyerName || '').trim(),
totalAmountFen: Number(detail.totalAmount || 0),
totalAmount: formatFenToAmount(detail.totalAmount),
paidAt: detail.paidAt || null,
enriched: Boolean(detailResult?.enriched),
enrichReason: String(detailResult?.reason || '').trim(),
errorMessage: String(detailResult?.errorMessage || '').trim(),
},
items: items.map((item, index) => {
const observed = {
provider,
platform,
shopId,
shopName: resolvedShopName,
externalItemId: pickFirstNonEmpty([item?.externalItemId, item?.itemId]),
externalSkuCode: pickFirstNonEmpty([
item?.externalSkuCode,
item?.skuCode,
item?.externalItemId,
item?.itemId,
]),
externalSkuName: pickFirstNonEmpty([item?.externalSkuName, item?.skuName]),
latestSeenAt: null,
orderItemCount: Math.max(1, Number(item?.quantity || 0) || 1),
}
return {
lineId: [
platformOrderId,
index + 1,
observed.externalSkuCode || 'na',
observed.externalItemId || 'na',
].join(':'),
itemTitle: pickFirstNonEmpty([
item?.skuName,
item?.externalSkuName,
item?.externalSkuCode,
item?.externalItemId,
]),
quantity: observed.orderItemCount,
...mapAdminObservedProductItem(observed, bindings),
}
}),
}
}
@@ -0,0 +1,124 @@
import test from 'node:test'
import assert from 'node:assert/strict'
import {
buildFulfillmentLookupResult,
normalizeFulfillmentLookupPayload,
resolveFulfillmentLookupDetail,
} from './fulfillment.js'
test('normalizeFulfillmentLookupPayload validates required fields and defaults provider/platform', () => {
assert.deepEqual(
normalizeFulfillmentLookupPayload({
shopId: ' shop-1 ',
platformOrderId: ' order-1 ',
}),
{
provider: 'agiso',
platform: 'xianyu',
shopId: 'shop-1',
platformOrderId: 'order-1',
},
)
assert.throws(
() => normalizeFulfillmentLookupPayload({ platformOrderId: 'order-1' }),
/请先填写店铺 ID/,
)
})
test('resolveFulfillmentLookupDetail maps missing config into readable error', () => {
assert.throws(
() =>
resolveFulfillmentLookupDetail(
{
reason: 'missing_config',
parsed: { items: [] },
},
'order-1',
),
/当前店铺缺少订单详情查询配置/,
)
})
test('buildFulfillmentLookupResult assembles order and item matching payload', () => {
const result = buildFulfillmentLookupResult({
provider: 'agiso',
platform: 'xianyu',
shopId: 'shop-1',
platformOrderId: 'order-1',
detail: {
shopName: '店铺A',
buyerName: '买家A',
totalAmount: 12345,
paidAt: '2026-05-04T10:00:00.000Z',
items: [
{
externalItemId: 'item-1',
externalSkuCode: 'sku-1',
externalSkuName: '礼包A',
skuName: '礼包A',
quantity: 2,
},
],
},
detailResult: {
enriched: true,
reason: 'ok',
errorMessage: '',
},
bindings: [
{
provider: 'agiso',
platform: 'xianyu',
shopId: 'shop-1',
skuCode: 'internal-1',
skuName: '内部商品',
profileKey: 'profile-1',
match: {
externalSkuCode: 'sku-1',
},
},
],
fallbackShopName: '备用店铺',
})
assert.deepEqual(result, {
order: {
provider: 'agiso',
platform: 'xianyu',
shopId: 'shop-1',
shopName: '店铺A',
platformOrderId: 'order-1',
buyerName: '买家A',
totalAmountFen: 12345,
totalAmount: '123.45',
paidAt: '2026-05-04T10:00:00.000Z',
enriched: true,
enrichReason: 'ok',
errorMessage: '',
},
items: [
{
lineId: 'order-1:1:sku-1:item-1',
itemTitle: '礼包A',
quantity: 2,
provider: 'agiso',
platform: 'xianyu',
shopId: 'shop-1',
shopName: '店铺A',
externalItemId: 'item-1',
externalSkuCode: 'sku-1',
externalSkuName: '礼包A',
latestSeenAt: null,
orderItemCount: 2,
configured: true,
matchedBinding: {
skuCode: 'internal-1',
skuName: '内部商品',
profileKey: 'profile-1',
},
},
],
})
})
@@ -0,0 +1,151 @@
// @ts-check
export function maskSecret(value) {
const normalized = String(value || '').trim()
if (!normalized) {
return ''
}
if (normalized.length <= 10) {
return `${normalized.slice(0, 2)}****${normalized.slice(-2)}`
}
return `${normalized.slice(0, 6)}****${normalized.slice(-6)}`
}
export function maskPhone(value) {
const normalized = String(value || '').trim()
if (!normalized) {
return ''
}
if (normalized.length < 7) {
return normalized
}
return `${normalized.slice(0, 3)}****${normalized.slice(-4)}`
}
export function mapAdminKhhaoSourceConfig(config = {}) {
return {
enabled: config.enabled !== false,
baseUrl: String(config.baseUrl || '').trim(),
username: String(config.username || '').trim(),
password: String(config.password || '').trim(),
maxCaptchaAttempts: Number(config.maxCaptchaAttempts || 3) || 3,
autoSync: {
enabled: config.autoSync?.enabled === true,
intervalMinutes: Number(config.autoSync?.intervalMinutes || 3) || 3,
pages: Number(config.autoSync?.pages || 2) || 2,
pageSize: Number(config.autoSync?.pageSize || 20) || 20,
},
}
}
export function mapAdminKhhaoSyncState(syncState = {}) {
return {
syncFromCreatedAt: String(syncState.syncFromCreatedAt || '').trim(),
lastRunStartedAt: String(syncState.lastRunStartedAt || '').trim(),
lastRunFinishedAt: String(syncState.lastRunFinishedAt || '').trim(),
lastRunStatus: String(syncState.lastRunStatus || '').trim(),
lastErrorMessage: String(syncState.lastErrorMessage || '').trim(),
fetchedCount: Number(syncState.fetchedCount || 0),
syncedCount: Number(syncState.syncedCount || 0),
ignoredCount: Number(syncState.ignoredCount || 0),
watchMode: String(syncState.watchMode || 'normal').trim() || 'normal',
watchOrders: Array.isArray(syncState.watchOrders)
? syncState.watchOrders.map((item) => ({
platformOrderId: String(item?.platformOrderId || '').trim(),
shopId: String(item?.shopId || '').trim(),
shopName: String(item?.shopName || '').trim(),
itemTitle: String(item?.itemTitle || '').trim(),
payStatus: String(item?.payStatus || '').trim(),
detectedAt: String(item?.detectedAt || '').trim(),
lastSeenAt: String(item?.lastSeenAt || '').trim(),
}))
: [],
}
}
export function mapAdminKuaishouEticketShopItem(item = {}) {
return {
shopId: String(item.shopId || '').trim(),
kshopName: String(item.kshopName || '').trim(),
cookie: String(item.cookie || '').trim(),
cookieMasked: maskSecret(item.cookie),
hasCookie: Boolean(String(item.cookie || '').trim()),
userAvatar: String(item.userAvatar || '').trim(),
enabled: item.enabled !== false,
}
}
export function mapAdminKuaishouEticketSourceConfig(config = {}, shops = []) {
return {
enabled: config.enabled !== false,
baseUrl: String(config.baseUrl || '').trim(),
shops: shops.map((item) => mapAdminKuaishouEticketShopItem(item)),
}
}
export function mapAdminCloudtentaclesSourceConfig(config = {}) {
return {
enabled: config.enabled !== false,
baseUrl: String(config.baseUrl || '').trim(),
username: String(config.username || '').trim(),
password: String(config.password || '').trim(),
phone: String(config.phone || '').trim(),
deviceId: String(config.deviceId || '-').trim() || '-',
deviceType: Number(config.deviceType || 0),
}
}
export 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()),
}
}
export function mapAdminKuaishouCloudFulfillmentItem(item = {}) {
return {
id: String(item.id || '').trim(),
enabled: item.enabled !== false,
priority: Number(item.priority || 100) || 100,
provider: String(item.provider || 'khhao').trim() || 'khhao',
platform: String(item.platform || 'kuaishou').trim() || 'kuaishou',
shopId: String(item.shopId || '').trim(),
internalSkuCode: String(item.internalSkuCode || '').trim(),
internalSkuName: String(item.internalSkuName || '').trim(),
externalSkuCode: String(item.externalSkuCode || '').trim(),
externalItemId: String(item.externalItemId || '').trim(),
externalSkuName: String(item.externalSkuName || '').trim(),
resolvedSkuName: String(item.resolvedSkuName || '').trim(),
cloudSourceKey: String(item.cloudSourceKey || 'default').trim() || 'default',
cloudSkuId: Number(item.cloudSkuId || 0) || 0,
cloudSkuName: String(item.cloudSkuName || '').trim(),
vnKey: String(item.vnKey || '').trim(),
autoBuyEnabled: item.autoBuyEnabled !== false,
minAssetReserve: Number(item.minAssetReserve || 0) || 0,
autoReturnNumberAfterDispatch: item.autoReturnNumberAfterDispatch === true,
autoConsumeAfterDispatch: item.autoConsumeAfterDispatch === true,
kuaishouConsumeShopId: String(item.kuaishouConsumeShopId || '').trim(),
kuaishouConsumeShopName: String(item.kuaishouConsumeShopName || '').trim(),
notes: String(item.notes || '').trim(),
}
}
export function mapAdminKuaishouCloudFulfillmentSource(config = {}) {
return {
enabled: config.enabled !== false,
items: (Array.isArray(config.items) ? config.items : []).map((item) =>
mapAdminKuaishouCloudFulfillmentItem(item),
),
}
}
@@ -0,0 +1,196 @@
import test from 'node:test'
import assert from 'node:assert/strict'
import {
mapAdminCloudtentaclesSession,
mapAdminKhhaoSourceConfig,
mapAdminKhhaoSyncState,
mapAdminKuaishouCloudFulfillmentSource,
mapAdminKuaishouEticketSourceConfig,
maskPhone,
maskSecret,
} from './mappers.js'
test('maskSecret preserves edges while hiding middle characters', () => {
assert.equal(maskSecret('abcdef1234567890'), 'abcdef****567890')
assert.equal(maskSecret('12345678'), '12****78')
assert.equal(maskSecret(''), '')
})
test('maskPhone hides middle digits for mobile numbers', () => {
assert.equal(maskPhone('13812345678'), '138****5678')
assert.equal(maskPhone('123456'), '123456')
assert.equal(maskPhone(''), '')
})
test('mapAdminKhhaoSourceConfig normalizes defaults and nested auto sync values', () => {
assert.deepEqual(
mapAdminKhhaoSourceConfig({
enabled: true,
baseUrl: ' https://admin.khhao.com ',
username: ' user ',
password: ' pass ',
maxCaptchaAttempts: '0',
autoSync: {
enabled: true,
intervalMinutes: '',
pages: '0',
pageSize: '50',
},
}),
{
enabled: true,
baseUrl: 'https://admin.khhao.com',
username: 'user',
password: 'pass',
maxCaptchaAttempts: 3,
autoSync: {
enabled: true,
intervalMinutes: 3,
pages: 2,
pageSize: 50,
},
},
)
})
test('mapAdminKhhaoSyncState normalizes watch orders and fallback values', () => {
assert.deepEqual(
mapAdminKhhaoSyncState({
watchMode: '',
fetchedCount: '8',
watchOrders: [
{
platformOrderId: ' A1 ',
shopId: ' S1 ',
shopName: ' 店铺 ',
itemTitle: ' 商品 ',
payStatus: ' unpaid ',
detectedAt: ' t1 ',
lastSeenAt: ' t2 ',
},
],
}),
{
syncFromCreatedAt: '',
lastRunStartedAt: '',
lastRunFinishedAt: '',
lastRunStatus: '',
lastErrorMessage: '',
fetchedCount: 8,
syncedCount: 0,
ignoredCount: 0,
watchMode: 'normal',
watchOrders: [
{
platformOrderId: 'A1',
shopId: 'S1',
shopName: '店铺',
itemTitle: '商品',
payStatus: 'unpaid',
detectedAt: 't1',
lastSeenAt: 't2',
},
],
},
)
})
test('mapAdminKuaishouEticketSourceConfig maps shops and masks cookies', () => {
assert.deepEqual(
mapAdminKuaishouEticketSourceConfig(
{ enabled: true, baseUrl: ' https://s.kwaixiaodian.com ' },
[{ shopId: ' 1 ', kshopName: ' A店 ', cookie: 'cookie-abcdef123456', enabled: true }],
),
{
enabled: true,
baseUrl: 'https://s.kwaixiaodian.com',
shops: [
{
shopId: '1',
kshopName: 'A店',
cookie: 'cookie-abcdef123456',
cookieMasked: 'cookie****123456',
hasCookie: true,
userAvatar: '',
enabled: true,
},
],
},
)
})
test('mapAdminCloudtentaclesSession derives masked fields and token presence', () => {
assert.deepEqual(
mapAdminCloudtentaclesSession({
token: 'abcdef1234567890',
baseUrl: ' https://123.207.217.176 ',
username: ' admin ',
phone: '13812345678',
loggedInAt: ' 2026-05-04T10:00:00.000Z ',
deviceId: ' dev-1 ',
deviceType: '2',
}),
{
token: 'abcdef1234567890',
tokenMasked: 'abcdef****567890',
baseUrl: 'https://123.207.217.176',
username: 'admin',
phoneMasked: '138****5678',
loggedInAt: '2026-05-04T10:00:00.000Z',
deviceId: 'dev-1',
deviceType: 2,
hasToken: true,
},
)
})
test('mapAdminKuaishouCloudFulfillmentSource normalizes items and defaults', () => {
assert.deepEqual(
mapAdminKuaishouCloudFulfillmentSource({
enabled: true,
items: [
{
id: ' item-1 ',
provider: '',
platform: '',
cloudSourceKey: '',
cloudSkuId: '0',
autoConsumeAfterDispatch: true,
kuaishouConsumeShopId: ' ks1 ',
notes: ' note ',
},
],
}),
{
enabled: true,
items: [
{
id: 'item-1',
enabled: true,
priority: 100,
provider: 'khhao',
platform: 'kuaishou',
shopId: '',
internalSkuCode: '',
internalSkuName: '',
externalSkuCode: '',
externalItemId: '',
externalSkuName: '',
resolvedSkuName: '',
cloudSourceKey: 'default',
cloudSkuId: 0,
cloudSkuName: '',
vnKey: '',
autoBuyEnabled: true,
minAssetReserve: 0,
autoReturnNumberAfterDispatch: false,
autoConsumeAfterDispatch: true,
kuaishouConsumeShopId: 'ks1',
kuaishouConsumeShopName: '',
notes: 'note',
},
],
},
)
})
@@ -0,0 +1,54 @@
// @ts-check
import { query } from '../../../db/client.js'
import { mapAdminObservedProductItem } from './domain.js'
export const ADMIN_OBSERVED_PRODUCTS_QUERY = `
SELECT
o.provider,
o.platform,
o.shop_id,
MAX(CASE WHEN trim(o.shop_name) != '' THEN o.shop_name ELSE '' END) AS shop_name,
COALESCE(NULLIF(oi.item_snapshot_json ->> 'externalItemId', ''), '') AS external_item_id,
COALESCE(NULLIF(oi.item_snapshot_json ->> 'externalSkuCode', ''), '') AS external_sku_code,
COALESCE(NULLIF(oi.item_snapshot_json ->> 'externalSkuName', ''), '') AS external_sku_name,
MAX(oi.created_at) AS latest_seen_at,
COUNT(*)::int AS order_item_count
FROM order_items oi
JOIN orders o ON o.id = oi.order_id
WHERE
COALESCE(NULLIF(oi.item_snapshot_json ->> 'externalItemId', ''), '') != ''
OR COALESCE(NULLIF(oi.item_snapshot_json ->> 'externalSkuCode', ''), '') != ''
OR COALESCE(NULLIF(oi.item_snapshot_json ->> 'externalSkuName', ''), '') != ''
GROUP BY
o.provider,
o.platform,
o.shop_id,
external_item_id,
external_sku_code,
external_sku_name
ORDER BY latest_seen_at DESC, o.platform ASC, o.shop_id ASC, external_sku_code ASC
LIMIT 200
`
export function mapAdminObservedProductRow(row = {}) {
return {
provider: String(row.provider || '').trim(),
platform: String(row.platform || '').trim(),
shopId: String(row.shop_id || '').trim(),
shopName: String(row.shop_name || '').trim(),
externalItemId: String(row.external_item_id || '').trim(),
externalSkuCode: String(row.external_sku_code || '').trim(),
externalSkuName: String(row.external_sku_name || '').trim(),
latestSeenAt: row.latest_seen_at || null,
orderItemCount: Number(row.order_item_count || 0),
}
}
export async function listAdminObservedProducts(bindings = [], options = {}) {
const queryImpl = /** @type {(sql: string) => Promise<{ rows: Array<Record<string, unknown>> }>} */ (
options.query || query
)
const rowsResult = await queryImpl(ADMIN_OBSERVED_PRODUCTS_QUERY)
return rowsResult.rows.map((row) => mapAdminObservedProductItem(mapAdminObservedProductRow(row), bindings))
}
@@ -0,0 +1,97 @@
import test from 'node:test'
import assert from 'node:assert/strict'
import {
ADMIN_OBSERVED_PRODUCTS_QUERY,
listAdminObservedProducts,
mapAdminObservedProductRow,
} from './observed-products.js'
test('mapAdminObservedProductRow normalizes query row fields', () => {
assert.deepEqual(
mapAdminObservedProductRow({
provider: ' agiso ',
platform: ' xianyu ',
shop_id: ' shop-1 ',
shop_name: ' 店铺A ',
external_item_id: ' item-1 ',
external_sku_code: ' sku-1 ',
external_sku_name: ' 商品A ',
latest_seen_at: '2026-05-04T12:00:00.000Z',
order_item_count: '3',
}),
{
provider: 'agiso',
platform: 'xianyu',
shopId: 'shop-1',
shopName: '店铺A',
externalItemId: 'item-1',
externalSkuCode: 'sku-1',
externalSkuName: '商品A',
latestSeenAt: '2026-05-04T12:00:00.000Z',
orderItemCount: 3,
},
)
})
test('listAdminObservedProducts queries rows and maps matched binding summary', async () => {
/** @type {string[]} */
const sqlCalls = []
const result = await listAdminObservedProducts(
[
{
provider: 'agiso',
platform: 'xianyu',
shopId: 'shop-1',
skuCode: 'inner-1',
skuName: '内部商品A',
profileKey: 'manual_review',
match: {
externalSkuCode: 'sku-1',
},
},
],
{
async query(sql) {
sqlCalls.push(sql)
return {
rows: [
{
provider: 'agiso',
platform: 'xianyu',
shop_id: 'shop-1',
shop_name: '店铺A',
external_item_id: '',
external_sku_code: 'sku-1',
external_sku_name: '商品A',
latest_seen_at: '2026-05-04T12:00:00.000Z',
order_item_count: 2,
},
],
}
},
},
)
assert.equal(sqlCalls.length, 1)
assert.equal(sqlCalls[0], ADMIN_OBSERVED_PRODUCTS_QUERY)
assert.deepEqual(result, [
{
provider: 'agiso',
platform: 'xianyu',
shopId: 'shop-1',
shopName: '店铺A',
externalItemId: '',
externalSkuCode: 'sku-1',
externalSkuName: '商品A',
latestSeenAt: '2026-05-04T12:00:00.000Z',
orderItemCount: 2,
configured: true,
matchedBinding: {
skuCode: 'inner-1',
skuName: '内部商品A',
profileKey: 'manual_review',
},
},
])
})
@@ -0,0 +1,789 @@
// @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,
resolveKuaishouEticketAdminContext,
} from './context.js'
import {
buildFulfillmentLookupResult,
normalizeFulfillmentLookupPayload,
resolveFulfillmentLookupDetail,
} from './fulfillment.js'
import {
resolveAdminCloudtentaclesCredentialPayload,
resolveAdminCloudtentaclesSessionPayload,
} from './cloudtentacles.js'
import { listAdminObservedProducts } from './observed-products.js'
import {
assertAdminFulfillmentBindingsInput,
buildAdminFulfillmentBindingUniqueKey,
normalizeAdminFulfillmentBindingItem,
} from './validation.js'
import {
applyOptionalStringField,
mapAdminAgisoMessagingDefaults,
mapAdminAgisoShopConfigItem,
mapAdminFulfillmentBindingConfigItem,
} from './domain.js'
import {
mapAdminCloudtentaclesSession,
mapAdminCloudtentaclesSourceConfig,
mapAdminKhhaoSourceConfig,
mapAdminKhhaoSyncState,
mapAdminKuaishouCloudFulfillmentSource,
mapAdminKuaishouEticketSourceConfig,
maskPhone,
maskSecret,
} from './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 result = await sendCloudtentaclesSmsCode(resolveAdminCloudtentaclesCredentialPayload(payload))
return {
...result,
phoneMasked: maskPhone(result.phone),
}
}
/** @param {AdminCloudtentaclesTestLoginInput} [payload] */
export async function testAdminCloudtentaclesLogin(payload = /** @type {AdminCloudtentaclesTestLoginInput} */ ({})) {
const savedSource = getCloudtentaclesSourceConfig()
const credentialContext = resolveAdminCloudtentaclesCredentialPayload(payload, { savedSource })
const session = await loginCloudtentaclesSession({
...credentialContext,
code: String(payload.code || '').trim(),
})
const savedSession = saveCloudtentaclesSessionState({
token: session.token,
baseUrl: session.baseUrl,
username: session.username,
phone: session.phone,
loggedInAt: session.loggedInAt,
deviceId: credentialContext.deviceId,
deviceType: credentialContext.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 sessionContext = resolveAdminCloudtentaclesSessionPayload(payload, { savedSource, persistedSession })
const session = await validateCloudtentaclesSession(sessionContext)
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: sessionContext.deviceId,
deviceType: sessionContext.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} */ ({})) {
return getCloudtentaclesAsset(resolveAdminCloudtentaclesSessionPayload(payload))
}
/** @param {AdminCloudtentaclesCatalogQueryInput} [payload] */
export async function getAdminCloudtentaclesCategories(payload = /** @type {AdminCloudtentaclesCatalogQueryInput} */ ({})) {
return getCloudtentaclesCategories(resolveAdminCloudtentaclesSessionPayload(payload))
}
/** @param {AdminCloudtentaclesCatalogQueryInput} [payload] */
export async function getAdminCloudtentaclesSkuList(payload = /** @type {AdminCloudtentaclesCatalogQueryInput} */ ({})) {
return listCloudtentaclesSku(resolveAdminCloudtentaclesSessionPayload(payload))
}
/** @param {AdminCloudtentaclesSkuBuyInput} [payload] */
export async function buyAdminCloudtentaclesSku(payload = /** @type {AdminCloudtentaclesSkuBuyInput} */ ({})) {
const context = resolveAdminCloudtentaclesSessionPayload(payload)
return buyCloudtentaclesSku({
...context,
id: payload.id,
count: payload.count,
})
}
/** @param {AdminCloudtentaclesSkuUseInput} [payload] */
export async function useAdminCloudtentaclesSku(payload = /** @type {AdminCloudtentaclesSkuUseInput} */ ({})) {
const context = resolveAdminCloudtentaclesSessionPayload(payload)
return useCloudtentaclesSku({
...context,
id: payload.id,
virtualNumberId: payload.virtualNumberId,
phone: payload.phone,
})
}
/** @param {AdminCloudtentaclesCatalogQueryInput} [payload] */
export async function getAdminCloudtentaclesKnapsack(payload = /** @type {AdminCloudtentaclesCatalogQueryInput} */ ({})) {
return getCloudtentaclesKnapsack(resolveAdminCloudtentaclesSessionPayload(payload))
}
/** @param {AdminCloudtentaclesVirtualNumberInput} [payload] */
export async function listAdminCloudtentaclesVirtualNumbers(payload = /** @type {AdminCloudtentaclesVirtualNumberInput} */ ({})) {
const context = resolveAdminCloudtentaclesSessionPayload(payload)
return listCloudtentaclesVirtualNumbers({
...context,
key: payload.key,
})
}
/** @param {AdminCloudtentaclesVirtualNumberInput} [payload] */
export async function appointAdminCloudtentaclesVirtualNumber(payload = /** @type {AdminCloudtentaclesVirtualNumberInput} */ ({})) {
const context = resolveAdminCloudtentaclesSessionPayload(payload)
return appointCloudtentaclesVirtualNumber({
...context,
key: payload.key,
})
}
/** @param {AdminCloudtentaclesVirtualNumberInput} [payload] */
export async function generateAdminCloudtentaclesLoginCode(payload = /** @type {AdminCloudtentaclesVirtualNumberInput} */ ({})) {
const context = resolveAdminCloudtentaclesSessionPayload(payload)
return generateCloudtentaclesLoginCode({
...context,
key: payload.key,
id: payload.id,
})
}
/** @param {AdminCloudtentaclesVirtualNumberInput} [payload] */
export async function fetchAdminCloudtentaclesVirtualNumberCode(payload = /** @type {AdminCloudtentaclesVirtualNumberInput} */ ({})) {
const context = resolveAdminCloudtentaclesSessionPayload(payload)
return fetchCloudtentaclesVirtualNumberCode({
...context,
key: payload.key,
phone: payload.phone,
})
}
/** @param {AdminCloudtentaclesVirtualNumberInput} [payload] */
export async function verifyAdminCloudtentaclesLoginCode(payload = /** @type {AdminCloudtentaclesVirtualNumberInput} */ ({})) {
const context = resolveAdminCloudtentaclesSessionPayload(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 = resolveAdminCloudtentaclesSessionPayload(payload)
return getCloudtentaclesBindUrl({
...context,
key: payload.key,
id: payload.id,
})
}
/** @param {AdminCloudtentaclesVirtualNumberInput} [payload] */
export async function backAdminCloudtentaclesVirtualNumber(payload = /** @type {AdminCloudtentaclesVirtualNumberInput} */ ({})) {
const context = resolveAdminCloudtentaclesSessionPayload(payload)
return backCloudtentaclesVirtualNumber({
...context,
key: payload.key,
id: payload.id,
})
}
/** @param {AdminCloudtentaclesFullFlowInput} [payload] */
export async function runAdminCloudtentaclesFullFlow(payload = /** @type {AdminCloudtentaclesFullFlowInput} */ ({})) {
const context = resolveAdminCloudtentaclesSessionPayload(payload)
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
}
@@ -0,0 +1,137 @@
// @ts-check
import { createHttpError } from '../../../utils/http.js'
import { normalizeProductName } from '../../order/product-match-service.js'
import { isPlainObject } from './context.js'
import { resolveFulfillmentBindingMatchShopId } from './domain.js'
export function assertAdminFulfillmentBindingsInput(bindings) {
if (Array.isArray(bindings)) {
return
}
throw createHttpError('履约配置格式不正确', {
statusCode: 400,
errorCode: 'admin_fulfillment_bindings_invalid_payload',
})
}
export function normalizeAdminFulfillmentBindingItem(
rawBinding,
options = {},
) {
const index = Number(options.index || 0)
const kuaishouEticketSource = /** @type {Record<string, unknown>} */ (options.kuaishouEticketSource || {})
const resolveKuaishouEticketShopConfig = /** @type {(shop: Record<string, unknown>, source: Record<string, unknown>) => Record<string, unknown> | null} */ (
options.resolveKuaishouEticketShopConfig || (() => null)
)
if (!isPlainObject(rawBinding)) {
throw createHttpError(`${index + 1} 条规则格式不正确`, {
statusCode: 400,
errorCode: 'admin_fulfillment_bindings_invalid_item',
})
}
const provider = String(rawBinding.provider || 'agiso').trim() || 'agiso'
const platform = String(rawBinding.platform || '').trim()
let shopId = String(rawBinding.shopId || '').trim()
let shopName = String(rawBinding.shopName || '').trim()
const khhaoShopId = String(rawBinding.khhaoShopId || '').trim()
const skuCode = String(rawBinding.skuCode || '').trim()
const skuName = String(rawBinding.skuName || '').trim()
const profileKey = String(rawBinding.profileKey || '').trim() || 'manual_review'
const match = isPlainObject(rawBinding.match) ? rawBinding.match : {}
const externalItemId = String(match.externalItemId || '').trim()
const externalSkuCode = String(match.externalSkuCode || '').trim()
const externalSkuName = String(match.externalSkuName || '').trim()
const config = isPlainObject(rawBinding.config) ? { ...rawBinding.config } : {}
if (provider === 'khhao' && platform === 'kuaishou') {
if (!khhaoShopId) {
throw createHttpError(`${index + 1} 条快手规则缺少 khhao 店铺 ID`, {
statusCode: 400,
errorCode: 'admin_fulfillment_bindings_missing_khhao_shop_id',
})
}
const matchedShop = resolveKuaishouEticketShopConfig(
{
shopId,
shopName,
},
kuaishouEticketSource,
)
const matchedShopId = String(matchedShop?.shopId || '').trim()
const matchedShopName = String(matchedShop?.kshopName || '').trim()
if (!matchedShopId || !matchedShopName) {
throw createHttpError(
`${index + 1} 条快手规则的店铺未匹配到已配置的快手官方店铺,请先到“平台配置”补齐 Cookie 后再选择`,
{
statusCode: 400,
errorCode: 'admin_fulfillment_bindings_kuaishou_shop_not_configured',
},
)
}
shopId = matchedShopId
shopName = matchedShopName
config.kuaishouShop = {
...(isPlainObject(config.kuaishouShop) ? config.kuaishouShop : {}),
shopId,
shopName,
khhaoShopId,
}
}
if (!skuCode) {
throw createHttpError(`${index + 1} 条规则缺少内部履约 SKU`, {
statusCode: 400,
errorCode: 'admin_fulfillment_bindings_missing_sku_code',
})
}
if (!externalItemId && !externalSkuCode && !externalSkuName) {
throw createHttpError(`${index + 1} 条规则至少需要一种外部匹配条件`, {
statusCode: 400,
errorCode: 'admin_fulfillment_bindings_missing_match_condition',
})
}
return {
provider,
platform,
shopId,
shopName,
khhaoShopId,
skuCode,
skuName,
profileKey,
enabled: rawBinding.enabled !== false,
priority: rawBinding.priority,
config,
match: {
externalSkuCode,
externalItemId,
externalSkuName,
config: isPlainObject(match.config) ? match.config : {},
},
}
}
export function buildAdminFulfillmentBindingUniqueKey(binding) {
return [
String(binding?.provider || '').trim(),
String(binding?.platform || '').trim(),
resolveFulfillmentBindingMatchShopId({
provider: binding?.provider,
platform: binding?.platform,
shopId: binding?.shopId,
khhaoShopId: binding?.khhaoShopId,
}),
String(binding?.match?.externalItemId || '').trim(),
String(binding?.match?.externalSkuCode || '').trim(),
normalizeProductName(String(binding?.match?.externalSkuName || '').trim()),
String(binding?.skuCode || '').trim(),
].join('::')
}
@@ -0,0 +1,119 @@
import test from 'node:test'
import assert from 'node:assert/strict'
import {
assertAdminFulfillmentBindingsInput,
buildAdminFulfillmentBindingUniqueKey,
normalizeAdminFulfillmentBindingItem,
} from './validation.js'
test('assertAdminFulfillmentBindingsInput rejects non-array payloads', () => {
assert.doesNotThrow(() => assertAdminFulfillmentBindingsInput([]))
assert.throws(() => assertAdminFulfillmentBindingsInput({}), /履约配置格式不正确/)
})
test('normalizeAdminFulfillmentBindingItem normalizes kuaishou binding shop mapping', () => {
const result = normalizeAdminFulfillmentBindingItem(
{
provider: 'khhao',
platform: 'kuaishou',
shopId: 'legacy-shop',
shopName: '旧店铺',
khhaoShopId: 'khhao-1',
skuCode: 'sku-1',
match: {
externalSkuCode: 'ext-1',
},
config: {
kuaishouShop: {
note: 'keep',
},
},
},
{
index: 0,
kuaishouEticketSource: { source: true },
resolveKuaishouEticketShopConfig() {
return {
shopId: 'ks-100',
kshopName: '快手店铺A',
}
},
},
)
assert.deepEqual(result, {
provider: 'khhao',
platform: 'kuaishou',
shopId: 'ks-100',
shopName: '快手店铺A',
khhaoShopId: 'khhao-1',
skuCode: 'sku-1',
skuName: '',
profileKey: 'manual_review',
enabled: true,
priority: undefined,
config: {
kuaishouShop: {
note: 'keep',
shopId: 'ks-100',
shopName: '快手店铺A',
khhaoShopId: 'khhao-1',
},
},
match: {
externalSkuCode: 'ext-1',
externalItemId: '',
externalSkuName: '',
config: {},
},
})
})
test('normalizeAdminFulfillmentBindingItem rejects missing sku and match conditions', () => {
assert.throws(
() =>
normalizeAdminFulfillmentBindingItem(
{
provider: 'agiso',
platform: 'xianyu',
match: {
externalSkuCode: 'ext-1',
},
},
{ index: 1 },
),
/第 2 条规则缺少内部履约 SKU/,
)
assert.throws(
() =>
normalizeAdminFulfillmentBindingItem(
{
provider: 'agiso',
platform: 'xianyu',
skuCode: 'sku-1',
},
{ index: 2 },
),
/第 3 条规则至少需要一种外部匹配条件/,
)
})
test('buildAdminFulfillmentBindingUniqueKey uses normalized external sku name and shop id rules', () => {
assert.equal(
buildAdminFulfillmentBindingUniqueKey({
provider: 'khhao',
platform: 'kuaishou',
shopId: 'shop-a',
khhaoShopId: 'khhao-a',
skuCode: 'sku-1',
match: {
externalItemId: '',
externalSkuCode: '',
externalSkuName: ' 礼包 A ',
},
}),
'khhao::kuaishou::khhao-a::::::礼包 a::sku-1',
)
})