移除了 khhao 平台相关
This commit is contained in:
@@ -270,31 +270,6 @@ function applyEnvOverrides(baseConfig) {
|
||||
nextConfig.platforms.ninetyone.cardsEncoding = ninetyoneCardsEncoding
|
||||
}
|
||||
|
||||
const khhaoBaseUrl = String(process.env.KHHAO_BASE_URL || '').trim()
|
||||
if (khhaoBaseUrl) {
|
||||
nextConfig.platforms.khhao.baseUrl = khhaoBaseUrl
|
||||
}
|
||||
|
||||
const khhaoTimeoutMs = parseInteger(process.env.KHHAO_TIMEOUT_MS)
|
||||
if (khhaoTimeoutMs !== null) {
|
||||
nextConfig.platforms.khhao.timeoutMs = khhaoTimeoutMs
|
||||
}
|
||||
|
||||
const khhaoLoginPath = String(process.env.KHHAO_LOGIN_PATH || '').trim()
|
||||
if (khhaoLoginPath) {
|
||||
nextConfig.platforms.khhao.loginPath = khhaoLoginPath
|
||||
}
|
||||
|
||||
const khhaoCaptchaPath = String(process.env.KHHAO_CAPTCHA_PATH || '').trim()
|
||||
if (khhaoCaptchaPath) {
|
||||
nextConfig.platforms.khhao.captchaPath = khhaoCaptchaPath
|
||||
}
|
||||
|
||||
const khhaoOrderListPath = String(process.env.KHHAO_ORDER_LIST_PATH || '').trim()
|
||||
if (khhaoOrderListPath) {
|
||||
nextConfig.platforms.khhao.orderListPath = khhaoOrderListPath
|
||||
}
|
||||
|
||||
const cloudtentaclesBaseUrl = String(process.env.CLOUDTENTACLES_BASE_URL || '').trim()
|
||||
if (cloudtentaclesBaseUrl) {
|
||||
nextConfig.platforms.cloudtentacles.baseUrl = cloudtentaclesBaseUrl
|
||||
|
||||
@@ -9,7 +9,6 @@ import open91Router from './routes/open-91.js'
|
||||
import webhooksRouter from './routes/webhooks.js'
|
||||
import { ensureAdminUsersBootstrapped } from './services/admin/admin-auth-service.js'
|
||||
import { ensureFulfillmentCatalogBootstrapped } from './services/bootstrap/fulfillment-bootstrap-service.js'
|
||||
import { startKhhaoAutoSyncLoop, stopKhhaoAutoSyncLoop } from './services/platforms/khhao/auto-sync-service.js'
|
||||
import { closeLocalOcrWorker, warmupLocalOcrWorker } from './services/session/ocr.js'
|
||||
import { closeAllTencentBrowserSessions, warmupTencentBrowser } from './services/session/session.js'
|
||||
import { buildSuccessPayload } from './utils/http.js'
|
||||
@@ -163,7 +162,6 @@ async function bootstrapCoreServices() {
|
||||
attempt: startupState.core.attemptCount,
|
||||
})
|
||||
|
||||
startKhhaoAutoSyncLoop()
|
||||
void bootstrapBrowser()
|
||||
void bootstrapOcr()
|
||||
break
|
||||
@@ -372,12 +370,6 @@ async function shutdown(signal) {
|
||||
logError('[shutdown]', 'failed to close local OCR worker', error)
|
||||
}
|
||||
|
||||
try {
|
||||
stopKhhaoAutoSyncLoop()
|
||||
} catch (error) {
|
||||
logError('[shutdown]', 'failed to stop khhao auto sync loop', error)
|
||||
}
|
||||
|
||||
if (!server.listening) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -19,7 +19,6 @@ import {
|
||||
useAdminCloudtentaclesSku,
|
||||
getAdminAgisoShopConfigs,
|
||||
getAdminFulfillmentBindingConfigs,
|
||||
getAdminKhhaoSourceConfig,
|
||||
getAdminNinetyoneOrders,
|
||||
consumeAdminKuaishouEticket,
|
||||
failAdminNinetyoneOrder,
|
||||
@@ -27,18 +26,14 @@ import {
|
||||
lookupAdminFulfillmentBindingOrder,
|
||||
queryAdminKuaishouEticketShopInfo,
|
||||
queryAdminKuaishouEticketDetail,
|
||||
queryAdminKhhaoOrders,
|
||||
retryAdminNinetyoneOrder,
|
||||
runAdminCloudtentaclesFullFlow,
|
||||
sendAdminCloudtentaclesSmsCode,
|
||||
syncAdminKhhaoOrders,
|
||||
testAdminCloudtentaclesLogin,
|
||||
testAdminKhhaoLogin,
|
||||
updateAdminKuaishouCloudFulfillmentConfig,
|
||||
updateAdminKuaishouEticketSourceConfig,
|
||||
updateAdminCloudtentaclesSourceConfig,
|
||||
updateAdminAgisoShopConfigs,
|
||||
updateAdminKhhaoSourceConfig,
|
||||
updateAdminFulfillmentBindingConfigs,
|
||||
verifyAdminCloudtentaclesLoginCode,
|
||||
validateAdminCloudtentaclesSession,
|
||||
@@ -48,9 +43,6 @@ import { createJsonHandler, requireAdminRoles } from './shared.js'
|
||||
/** @typedef {import('../../types/admin-route-inputs.js').AdminAgisoShopConfigRouteBody} AdminAgisoShopConfigRouteBody */
|
||||
/** @typedef {import('../../types/admin-route-inputs.js').AdminFulfillmentBindingConfigRouteBody} AdminFulfillmentBindingConfigRouteBody */
|
||||
/** @typedef {import('../../types/admin-route-inputs.js').AdminFulfillmentBindingLookupRouteBody} AdminFulfillmentBindingLookupRouteBody */
|
||||
/** @typedef {import('../../types/admin-route-inputs.js').AdminKhhaoOrderQueryRouteBody} AdminKhhaoOrderQueryRouteBody */
|
||||
/** @typedef {import('../../types/admin-route-inputs.js').AdminKhhaoSourceConfigRouteBody} AdminKhhaoSourceConfigRouteBody */
|
||||
/** @typedef {import('../../types/admin-route-inputs.js').AdminKhhaoTestLoginRouteBody} AdminKhhaoTestLoginRouteBody */
|
||||
/** @typedef {import('../../types/admin-route-inputs.js').AdminEntityRouteParams} AdminEntityRouteParams */
|
||||
/** @typedef {import('../../types/admin-route-inputs.js').AdminKuaishouEticketSourceConfigRouteBody} AdminKuaishouEticketSourceConfigRouteBody */
|
||||
/** @typedef {import('../../types/admin-route-inputs.js').AdminKuaishouEticketDetailQueryRouteBody} AdminKuaishouEticketDetailQueryRouteBody */
|
||||
@@ -133,111 +125,6 @@ router.post('/platform-config/kuaishou-eticket-source', createJsonHandler(
|
||||
},
|
||||
))
|
||||
|
||||
router.get('/platform-config/khhao-source', createJsonHandler(
|
||||
() => getAdminKhhaoSourceConfig(),
|
||||
{
|
||||
successMessage: 'ok',
|
||||
errorMessage: '读取 khhao 来源配置失败',
|
||||
scope: '[admin/platform-config/khhao-source]',
|
||||
},
|
||||
))
|
||||
|
||||
router.post('/platform-config/khhao-source', createJsonHandler(
|
||||
(req) => updateAdminKhhaoSourceConfig(/** @type {AdminKhhaoSourceConfigRouteBody} */ (req.body)),
|
||||
{
|
||||
successMessage: 'khhao 来源配置已保存',
|
||||
errorMessage: '保存 khhao 来源配置失败',
|
||||
scope: '[admin/platform-config/khhao-source]',
|
||||
audit: (_req, data) => {
|
||||
const result = /** @type {{ filePath?: string, source?: { username?: string, enabled?: boolean } }} */ (data)
|
||||
return {
|
||||
action: 'platform_khhao_source_updated',
|
||||
targetType: 'platform_config',
|
||||
targetId: 'khhao_source',
|
||||
data: {
|
||||
filePath: String(result.filePath || '').trim(),
|
||||
username: String(result.source?.username || '').trim(),
|
||||
enabled: Boolean(result.source?.enabled),
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
))
|
||||
|
||||
router.post('/platform-config/khhao/test-login', createJsonHandler(
|
||||
(req) => testAdminKhhaoLogin(/** @type {AdminKhhaoTestLoginRouteBody} */ (req.body)),
|
||||
{
|
||||
successMessage: 'khhao 登录测试成功',
|
||||
errorMessage: 'khhao 登录测试失败',
|
||||
scope: '[admin/platform-config/khhao/test-login]',
|
||||
audit: (req, data) => {
|
||||
const body = /** @type {AdminKhhaoTestLoginRouteBody} */ (req.body)
|
||||
const result = /** @type {{ baseUrl?: string, username?: string, attempt?: number, session?: { cookieCount?: number } }} */ (data)
|
||||
return {
|
||||
action: 'platform_khhao_test_login',
|
||||
targetType: 'platform_config',
|
||||
targetId: String(result.username || body.username || '').trim() || 'khhao',
|
||||
data: {
|
||||
baseUrl: result.baseUrl || String(body.baseUrl || '').trim(),
|
||||
attempt: Number(result.attempt || 0),
|
||||
cookieCount: Number(result.session?.cookieCount || 0),
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
))
|
||||
|
||||
router.post('/platform-config/khhao/query-orders', createJsonHandler(
|
||||
(req) => queryAdminKhhaoOrders(/** @type {AdminKhhaoOrderQueryRouteBody} */ (req.body)),
|
||||
{
|
||||
successMessage: 'khhao 订单查询成功',
|
||||
errorMessage: 'khhao 订单查询失败',
|
||||
scope: '[admin/platform-config/khhao/query-orders]',
|
||||
audit: (req, data) => {
|
||||
const body = /** @type {AdminKhhaoOrderQueryRouteBody} */ (req.body)
|
||||
const result = /** @type {{ page?: number, limit?: number, itemCount?: number, total?: number, baseUrl?: string }} */ (data)
|
||||
return {
|
||||
action: 'platform_khhao_query_orders',
|
||||
targetType: 'platform_config',
|
||||
targetId: String(body.username || '').trim() || 'khhao',
|
||||
data: {
|
||||
baseUrl: result.baseUrl || String(body.baseUrl || '').trim(),
|
||||
page: Number(result.page || 1),
|
||||
limit: Number(result.limit || 50),
|
||||
itemCount: Number(result.itemCount || 0),
|
||||
total: Number(result.total || 0),
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
))
|
||||
|
||||
router.post('/platform-config/khhao/sync-orders', createJsonHandler(
|
||||
(req) => syncAdminKhhaoOrders(/** @type {AdminKhhaoOrderQueryRouteBody} */ (req.body)),
|
||||
{
|
||||
successMessage: 'khhao 订单同步成功',
|
||||
errorMessage: 'khhao 订单同步失败',
|
||||
scope: '[admin/platform-config/khhao/sync-orders]',
|
||||
audit: (req, data) => {
|
||||
const body = /** @type {AdminKhhaoOrderQueryRouteBody} */ (req.body)
|
||||
const result = /** @type {{ page?: number, limit?: number, fetchedCount?: number, syncedCount?: number, ignoredCount?: number, baseUrl?: string }} */ (data)
|
||||
return {
|
||||
action: 'platform_khhao_sync_orders',
|
||||
targetType: 'platform_config',
|
||||
targetId: String(body.username || '').trim() || 'khhao',
|
||||
data: {
|
||||
baseUrl: result.baseUrl || String(body.baseUrl || '').trim(),
|
||||
page: Number(result.page || 1),
|
||||
limit: Number(result.limit || 50),
|
||||
fetchedCount: Number(result.fetchedCount || 0),
|
||||
syncedCount: Number(result.syncedCount || 0),
|
||||
ignoredCount: Number(result.ignoredCount || 0),
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
))
|
||||
|
||||
router.get('/platform-config/ninetyone/orders', createJsonHandler(
|
||||
(req) => getAdminNinetyoneOrders(req.query),
|
||||
{
|
||||
|
||||
@@ -50,7 +50,6 @@ export function mapAdminFulfillmentBindingConfigItem(item) {
|
||||
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(),
|
||||
@@ -66,19 +65,6 @@ export function mapAdminFulfillmentBindingConfigItem(item) {
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
mapAdminFulfillmentBindingConfigItem,
|
||||
mapAdminObservedProductItem,
|
||||
matchesObservedProduct,
|
||||
resolveFulfillmentBindingMatchShopId,
|
||||
} from './domain.js'
|
||||
|
||||
test('mapAdminAgisoMessagingDefaults normalizes escaped newlines', () => {
|
||||
@@ -78,7 +77,6 @@ test('mapAdminFulfillmentBindingConfigItem normalizes binding shape', () => {
|
||||
platform: 'xianyu',
|
||||
shopId: 'shop-1',
|
||||
shopName: '',
|
||||
khhaoShopId: '',
|
||||
skuCode: 'sku-1',
|
||||
skuName: '',
|
||||
profileKey: '',
|
||||
@@ -95,28 +93,6 @@ test('mapAdminFulfillmentBindingConfigItem normalizes binding shape', () => {
|
||||
)
|
||||
})
|
||||
|
||||
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',
|
||||
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
saveOrderFulfillmentBindingConfigs,
|
||||
} from '../../order/fulfillment-binding-config-service.js'
|
||||
import { enrichAgisoXianyuTradeOrder } from '../../platforms/agiso/xianyu/order-detail-service.js'
|
||||
import { getKuaishouEticketSourceConfig, resolveKuaishouEticketShopConfig } from '../../platforms/kuaishou-eticket/source-config-service.js'
|
||||
import { syncConfiguredFulfillmentBindings } from '../../bootstrap/fulfillment-bootstrap-service.js'
|
||||
import { getFulfillmentProfileByKey } from '../../../repositories/fulfillment-profile-repo.js'
|
||||
import { createHttpError } from '../../../utils/http.js'
|
||||
@@ -78,8 +77,6 @@ export async function updateAdminFulfillmentBindingConfigs(
|
||||
) {
|
||||
const bindingsInput = Array.isArray(payload.bindings) ? payload.bindings : []
|
||||
const normalizedBindings = await validateAdminFulfillmentBindingConfigs(bindingsInput, {
|
||||
kuaishouEticketSource: getKuaishouEticketSourceConfig(),
|
||||
resolveKuaishouEticketShopConfig,
|
||||
getFulfillmentProfileByKey,
|
||||
})
|
||||
assertLegacyBindingConfigsOnly(normalizedBindings)
|
||||
|
||||
@@ -1,76 +0,0 @@
|
||||
// @ts-check
|
||||
|
||||
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 {
|
||||
buildAdminKhhaoLoginPayload,
|
||||
buildAdminKhhaoLoginResult,
|
||||
buildAdminKhhaoOrderQueryResult,
|
||||
buildAdminKhhaoSyncPayload,
|
||||
resolveAdminKhhaoCredentialPayload,
|
||||
} from './khhao.js'
|
||||
import { normalizeAdminKhhaoSourceConfigPayload } from './writes.js'
|
||||
import {
|
||||
mapAdminKhhaoSourceConfig,
|
||||
mapAdminKhhaoSyncState,
|
||||
} from './mappers.js'
|
||||
|
||||
/** @typedef {import('../../../types/admin-write-inputs.js').AdminKhhaoSourceConfigInput} AdminKhhaoSourceConfigInput */
|
||||
/** @typedef {import('../../../types/admin-write-inputs.js').AdminKhhaoOrderQueryInput} AdminKhhaoOrderQueryInput */
|
||||
/** @typedef {import('../../../types/admin-write-inputs.js').AdminKhhaoTestLoginInput} AdminKhhaoTestLoginInput */
|
||||
|
||||
export function getAdminKhhaoSourceConfig() {
|
||||
const config = getKhhaoSourceConfig()
|
||||
const syncState = getKhhaoSyncState()
|
||||
|
||||
return {
|
||||
filePath: getKhhaoSourcesFilePath(),
|
||||
stateFilePath: getKhhaoSyncStateFilePath(),
|
||||
source: mapAdminKhhaoSourceConfig(config),
|
||||
syncState: mapAdminKhhaoSyncState(syncState),
|
||||
}
|
||||
}
|
||||
|
||||
/** @param {AdminKhhaoSourceConfigInput} [payload] */
|
||||
export function updateAdminKhhaoSourceConfig(payload = /** @type {AdminKhhaoSourceConfigInput} */ ({})) {
|
||||
const saved = saveKhhaoSourceConfig(normalizeAdminKhhaoSourceConfigPayload(payload))
|
||||
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 session = await loginKhhaoSession(buildAdminKhhaoLoginPayload(payload))
|
||||
return buildAdminKhhaoLoginResult(session)
|
||||
}
|
||||
|
||||
/** @param {AdminKhhaoOrderQueryInput} [payload] */
|
||||
export async function queryAdminKhhaoOrders(payload = /** @type {AdminKhhaoOrderQueryInput} */ ({})) {
|
||||
const credential = resolveAdminKhhaoCredentialPayload(payload)
|
||||
const session = await loginKhhaoSession(buildAdminKhhaoLoginPayload(payload, {
|
||||
requestIdScope: 'query-orders',
|
||||
}))
|
||||
|
||||
const result = await queryKhhaoOrderList({
|
||||
baseUrl: credential.baseUrl,
|
||||
page: payload.page,
|
||||
limit: payload.limit,
|
||||
session,
|
||||
})
|
||||
|
||||
return buildAdminKhhaoOrderQueryResult(session, result)
|
||||
}
|
||||
|
||||
/** @param {AdminKhhaoOrderQueryInput} [payload] */
|
||||
export async function syncAdminKhhaoOrders(payload = /** @type {AdminKhhaoOrderQueryInput} */ ({})) {
|
||||
return syncKhhaoOrders(buildAdminKhhaoSyncPayload(payload))
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
// @ts-check
|
||||
|
||||
import { getKhhaoSourceConfig } from '../../platforms/khhao/source-config-service.js'
|
||||
import { mapKhhaoOrderPreviewList } from '../../platforms/khhao/order-mapper-service.js'
|
||||
import { maskSecret } from './mappers.js'
|
||||
|
||||
export function resolveAdminKhhaoCredentialPayload(
|
||||
payload = {},
|
||||
options = {},
|
||||
) {
|
||||
const savedSource = /** @type {Record<string, unknown>} */ (
|
||||
options.savedSource || getKhhaoSourceConfig()
|
||||
)
|
||||
|
||||
return {
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
export function buildAdminKhhaoLoginPayload(
|
||||
payload = {},
|
||||
options = {},
|
||||
) {
|
||||
const credential = resolveAdminKhhaoCredentialPayload(payload, options)
|
||||
const requestIdScope = String(options.requestIdScope || 'test-login').trim() || 'test-login'
|
||||
|
||||
return {
|
||||
...credential,
|
||||
includeImageBase64: payload.includeImageBase64,
|
||||
requestId: `admin-khhao-${requestIdScope}:${credential.username || 'anonymous'}`,
|
||||
}
|
||||
}
|
||||
|
||||
export function buildAdminKhhaoSyncPayload(
|
||||
payload = {},
|
||||
options = {},
|
||||
) {
|
||||
return {
|
||||
...payload,
|
||||
...resolveAdminKhhaoCredentialPayload(payload, options),
|
||||
}
|
||||
}
|
||||
|
||||
export function buildAdminKhhaoLoginResult(session = {}) {
|
||||
const cookieKeys = Object.keys(session.cookieMap || {})
|
||||
|
||||
return {
|
||||
baseUrl: String(session.baseUrl || '').trim(),
|
||||
username: String(session.username || '').trim(),
|
||||
loggedInAt: String(session.loggedInAt || '').trim(),
|
||||
attempt: Number(session.attempt || 0),
|
||||
responseMessage: String(session.responseMessage || '').trim(),
|
||||
captcha: {
|
||||
recognizedText: String(session.captchaText || '').trim(),
|
||||
imageBase64: String(session.captchaImageBase64 || '').trim(),
|
||||
},
|
||||
session: {
|
||||
cookieKeys,
|
||||
cookieCount: cookieKeys.length,
|
||||
cookieHeaderMasked: maskSecret(session.cookieHeader),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export function buildAdminKhhaoOrderQueryResult(
|
||||
session = {},
|
||||
result = {},
|
||||
) {
|
||||
const items = Array.isArray(result.items) ? result.items : []
|
||||
|
||||
return {
|
||||
baseUrl: String(session.baseUrl || '').trim(),
|
||||
page: Number(result.page || 1),
|
||||
limit: Number(result.limit || 0),
|
||||
total: Number(result.total || 0),
|
||||
itemCount: items.length,
|
||||
items: mapKhhaoOrderPreviewList(items),
|
||||
rawItems: items,
|
||||
}
|
||||
}
|
||||
@@ -1,166 +0,0 @@
|
||||
import test from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
|
||||
import {
|
||||
buildAdminKhhaoLoginPayload,
|
||||
buildAdminKhhaoLoginResult,
|
||||
buildAdminKhhaoOrderQueryResult,
|
||||
buildAdminKhhaoSyncPayload,
|
||||
resolveAdminKhhaoCredentialPayload,
|
||||
} from './khhao.js'
|
||||
|
||||
test('resolveAdminKhhaoCredentialPayload merges payload and saved source defaults', () => {
|
||||
assert.deepEqual(
|
||||
resolveAdminKhhaoCredentialPayload(
|
||||
{
|
||||
baseUrl: ' https://override.example.com ',
|
||||
maxCaptchaAttempts: 5,
|
||||
},
|
||||
{
|
||||
savedSource: {
|
||||
baseUrl: 'https://saved.example.com',
|
||||
username: 'saved-user',
|
||||
password: 'saved-pass',
|
||||
maxCaptchaAttempts: 3,
|
||||
},
|
||||
},
|
||||
),
|
||||
{
|
||||
baseUrl: 'https://override.example.com',
|
||||
username: 'saved-user',
|
||||
password: 'saved-pass',
|
||||
maxCaptchaAttempts: 5,
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
test('buildAdminKhhaoLoginPayload appends request id and includeImageBase64', () => {
|
||||
assert.deepEqual(
|
||||
buildAdminKhhaoLoginPayload(
|
||||
{
|
||||
username: ' user ',
|
||||
includeImageBase64: true,
|
||||
},
|
||||
{
|
||||
requestIdScope: 'query-orders',
|
||||
savedSource: {
|
||||
baseUrl: 'https://saved.example.com',
|
||||
password: 'saved-pass',
|
||||
maxCaptchaAttempts: 3,
|
||||
},
|
||||
},
|
||||
),
|
||||
{
|
||||
baseUrl: 'https://saved.example.com',
|
||||
username: 'user',
|
||||
password: 'saved-pass',
|
||||
maxCaptchaAttempts: 3,
|
||||
includeImageBase64: true,
|
||||
requestId: 'admin-khhao-query-orders:user',
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
test('buildAdminKhhaoSyncPayload merges normalized credential fields into payload', () => {
|
||||
assert.deepEqual(
|
||||
buildAdminKhhaoSyncPayload(
|
||||
{
|
||||
page: 2,
|
||||
limit: 20,
|
||||
username: ' user ',
|
||||
},
|
||||
{
|
||||
savedSource: {
|
||||
baseUrl: 'https://saved.example.com',
|
||||
password: 'saved-pass',
|
||||
maxCaptchaAttempts: 4,
|
||||
},
|
||||
},
|
||||
),
|
||||
{
|
||||
page: 2,
|
||||
limit: 20,
|
||||
username: 'user',
|
||||
baseUrl: 'https://saved.example.com',
|
||||
password: 'saved-pass',
|
||||
maxCaptchaAttempts: 4,
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
test('buildAdminKhhaoLoginResult assembles login response payload', () => {
|
||||
assert.deepEqual(
|
||||
buildAdminKhhaoLoginResult({
|
||||
baseUrl: ' https://admin.khhao.com ',
|
||||
username: ' test-user ',
|
||||
loggedInAt: ' 2026-05-04T12:00:00.000Z ',
|
||||
attempt: 2,
|
||||
responseMessage: ' success ',
|
||||
captchaText: ' abcd ',
|
||||
captchaImageBase64: ' image-data ',
|
||||
cookieMap: {
|
||||
PHPSESSID: 'a',
|
||||
foo: 'b',
|
||||
},
|
||||
cookieHeader: 'abcdef1234567890',
|
||||
}),
|
||||
{
|
||||
baseUrl: 'https://admin.khhao.com',
|
||||
username: 'test-user',
|
||||
loggedInAt: '2026-05-04T12:00:00.000Z',
|
||||
attempt: 2,
|
||||
responseMessage: 'success',
|
||||
captcha: {
|
||||
recognizedText: 'abcd',
|
||||
imageBase64: 'image-data',
|
||||
},
|
||||
session: {
|
||||
cookieKeys: ['PHPSESSID', 'foo'],
|
||||
cookieCount: 2,
|
||||
cookieHeaderMasked: 'abcdef****567890',
|
||||
},
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
test('buildAdminKhhaoOrderQueryResult maps preview list and preserves raw items', () => {
|
||||
const rawItems = [
|
||||
{
|
||||
ordersn: ' A1 ',
|
||||
pingtai: '3',
|
||||
pingtaiName: '快手',
|
||||
shopid: '1001',
|
||||
shopName: '店铺A',
|
||||
goodid: 'g1',
|
||||
goodName: '商品A',
|
||||
sku: 'sku-1',
|
||||
num: '2',
|
||||
fee: '12.34',
|
||||
status: '2',
|
||||
statusName: '<span>已支付</span>',
|
||||
addtime: '2026-05-04 10:00:00',
|
||||
},
|
||||
]
|
||||
|
||||
const result = buildAdminKhhaoOrderQueryResult(
|
||||
{
|
||||
baseUrl: ' https://admin.khhao.com ',
|
||||
},
|
||||
{
|
||||
page: 2,
|
||||
limit: 20,
|
||||
total: 50,
|
||||
items: rawItems,
|
||||
},
|
||||
)
|
||||
|
||||
assert.equal(result.baseUrl, 'https://admin.khhao.com')
|
||||
assert.equal(result.page, 2)
|
||||
assert.equal(result.limit, 20)
|
||||
assert.equal(result.total, 50)
|
||||
assert.equal(result.itemCount, 1)
|
||||
assert.equal(result.rawItems, rawItems)
|
||||
assert.equal(result.items[0].platformOrderId, 'A1')
|
||||
assert.equal(result.items[0].platform, 'kuaishou')
|
||||
assert.equal(result.items[0].itemTitle, '商品A')
|
||||
})
|
||||
@@ -26,47 +26,6 @@ export function maskPhone(value) {
|
||||
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(),
|
||||
@@ -118,10 +77,9 @@ export function mapAdminKuaishouCloudFulfillmentItem(item = {}) {
|
||||
id: String(item.id || '').trim(),
|
||||
enabled: item.enabled !== false,
|
||||
priority: Number(item.priority || 100) || 100,
|
||||
provider: String(item.provider || 'khhao').trim() || 'khhao',
|
||||
provider: String(item.provider || '91kaquan').trim() || '91kaquan',
|
||||
platform: String(item.platform || 'kuaishou').trim() || 'kuaishou',
|
||||
shopId: String(item.shopId || '').trim(),
|
||||
khhaoShopId: String(item.khhaoShopId || '').trim(),
|
||||
internalSkuCode: String(item.internalSkuCode || '').trim(),
|
||||
internalSkuName: String(item.internalSkuName || '').trim(),
|
||||
externalSkuCode: String(item.externalSkuCode || '').trim(),
|
||||
|
||||
@@ -3,8 +3,6 @@ import assert from 'node:assert/strict'
|
||||
|
||||
import {
|
||||
mapAdminCloudtentaclesSession,
|
||||
mapAdminKhhaoSourceConfig,
|
||||
mapAdminKhhaoSyncState,
|
||||
mapAdminKuaishouCloudFulfillmentSource,
|
||||
mapAdminKuaishouEticketSourceConfig,
|
||||
maskPhone,
|
||||
@@ -23,79 +21,6 @@ test('maskPhone hides middle digits for mobile numbers', () => {
|
||||
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(
|
||||
@@ -169,10 +94,9 @@ test('mapAdminKuaishouCloudFulfillmentSource normalizes items and defaults', ()
|
||||
id: 'item-1',
|
||||
enabled: true,
|
||||
priority: 100,
|
||||
provider: 'khhao',
|
||||
provider: '91kaquan',
|
||||
platform: 'kuaishou',
|
||||
shopId: '',
|
||||
khhaoShopId: '',
|
||||
internalSkuCode: '',
|
||||
internalSkuName: '',
|
||||
externalSkuCode: '',
|
||||
|
||||
@@ -3,14 +3,6 @@ export {
|
||||
updateAdminAgisoShopConfigs,
|
||||
} from './agiso-service.js'
|
||||
|
||||
export {
|
||||
getAdminKhhaoSourceConfig,
|
||||
updateAdminKhhaoSourceConfig,
|
||||
testAdminKhhaoLogin,
|
||||
queryAdminKhhaoOrders,
|
||||
syncAdminKhhaoOrders,
|
||||
} from './khhao-service.js'
|
||||
|
||||
export {
|
||||
getAdminNinetyoneOrders,
|
||||
retryAdminNinetyoneOrder,
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
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)) {
|
||||
@@ -21,10 +20,6 @@ export function normalizeAdminFulfillmentBindingItem(
|
||||
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,
|
||||
@@ -34,9 +29,8 @@ export function normalizeAdminFulfillmentBindingItem(
|
||||
|
||||
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 shopId = String(rawBinding.shopId || '').trim()
|
||||
const shopName = String(rawBinding.shopName || '').trim()
|
||||
const skuCode = String(rawBinding.skuCode || '').trim()
|
||||
const skuName = String(rawBinding.skuName || '').trim()
|
||||
const profileKey = String(rawBinding.profileKey || '').trim() || 'manual_review'
|
||||
@@ -46,44 +40,6 @@ export function normalizeAdminFulfillmentBindingItem(
|
||||
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,
|
||||
@@ -103,7 +59,6 @@ export function normalizeAdminFulfillmentBindingItem(
|
||||
platform,
|
||||
shopId,
|
||||
shopName,
|
||||
khhaoShopId,
|
||||
skuCode,
|
||||
skuName,
|
||||
profileKey,
|
||||
@@ -123,12 +78,7 @@ 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?.shopId || '').trim(),
|
||||
String(binding?.match?.externalItemId || '').trim(),
|
||||
String(binding?.match?.externalSkuCode || '').trim(),
|
||||
normalizeProductName(String(binding?.match?.externalSkuName || '').trim()),
|
||||
@@ -142,10 +92,6 @@ export async function validateAdminFulfillmentBindingConfigs(
|
||||
) {
|
||||
assertAdminFulfillmentBindingsInput(bindings)
|
||||
|
||||
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)
|
||||
)
|
||||
const getFulfillmentProfileByKey = /** @type {(profileKey: string) => Promise<unknown>} */ (
|
||||
options.getFulfillmentProfileByKey || (async () => null)
|
||||
)
|
||||
@@ -153,11 +99,7 @@ export async function validateAdminFulfillmentBindingConfigs(
|
||||
const normalizedBindings = []
|
||||
|
||||
for (const [index, rawBinding] of bindings.entries()) {
|
||||
const normalized = normalizeAdminFulfillmentBindingItem(rawBinding, {
|
||||
index,
|
||||
kuaishouEticketSource,
|
||||
resolveKuaishouEticketShopConfig,
|
||||
})
|
||||
const normalized = normalizeAdminFulfillmentBindingItem(rawBinding, { index })
|
||||
|
||||
const profile = await getFulfillmentProfileByKey(normalized.profileKey)
|
||||
if (!profile) {
|
||||
|
||||
@@ -13,14 +13,13 @@ test('assertAdminFulfillmentBindingsInput rejects non-array payloads', () => {
|
||||
assert.throws(() => assertAdminFulfillmentBindingsInput({}), /履约配置格式不正确/)
|
||||
})
|
||||
|
||||
test('normalizeAdminFulfillmentBindingItem normalizes kuaishou binding shop mapping', () => {
|
||||
test('normalizeAdminFulfillmentBindingItem normalizes generic binding fields', () => {
|
||||
const result = normalizeAdminFulfillmentBindingItem(
|
||||
{
|
||||
provider: 'khhao',
|
||||
platform: 'kuaishou',
|
||||
shopId: 'legacy-shop',
|
||||
shopName: '旧店铺',
|
||||
khhaoShopId: 'khhao-1',
|
||||
provider: ' agiso ',
|
||||
platform: ' xianyu ',
|
||||
shopId: ' shop-1 ',
|
||||
shopName: ' 店铺 ',
|
||||
skuCode: 'sku-1',
|
||||
match: {
|
||||
externalSkuCode: 'ext-1',
|
||||
@@ -31,24 +30,14 @@ test('normalizeAdminFulfillmentBindingItem normalizes kuaishou binding shop mapp
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
index: 0,
|
||||
kuaishouEticketSource: { source: true },
|
||||
resolveKuaishouEticketShopConfig() {
|
||||
return {
|
||||
shopId: 'ks-100',
|
||||
kshopName: '快手店铺A',
|
||||
}
|
||||
},
|
||||
},
|
||||
{ index: 0 },
|
||||
)
|
||||
|
||||
assert.deepEqual(result, {
|
||||
provider: 'khhao',
|
||||
platform: 'kuaishou',
|
||||
shopId: 'ks-100',
|
||||
shopName: '快手店铺A',
|
||||
khhaoShopId: 'khhao-1',
|
||||
provider: 'agiso',
|
||||
platform: 'xianyu',
|
||||
shopId: 'shop-1',
|
||||
shopName: '店铺',
|
||||
skuCode: 'sku-1',
|
||||
skuName: '',
|
||||
profileKey: 'manual_review',
|
||||
@@ -57,9 +46,6 @@ test('normalizeAdminFulfillmentBindingItem normalizes kuaishou binding shop mapp
|
||||
config: {
|
||||
kuaishouShop: {
|
||||
note: 'keep',
|
||||
shopId: 'ks-100',
|
||||
shopName: '快手店铺A',
|
||||
khhaoShopId: 'khhao-1',
|
||||
},
|
||||
},
|
||||
match: {
|
||||
@@ -104,10 +90,9 @@ test('normalizeAdminFulfillmentBindingItem rejects missing sku and match conditi
|
||||
test('buildAdminFulfillmentBindingUniqueKey uses normalized external sku name and shop id rules', () => {
|
||||
assert.equal(
|
||||
buildAdminFulfillmentBindingUniqueKey({
|
||||
provider: 'khhao',
|
||||
provider: '91kaquan',
|
||||
platform: 'kuaishou',
|
||||
shopId: 'shop-a',
|
||||
khhaoShopId: 'khhao-a',
|
||||
skuCode: 'sku-1',
|
||||
match: {
|
||||
externalItemId: '',
|
||||
@@ -115,7 +100,7 @@ test('buildAdminFulfillmentBindingUniqueKey uses normalized external sku name an
|
||||
externalSkuName: ' 礼包 A ',
|
||||
},
|
||||
}),
|
||||
'khhao::kuaishou::khhao-a::::::礼包 a::sku-1',
|
||||
'91kaquan::kuaishou::shop-a::::::礼包 a::sku-1',
|
||||
)
|
||||
})
|
||||
|
||||
|
||||
@@ -88,22 +88,6 @@ export function buildAdminAgisoMessagingSavePayload(
|
||||
}
|
||||
}
|
||||
|
||||
export function normalizeAdminKhhaoSourceConfigPayload(payload = {}) {
|
||||
return {
|
||||
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,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export function normalizeAdminCloudtentaclesSourceConfigPayload(payload = {}) {
|
||||
return {
|
||||
enabled: payload.enabled !== false,
|
||||
|
||||
@@ -5,7 +5,6 @@ import {
|
||||
buildAdminAgisoMessagingSavePayload,
|
||||
buildAdminAgisoShopConfigUpdate,
|
||||
normalizeAdminCloudtentaclesSourceConfigPayload,
|
||||
normalizeAdminKhhaoSourceConfigPayload,
|
||||
} from './writes.js'
|
||||
|
||||
test('buildAdminAgisoShopConfigUpdate merges item fields and drops explicit empty templates', () => {
|
||||
@@ -102,37 +101,6 @@ test('buildAdminAgisoMessagingSavePayload updates defaults and keeps only shops
|
||||
)
|
||||
})
|
||||
|
||||
test('normalizeAdminKhhaoSourceConfigPayload applies defaults and numeric fallbacks', () => {
|
||||
assert.deepEqual(
|
||||
normalizeAdminKhhaoSourceConfigPayload({
|
||||
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('normalizeAdminCloudtentaclesSourceConfigPayload applies defaults', () => {
|
||||
assert.deepEqual(
|
||||
normalizeAdminCloudtentaclesSourceConfigPayload({
|
||||
|
||||
@@ -165,26 +165,17 @@ export async function syncConfiguredFulfillmentBindings(profileMap = {}) {
|
||||
}
|
||||
|
||||
export function resolveBindingMatchShopIds(binding = {}) {
|
||||
if (String(binding.provider || '').trim() === 'khhao' && String(binding.platform || '').trim() === 'kuaishou') {
|
||||
const candidates = [...new Set([
|
||||
String(binding.khhaoShopId || '').trim(),
|
||||
String(binding.shopId || '').trim(),
|
||||
].filter(Boolean))]
|
||||
return candidates.length > 0 ? candidates : ['']
|
||||
}
|
||||
|
||||
return [String(binding.shopId || '').trim()]
|
||||
}
|
||||
|
||||
function resolveBindingRuntimeConfig(binding = {}) {
|
||||
const baseConfig = isPlainObject(binding.config) ? { ...binding.config } : {}
|
||||
|
||||
if (String(binding.provider || '').trim() === 'khhao' && String(binding.platform || '').trim() === 'kuaishou') {
|
||||
if (String(binding.provider || '').trim() === '91kaquan' && String(binding.platform || '').trim() === 'kuaishou') {
|
||||
baseConfig.kuaishouShop = {
|
||||
...(isPlainObject(baseConfig.kuaishouShop) ? baseConfig.kuaishouShop : {}),
|
||||
shopId: String(binding.shopId || '').trim(),
|
||||
shopName: String(binding.shopName || '').trim(),
|
||||
khhaoShopId: String(binding.khhaoShopId || '').trim(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,15 +3,14 @@ import assert from 'node:assert/strict'
|
||||
|
||||
import { resolveBindingMatchShopIds } from './fulfillment-bootstrap-service.js'
|
||||
|
||||
test('resolveBindingMatchShopIds returns dual shop ids for khhao kuaishou bindings', () => {
|
||||
test('resolveBindingMatchShopIds returns configured shop id for 91kaquan kuaishou bindings', () => {
|
||||
assert.deepEqual(
|
||||
resolveBindingMatchShopIds({
|
||||
provider: 'khhao',
|
||||
provider: '91kaquan',
|
||||
platform: 'kuaishou',
|
||||
shopId: '4269276762',
|
||||
khhaoShopId: '10',
|
||||
shopId: '91kaquan',
|
||||
}),
|
||||
['10', '4269276762'],
|
||||
['91kaquan'],
|
||||
)
|
||||
})
|
||||
|
||||
|
||||
@@ -57,7 +57,7 @@ export function filterLegacyOrderFulfillmentBindings(bindings = []) {
|
||||
}
|
||||
|
||||
export function isKuaishouCloudFulfillmentBinding(binding = {}) {
|
||||
return String(binding?.provider || '').trim() === 'khhao'
|
||||
return String(binding?.provider || '').trim() === '91kaquan'
|
||||
&& String(binding?.platform || '').trim() === 'kuaishou'
|
||||
}
|
||||
|
||||
@@ -70,7 +70,6 @@ function normalizeOrderFulfillmentBinding(rawValue) {
|
||||
const platform = String(rawValue.platform || '').trim()
|
||||
const shopId = String(rawValue.shopId || '').trim()
|
||||
const shopName = String(rawValue.shopName || '').trim()
|
||||
const khhaoShopId = String(rawValue.khhaoShopId || '').trim()
|
||||
const skuCode = String(rawValue.skuCode || '').trim()
|
||||
const skuName = String(rawValue.skuName || '').trim()
|
||||
const profileKey = String(rawValue.profileKey || '').trim() || 'manual_review'
|
||||
@@ -88,7 +87,6 @@ function normalizeOrderFulfillmentBinding(rawValue) {
|
||||
platform,
|
||||
shopId,
|
||||
shopName,
|
||||
khhaoShopId,
|
||||
skuCode,
|
||||
skuName,
|
||||
profileKey,
|
||||
|
||||
@@ -6,10 +6,10 @@ import {
|
||||
isKuaishouCloudFulfillmentBinding,
|
||||
} from './fulfillment-binding-config-service.js'
|
||||
|
||||
test('isKuaishouCloudFulfillmentBinding only marks khhao kuaishou rules as cloud rules', () => {
|
||||
test('isKuaishouCloudFulfillmentBinding only marks 91kaquan kuaishou rules as cloud rules', () => {
|
||||
assert.equal(
|
||||
isKuaishouCloudFulfillmentBinding({
|
||||
provider: 'khhao',
|
||||
provider: '91kaquan',
|
||||
platform: 'kuaishou',
|
||||
}),
|
||||
true,
|
||||
@@ -24,7 +24,7 @@ test('isKuaishouCloudFulfillmentBinding only marks khhao kuaishou rules as cloud
|
||||
)
|
||||
})
|
||||
|
||||
test('filterLegacyOrderFulfillmentBindings keeps agiso rules and removes khhao kuaishou rules', () => {
|
||||
test('filterLegacyOrderFulfillmentBindings keeps agiso rules and removes 91kaquan kuaishou rules', () => {
|
||||
assert.deepEqual(
|
||||
filterLegacyOrderFulfillmentBindings([
|
||||
{
|
||||
@@ -34,10 +34,9 @@ test('filterLegacyOrderFulfillmentBindings keeps agiso rules and removes khhao k
|
||||
skuCode: 'sku-a',
|
||||
},
|
||||
{
|
||||
provider: 'khhao',
|
||||
provider: '91kaquan',
|
||||
platform: 'kuaishou',
|
||||
shopId: '4269276762',
|
||||
khhaoShopId: '10',
|
||||
shopId: '91kaquan',
|
||||
skuCode: 'sku-b',
|
||||
},
|
||||
]),
|
||||
|
||||
@@ -30,10 +30,9 @@ export function mapKuaishouCloudFulfillmentItemsToBindings(config = {}) {
|
||||
return items
|
||||
.filter((item) => item && item.enabled !== false)
|
||||
.map((item) => ({
|
||||
provider: String(item.provider || 'khhao').trim() || 'khhao',
|
||||
provider: String(item.provider || '91kaquan').trim() || '91kaquan',
|
||||
platform: String(item.platform || 'kuaishou').trim() || 'kuaishou',
|
||||
shopId: String(item.shopId || '').trim(),
|
||||
khhaoShopId: String(item.khhaoShopId || '').trim(),
|
||||
skuCode: String(item.internalSkuCode || '').trim(),
|
||||
skuName: String(item.internalSkuName || '').trim(),
|
||||
profileKey: 'kuaishou_ct_assisted',
|
||||
@@ -121,10 +120,9 @@ function normalizeKuaishouCloudFulfillmentItem(rawValue) {
|
||||
id: String(rawValue.id || internalSkuCode).trim() || internalSkuCode,
|
||||
enabled: rawValue.enabled !== false,
|
||||
priority: normalizePriority(rawValue.priority),
|
||||
provider: String(rawValue.provider || 'khhao').trim() || 'khhao',
|
||||
provider: String(rawValue.provider || '91kaquan').trim() || '91kaquan',
|
||||
platform: String(rawValue.platform || 'kuaishou').trim() || 'kuaishou',
|
||||
shopId: String(rawValue.shopId || '').trim(),
|
||||
khhaoShopId: String(rawValue.khhaoShopId || '').trim(),
|
||||
internalSkuCode,
|
||||
internalSkuName: String(rawValue.internalSkuName || '').trim() || internalSkuCode,
|
||||
externalSkuCode,
|
||||
|
||||
@@ -3,16 +3,15 @@ import assert from 'node:assert/strict'
|
||||
|
||||
import { mapKuaishouCloudFulfillmentItemsToBindings } from './kuaishou-cloud-fulfillment-config-service.js'
|
||||
|
||||
test('mapKuaishouCloudFulfillmentItemsToBindings preserves khhao shop id', () => {
|
||||
test('mapKuaishouCloudFulfillmentItemsToBindings maps 91kaquan rules', () => {
|
||||
assert.deepEqual(
|
||||
mapKuaishouCloudFulfillmentItemsToBindings({
|
||||
items: [
|
||||
{
|
||||
enabled: true,
|
||||
provider: 'khhao',
|
||||
provider: '91kaquan',
|
||||
platform: 'kuaishou',
|
||||
shopId: '4269276762',
|
||||
khhaoShopId: '10',
|
||||
shopId: '91kaquan',
|
||||
internalSkuCode: 'sku-1',
|
||||
internalSkuName: '内部商品',
|
||||
externalSkuCode: '952',
|
||||
@@ -24,10 +23,9 @@ test('mapKuaishouCloudFulfillmentItemsToBindings preserves khhao shop id', () =>
|
||||
}),
|
||||
[
|
||||
{
|
||||
provider: 'khhao',
|
||||
provider: '91kaquan',
|
||||
platform: 'kuaishou',
|
||||
shopId: '4269276762',
|
||||
khhaoShopId: '10',
|
||||
shopId: '91kaquan',
|
||||
skuCode: 'sku-1',
|
||||
skuName: '内部商品',
|
||||
profileKey: 'kuaishou_ct_assisted',
|
||||
|
||||
@@ -1,227 +0,0 @@
|
||||
// @ts-check
|
||||
|
||||
import { getKhhaoSourceConfig } from './source-config-service.js'
|
||||
import { ensureKhhaoSyncBaseline, getKhhaoSyncState, saveKhhaoSyncState } from './sync-state-service.js'
|
||||
import { syncKhhaoOrders } from './order-sync-service.js'
|
||||
import { logError, logInfo, logWarn } from '../../../utils/logger.js'
|
||||
import { nowIso } from '../../../utils/time.js'
|
||||
|
||||
let timer = null
|
||||
let running = false
|
||||
const BOOST_INTERVAL_MIN_MS = 3_000
|
||||
const BOOST_INTERVAL_MAX_MS = 5_000
|
||||
|
||||
export function startKhhaoAutoSyncLoop() {
|
||||
scheduleNextRun(5_000)
|
||||
}
|
||||
|
||||
export function stopKhhaoAutoSyncLoop() {
|
||||
if (timer) {
|
||||
clearTimeout(timer)
|
||||
timer = null
|
||||
}
|
||||
}
|
||||
|
||||
async function runKhhaoAutoSyncCycle() {
|
||||
const source = getKhhaoSourceConfig()
|
||||
const currentState = getKhhaoSyncState()
|
||||
|
||||
if (!source.enabled || !source.autoSync?.enabled) {
|
||||
scheduleNextRun(resolveIntervalMs(source.autoSync?.intervalMinutes))
|
||||
return
|
||||
}
|
||||
|
||||
if (running) {
|
||||
logWarn('[khhao/auto-sync]', '检测到上一次自动同步仍在运行,本轮跳过')
|
||||
scheduleNextRun(resolveNextDelayMs(source, currentState))
|
||||
return
|
||||
}
|
||||
|
||||
if (!String(source.username || '').trim() || !String(source.password || '').trim()) {
|
||||
logWarn('[khhao/auto-sync]', 'khhao 自动同步已启用,但缺少账号或密码')
|
||||
scheduleNextRun(resolveIntervalMs(source.autoSync?.intervalMinutes))
|
||||
return
|
||||
}
|
||||
|
||||
running = true
|
||||
const baselineState = ensureKhhaoSyncBaseline()
|
||||
const startedAt = nowIso()
|
||||
|
||||
saveKhhaoSyncState({
|
||||
...baselineState,
|
||||
lastRunStartedAt: startedAt,
|
||||
lastRunStatus: 'running',
|
||||
lastErrorMessage: '',
|
||||
})
|
||||
|
||||
try {
|
||||
const result = await syncKhhaoOrders({
|
||||
baseUrl: source.baseUrl,
|
||||
username: source.username,
|
||||
password: source.password,
|
||||
maxCaptchaAttempts: source.maxCaptchaAttempts,
|
||||
pages: source.autoSync.pages,
|
||||
limit: source.autoSync.pageSize,
|
||||
createdAfter: baselineState.syncFromCreatedAt,
|
||||
})
|
||||
|
||||
const latestCreatedAt = resolveLatestOrderCreatedAt(result.results)
|
||||
const nextWatchOrders = reconcileKhhaoWatchOrders(baselineState.watchOrders, result.results, startedAt)
|
||||
const watchMode = nextWatchOrders.length > 0 ? 'boosted' : 'normal'
|
||||
|
||||
saveKhhaoSyncState({
|
||||
...getKhhaoSyncState(),
|
||||
syncFromCreatedAt: baselineState.syncFromCreatedAt,
|
||||
lastRunStartedAt: startedAt,
|
||||
lastRunFinishedAt: nowIso(),
|
||||
lastRunStatus: 'success',
|
||||
lastErrorMessage: '',
|
||||
fetchedCount: result.fetchedCount,
|
||||
syncedCount: result.syncedCount,
|
||||
ignoredCount: result.ignoredCount,
|
||||
lastOrderCreatedAt: latestCreatedAt,
|
||||
watchMode,
|
||||
watchOrders: nextWatchOrders,
|
||||
})
|
||||
|
||||
logInfo('[khhao/auto-sync]', 'khhao 自动同步完成', {
|
||||
fetchedCount: result.fetchedCount,
|
||||
syncedCount: result.syncedCount,
|
||||
ignoredCount: result.ignoredCount,
|
||||
syncFromCreatedAt: baselineState.syncFromCreatedAt,
|
||||
watchMode,
|
||||
watchOrderCount: nextWatchOrders.length,
|
||||
watchOrders: nextWatchOrders.map((item) => ({
|
||||
platformOrderId: item.platformOrderId,
|
||||
shopName: item.shopName,
|
||||
payStatus: item.payStatus,
|
||||
})),
|
||||
})
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error || 'khhao 自动同步失败')
|
||||
saveKhhaoSyncState({
|
||||
...getKhhaoSyncState(),
|
||||
syncFromCreatedAt: baselineState.syncFromCreatedAt,
|
||||
lastRunStartedAt: startedAt,
|
||||
lastRunFinishedAt: nowIso(),
|
||||
lastRunStatus: 'failed',
|
||||
lastErrorMessage: message,
|
||||
})
|
||||
logError('[khhao/auto-sync]', 'khhao 自动同步失败', error)
|
||||
} finally {
|
||||
running = false
|
||||
scheduleNextRun(resolveNextDelayMs(source, getKhhaoSyncState()))
|
||||
}
|
||||
}
|
||||
|
||||
function scheduleNextRun(delayMs) {
|
||||
stopKhhaoAutoSyncLoop()
|
||||
timer = setTimeout(() => {
|
||||
void runKhhaoAutoSyncCycle()
|
||||
}, delayMs)
|
||||
}
|
||||
|
||||
function resolveIntervalMs(value) {
|
||||
const minutes = Number(value)
|
||||
const normalized = Number.isInteger(minutes) && minutes > 0 ? minutes : 3
|
||||
return normalized * 60 * 1000
|
||||
}
|
||||
|
||||
function resolveNextDelayMs(source, syncState) {
|
||||
if (hasActiveKhhaoWatchOrders(syncState?.watchOrders)) {
|
||||
return resolveBoostIntervalMs()
|
||||
}
|
||||
|
||||
return resolveIntervalMs(source?.autoSync?.intervalMinutes)
|
||||
}
|
||||
|
||||
function resolveBoostIntervalMs() {
|
||||
const delta = BOOST_INTERVAL_MAX_MS - BOOST_INTERVAL_MIN_MS
|
||||
return BOOST_INTERVAL_MIN_MS + Math.floor(Math.random() * (delta + 1))
|
||||
}
|
||||
|
||||
export function reconcileKhhaoWatchOrders(currentWatchOrders = [], results = [], now = nowIso()) {
|
||||
const nextMap = new Map(normalizeKhhaoWatchOrders(currentWatchOrders).map((item) => [item.platformOrderId, item]))
|
||||
|
||||
for (const item of Array.isArray(results) ? results : []) {
|
||||
const platformOrderId = String(item?.platformOrderId || '').trim()
|
||||
if (!platformOrderId || item?.ignored) {
|
||||
continue
|
||||
}
|
||||
|
||||
const payStatus = normalizePayStatus(item?.payStatus)
|
||||
if (payStatus === 'paid') {
|
||||
nextMap.delete(platformOrderId)
|
||||
continue
|
||||
}
|
||||
|
||||
if (payStatus !== 'unpaid') {
|
||||
continue
|
||||
}
|
||||
|
||||
const previous = nextMap.get(platformOrderId)
|
||||
nextMap.set(platformOrderId, {
|
||||
platformOrderId,
|
||||
shopId: String(item?.shopId || previous?.shopId || '').trim(),
|
||||
shopName: String(item?.shopName || previous?.shopName || '').trim(),
|
||||
itemTitle: String(item?.itemTitle || previous?.itemTitle || '').trim(),
|
||||
payStatus,
|
||||
detectedAt: previous?.detectedAt || now,
|
||||
lastSeenAt: now,
|
||||
})
|
||||
}
|
||||
|
||||
return [...nextMap.values()]
|
||||
}
|
||||
|
||||
export function hasActiveKhhaoWatchOrders(watchOrders = []) {
|
||||
return normalizeKhhaoWatchOrders(watchOrders).length > 0
|
||||
}
|
||||
|
||||
function normalizeKhhaoWatchOrders(value) {
|
||||
return (Array.isArray(value) ? value : [])
|
||||
.map((item) => ({
|
||||
platformOrderId: String(item?.platformOrderId || '').trim(),
|
||||
shopId: String(item?.shopId || '').trim(),
|
||||
shopName: String(item?.shopName || '').trim(),
|
||||
itemTitle: String(item?.itemTitle || '').trim(),
|
||||
payStatus: normalizePayStatus(item?.payStatus),
|
||||
detectedAt: String(item?.detectedAt || '').trim(),
|
||||
lastSeenAt: String(item?.lastSeenAt || '').trim(),
|
||||
}))
|
||||
.filter((item) => item.platformOrderId && item.payStatus === 'unpaid')
|
||||
}
|
||||
|
||||
function normalizePayStatus(value) {
|
||||
const normalized = String(value || '').trim().toLowerCase()
|
||||
if (normalized === 'paid') {
|
||||
return 'paid'
|
||||
}
|
||||
|
||||
return normalized === 'unpaid' ? 'unpaid' : 'unknown'
|
||||
}
|
||||
|
||||
function resolveLatestOrderCreatedAt(results = []) {
|
||||
let latestText = ''
|
||||
let latestTime = 0
|
||||
|
||||
for (const item of Array.isArray(results) ? results : []) {
|
||||
const text = String(item?.rawOrderCreatedAt || item?.orderCreatedAt || '').trim()
|
||||
const timestamp = parseKhhaoOrderCreatedAt(text)
|
||||
if (Number.isFinite(timestamp) && timestamp > latestTime) {
|
||||
latestTime = timestamp
|
||||
latestText = new Date(timestamp).toISOString()
|
||||
}
|
||||
}
|
||||
|
||||
return latestText
|
||||
}
|
||||
|
||||
function parseKhhaoOrderCreatedAt(value) {
|
||||
const text = String(value || '').trim()
|
||||
if (!text) {
|
||||
return NaN
|
||||
}
|
||||
|
||||
return Date.parse(`${text.replace(' ', 'T')}+08:00`)
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
import test from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
|
||||
import {
|
||||
hasActiveKhhaoWatchOrders,
|
||||
reconcileKhhaoWatchOrders,
|
||||
} from './auto-sync-service.js'
|
||||
|
||||
test('reconcileKhhaoWatchOrders adds new unpaid synced order into watch list', () => {
|
||||
const now = '2026-05-03T12:00:00.000Z'
|
||||
const next = reconcileKhhaoWatchOrders([], [
|
||||
{
|
||||
platformOrderId: '2612300323173226',
|
||||
shopId: '4269276762',
|
||||
shopName: '稚嫩游戏交易店',
|
||||
itemTitle: '暗影哥特',
|
||||
payStatus: 'unpaid',
|
||||
ignored: false,
|
||||
},
|
||||
], now)
|
||||
|
||||
assert.equal(next.length, 1)
|
||||
assert.deepEqual(next[0], {
|
||||
platformOrderId: '2612300323173226',
|
||||
shopId: '4269276762',
|
||||
shopName: '稚嫩游戏交易店',
|
||||
itemTitle: '暗影哥特',
|
||||
payStatus: 'unpaid',
|
||||
detectedAt: now,
|
||||
lastSeenAt: now,
|
||||
})
|
||||
assert.equal(hasActiveKhhaoWatchOrders(next), true)
|
||||
})
|
||||
|
||||
test('reconcileKhhaoWatchOrders removes watched order after payment is detected', () => {
|
||||
const current = [
|
||||
{
|
||||
platformOrderId: '2612300323173226',
|
||||
shopId: '4269276762',
|
||||
shopName: '稚嫩游戏交易店',
|
||||
itemTitle: '暗影哥特',
|
||||
payStatus: 'unpaid',
|
||||
detectedAt: '2026-05-03T12:00:00.000Z',
|
||||
lastSeenAt: '2026-05-03T12:00:00.000Z',
|
||||
},
|
||||
]
|
||||
|
||||
const next = reconcileKhhaoWatchOrders(current, [
|
||||
{
|
||||
platformOrderId: '2612300323173226',
|
||||
shopId: '4269276762',
|
||||
shopName: '稚嫩游戏交易店',
|
||||
itemTitle: '暗影哥特',
|
||||
payStatus: 'paid',
|
||||
ignored: false,
|
||||
},
|
||||
], '2026-05-03T12:00:04.000Z')
|
||||
|
||||
assert.deepEqual(next, [])
|
||||
assert.equal(hasActiveKhhaoWatchOrders(next), false)
|
||||
})
|
||||
|
||||
test('reconcileKhhaoWatchOrders ignores ignored and unknown-pay-status rows', () => {
|
||||
const now = '2026-05-03T12:00:00.000Z'
|
||||
const next = reconcileKhhaoWatchOrders([], [
|
||||
{
|
||||
platformOrderId: 'ignored-order',
|
||||
payStatus: 'unpaid',
|
||||
ignored: true,
|
||||
},
|
||||
{
|
||||
platformOrderId: 'unknown-order',
|
||||
payStatus: 'unknown',
|
||||
ignored: false,
|
||||
},
|
||||
], now)
|
||||
|
||||
assert.deepEqual(next, [])
|
||||
})
|
||||
@@ -1,75 +0,0 @@
|
||||
// @ts-check
|
||||
|
||||
import { recognizeImageCaptcha } from '../../session/ocr.js'
|
||||
import { buildKhhaoUrl, buildCookieHeader, normalizeKhhaoCookieState, resolveKhhaoConfig } from './shared.js'
|
||||
|
||||
/**
|
||||
* @param {{
|
||||
* baseUrl?: string
|
||||
* captchaPath?: string
|
||||
* timeoutMs?: number
|
||||
* cookies?: Record<string, string>
|
||||
* requestId?: string
|
||||
* includeImageBase64?: boolean
|
||||
* }} [options]
|
||||
*/
|
||||
export async function fetchKhhaoCaptcha(options = {}) {
|
||||
const config = resolveKhhaoConfig(options)
|
||||
const cookieHeader = buildCookieHeader(options.cookies)
|
||||
const response = await fetchWithTimeout(buildKhhaoUrl(config.baseUrl, config.captchaPath), {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
accept: 'image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8',
|
||||
referer: buildKhhaoUrl(config.baseUrl, config.loginPath).toString(),
|
||||
...(cookieHeader ? { cookie: cookieHeader } : {}),
|
||||
},
|
||||
}, config.timeoutMs)
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`khhao 验证码请求失败,HTTP ${response.status}`)
|
||||
}
|
||||
|
||||
const imageBuffer = Buffer.from(await response.arrayBuffer())
|
||||
const imageBase64 = imageBuffer.toString('base64')
|
||||
const ocr = await recognizeImageCaptcha({
|
||||
imageBase64,
|
||||
imageContentType: String(response.headers.get('content-type') || 'image/png').trim() || 'image/png',
|
||||
tag: options.requestId ? `khhao-${options.requestId}` : 'khhao-login-captcha',
|
||||
})
|
||||
|
||||
if (ocr?.code !== 0) {
|
||||
throw new Error(ocr?.msg || 'khhao 验证码 OCR 识别失败')
|
||||
}
|
||||
|
||||
const captchaText = String(ocr?.data?.text || ocr?.data?.recognizedText || '').trim()
|
||||
if (!captchaText) {
|
||||
throw new Error('khhao 验证码 OCR 未识别出内容')
|
||||
}
|
||||
|
||||
return {
|
||||
captchaText,
|
||||
ocr,
|
||||
cookieMap: normalizeKhhaoCookieState(options.cookies, response.headers),
|
||||
contentType: String(response.headers.get('content-type') || 'image/png').trim() || 'image/png',
|
||||
imageBase64: options.includeImageBase64 ? imageBase64 : '',
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchWithTimeout(input, init, timeoutMs) {
|
||||
const controller = new AbortController()
|
||||
const timer = setTimeout(() => controller.abort(), timeoutMs)
|
||||
|
||||
try {
|
||||
return await fetch(input, {
|
||||
...init,
|
||||
signal: controller.signal,
|
||||
})
|
||||
} catch (error) {
|
||||
if (error instanceof Error && error.name === 'AbortError') {
|
||||
throw new Error(`khhao 验证码请求超时(${timeoutMs}ms)`)
|
||||
}
|
||||
throw error
|
||||
} finally {
|
||||
clearTimeout(timer)
|
||||
}
|
||||
}
|
||||
@@ -1,169 +0,0 @@
|
||||
// @ts-check
|
||||
|
||||
import { parseAmountToFen } from '../../../utils/money.js'
|
||||
import { resolveKuaishouEticketShopConfig } from '../kuaishou-eticket/source-config-service.js'
|
||||
|
||||
export function mapKhhaoOrderPreviewList(items = []) {
|
||||
return (Array.isArray(items) ? items : []).map((item) => mapKhhaoOrderPreview(item))
|
||||
}
|
||||
|
||||
export function mapKhhaoOrderToSourceEvent(item = {}) {
|
||||
const preview = mapKhhaoOrderPreview(item)
|
||||
const orderStatus = resolveKhhaoOrderStatus(preview.status)
|
||||
const payStatus = resolveKhhaoPayStatus(preview.status)
|
||||
const sourceShopId = String(preview.khhaoShopId || preview.shopId || '').trim()
|
||||
|
||||
return {
|
||||
provider: 'khhao',
|
||||
platform: preview.platform || 'unknown',
|
||||
shopId: sourceShopId,
|
||||
shopIdAliases: uniqueNonEmptyValues([sourceShopId, preview.shopId, ...(preview.shopIdAliases || [])]),
|
||||
shopName: preview.shopName,
|
||||
platformOrderId: preview.platformOrderId,
|
||||
orderStatus,
|
||||
payStatus,
|
||||
buyerId: '',
|
||||
buyerName: String(preview.raw.name || '').trim(),
|
||||
receiverContact: '',
|
||||
totalAmount: preview.totalAmountFen,
|
||||
currency: 'CNY',
|
||||
paidAt: payStatus === 'paid' ? normalizePaidAt(preview.orderCreatedAt) : null,
|
||||
rawPayload: preview.raw,
|
||||
items: [
|
||||
{
|
||||
itemId: preview.itemId,
|
||||
externalItemId: preview.itemId,
|
||||
externalSkuCode: preview.skuCode || preview.itemId,
|
||||
externalSkuName: preview.itemTitle || preview.skuCode || preview.itemId,
|
||||
skuCode: preview.skuCode || preview.itemId,
|
||||
skuName: preview.itemTitle || preview.skuCode || preview.itemId,
|
||||
quantity: preview.quantity,
|
||||
snapshot: preview.raw,
|
||||
spec: preview.raw,
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
export function mapKhhaoOrderPreview(item = {}) {
|
||||
const raw = isPlainObject(item) ? item : {}
|
||||
const platform = resolveKhhaoPlatform(raw.pingtai)
|
||||
const quantity = normalizeQuantity(raw.num)
|
||||
const internalShopId = String(raw.shopid || '').trim()
|
||||
const shopName = String(raw.shopName || '').trim()
|
||||
const resolvedShopIdentity = resolveKhhaoShopIdentity({
|
||||
platform,
|
||||
internalShopId,
|
||||
shopName,
|
||||
})
|
||||
|
||||
return {
|
||||
provider: 'khhao',
|
||||
platform,
|
||||
platformLabel: String(raw.pingtaiName || '').trim(),
|
||||
platformOrderId: String(raw.ordersn || '').trim(),
|
||||
shopId: resolvedShopIdentity.shopId,
|
||||
kuaishouShopId: resolvedShopIdentity.officialShopId,
|
||||
shopIdAliases: resolvedShopIdentity.shopIdAliases,
|
||||
khhaoShopId: internalShopId,
|
||||
internalShopId,
|
||||
shopName,
|
||||
itemId: String(raw.goodid || '').trim(),
|
||||
itemTitle: String(raw.goodName || '').trim(),
|
||||
skuCode: String(raw.sku || '').trim(),
|
||||
quantity,
|
||||
totalAmountFen: parseAmountToFen(raw.fee),
|
||||
status: String(raw.status || '').trim(),
|
||||
statusLabel: stripHtmlTags(raw.statusName),
|
||||
orderCreatedAt: String(raw.addtime || '').trim(),
|
||||
raw,
|
||||
}
|
||||
}
|
||||
|
||||
function resolveKhhaoShopIdentity({ platform = '', internalShopId = '', shopName = '' } = {}) {
|
||||
const fallbackShopId = String(internalShopId || '').trim()
|
||||
const normalizedShopName = String(shopName || '').trim()
|
||||
|
||||
if (String(platform || '').trim() !== 'kuaishou') {
|
||||
return {
|
||||
shopId: fallbackShopId,
|
||||
officialShopId: '',
|
||||
shopIdAliases: fallbackShopId ? [fallbackShopId] : [],
|
||||
}
|
||||
}
|
||||
|
||||
const matchedShop = resolveKuaishouEticketShopConfig({
|
||||
shopId: fallbackShopId,
|
||||
shopName: normalizedShopName,
|
||||
})
|
||||
const resolvedShopId = String(matchedShop?.shopId || '').trim() || fallbackShopId
|
||||
const shopIdAliases = uniqueNonEmptyValues([resolvedShopId, fallbackShopId])
|
||||
|
||||
return {
|
||||
shopId: resolvedShopId,
|
||||
officialShopId: String(matchedShop?.shopId || '').trim(),
|
||||
shopIdAliases,
|
||||
}
|
||||
}
|
||||
|
||||
export function resolveKhhaoPlatform(value) {
|
||||
const normalized = String(value || '').trim()
|
||||
|
||||
if (normalized === '3') {
|
||||
return 'kuaishou'
|
||||
}
|
||||
|
||||
return normalized ? 'unknown' : ''
|
||||
}
|
||||
|
||||
export function resolveKhhaoOrderStatus(value) {
|
||||
const normalized = String(value || '').trim()
|
||||
|
||||
if (normalized === '2') {
|
||||
return 'paid'
|
||||
}
|
||||
|
||||
if (normalized === '0') {
|
||||
return 'created'
|
||||
}
|
||||
|
||||
return 'unknown'
|
||||
}
|
||||
|
||||
export function resolveKhhaoPayStatus(value) {
|
||||
const normalized = String(value || '').trim()
|
||||
|
||||
if (normalized === '2') {
|
||||
return 'paid'
|
||||
}
|
||||
|
||||
if (normalized === '0') {
|
||||
return 'unpaid'
|
||||
}
|
||||
|
||||
return 'unknown'
|
||||
}
|
||||
|
||||
function normalizeQuantity(value) {
|
||||
const parsed = Number(value)
|
||||
return Number.isInteger(parsed) && parsed > 0 ? parsed : 1
|
||||
}
|
||||
|
||||
function stripHtmlTags(value) {
|
||||
return String(value || '').replace(/<[^>]+>/g, '').trim()
|
||||
}
|
||||
|
||||
function normalizePaidAt(value) {
|
||||
const text = String(value || '').trim()
|
||||
return text ? text.replace(' ', 'T') : null
|
||||
}
|
||||
|
||||
function isPlainObject(value) {
|
||||
return Object.prototype.toString.call(value) === '[object Object]'
|
||||
}
|
||||
|
||||
function uniqueNonEmptyValues(values) {
|
||||
return [...new Set((Array.isArray(values) ? values : [])
|
||||
.map((value) => String(value || '').trim())
|
||||
.filter(Boolean))]
|
||||
}
|
||||
@@ -1,112 +0,0 @@
|
||||
// @ts-check
|
||||
|
||||
import { createHttpError } from '../../../utils/http.js'
|
||||
import { buildKhhaoUrl, resolveKhhaoConfig } from './shared.js'
|
||||
|
||||
/**
|
||||
* @param {{
|
||||
* session?: {
|
||||
* baseUrl?: string
|
||||
* cookieHeader?: string
|
||||
* }
|
||||
* page?: number | string
|
||||
* limit?: number | string
|
||||
* baseUrl?: string
|
||||
* orderListPath?: string
|
||||
* timeoutMs?: number
|
||||
* }} [payload]
|
||||
*/
|
||||
export async function queryKhhaoOrderList(payload = {}) {
|
||||
const config = resolveKhhaoConfig(payload)
|
||||
const page = normalizePage(payload.page)
|
||||
const limit = normalizeLimit(payload.limit)
|
||||
const session = payload.session || {}
|
||||
const cookieHeader = String(session.cookieHeader || '').trim()
|
||||
|
||||
if (!cookieHeader) {
|
||||
throw createHttpError('khhao 查询订单缺少登录态 Cookie', {
|
||||
statusCode: 400,
|
||||
errorCode: 'khhao_query_missing_cookie',
|
||||
})
|
||||
}
|
||||
|
||||
const response = await fetchWithTimeout(buildKhhaoUrl(config.baseUrl, config.orderListPath, { page, limit }), {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
accept: 'application/json, text/javascript, */*; q=0.01',
|
||||
referer: buildKhhaoUrl(config.baseUrl, config.loginPath).toString(),
|
||||
'x-requested-with': 'XMLHttpRequest',
|
||||
cookie: cookieHeader,
|
||||
},
|
||||
}, config.timeoutMs)
|
||||
|
||||
const rawText = await response.text()
|
||||
if (!response.ok) {
|
||||
throw createHttpError(`khhao 订单查询失败,HTTP ${response.status}`, {
|
||||
statusCode: 502,
|
||||
errorCode: 'khhao_query_http_failed',
|
||||
})
|
||||
}
|
||||
|
||||
const payloadJson = tryParseJson(rawText)
|
||||
if (!payloadJson || typeof payloadJson !== 'object') {
|
||||
throw createHttpError('khhao 订单查询返回了无法解析的 JSON', {
|
||||
statusCode: 502,
|
||||
errorCode: 'khhao_query_invalid_json',
|
||||
})
|
||||
}
|
||||
|
||||
const items = Array.isArray(payloadJson.data) ? payloadJson.data : []
|
||||
|
||||
return {
|
||||
page,
|
||||
limit,
|
||||
total: Number(payloadJson.count || items.length || 0),
|
||||
items,
|
||||
raw: payloadJson,
|
||||
}
|
||||
}
|
||||
|
||||
function normalizePage(value) {
|
||||
const parsed = Number(value)
|
||||
return Number.isInteger(parsed) && parsed > 0 ? parsed : 1
|
||||
}
|
||||
|
||||
function normalizeLimit(value) {
|
||||
const parsed = Number(value)
|
||||
if (!Number.isInteger(parsed) || parsed <= 0) {
|
||||
return 50
|
||||
}
|
||||
|
||||
return Math.min(parsed, 200)
|
||||
}
|
||||
|
||||
async function fetchWithTimeout(input, init, timeoutMs) {
|
||||
const controller = new AbortController()
|
||||
const timer = setTimeout(() => controller.abort(), timeoutMs)
|
||||
|
||||
try {
|
||||
return await fetch(input, {
|
||||
...init,
|
||||
signal: controller.signal,
|
||||
})
|
||||
} catch (error) {
|
||||
if (error instanceof Error && error.name === 'AbortError') {
|
||||
throw createHttpError(`khhao 订单查询超时(${timeoutMs}ms)`, {
|
||||
statusCode: 504,
|
||||
errorCode: 'khhao_query_timeout',
|
||||
})
|
||||
}
|
||||
throw error
|
||||
} finally {
|
||||
clearTimeout(timer)
|
||||
}
|
||||
}
|
||||
|
||||
function tryParseJson(text) {
|
||||
try {
|
||||
return JSON.parse(text)
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
@@ -1,150 +0,0 @@
|
||||
// @ts-check
|
||||
|
||||
import { upsertOrderFromSource } from '../../order/order-service.js'
|
||||
import {
|
||||
mapKhhaoOrderPreview,
|
||||
mapKhhaoOrderToSourceEvent,
|
||||
resolveKhhaoOrderStatus,
|
||||
resolveKhhaoPayStatus,
|
||||
} from './order-mapper-service.js'
|
||||
import { queryKhhaoOrderList } from './order-query-service.js'
|
||||
import { loginKhhaoSession } from './session-service.js'
|
||||
|
||||
/**
|
||||
* @param {{
|
||||
* baseUrl?: string
|
||||
* username?: string
|
||||
* password?: string
|
||||
* page?: number | string
|
||||
* limit?: number | string
|
||||
* pages?: number | string
|
||||
* maxCaptchaAttempts?: number | string
|
||||
* createdAfter?: string
|
||||
* }} [payload]
|
||||
*/
|
||||
export async function syncKhhaoOrders(payload = {}) {
|
||||
const session = await loginKhhaoSession({
|
||||
baseUrl: payload.baseUrl,
|
||||
username: payload.username,
|
||||
password: payload.password,
|
||||
maxCaptchaAttempts: payload.maxCaptchaAttempts,
|
||||
requestId: `admin-khhao-sync-orders:${String(payload.username || '').trim() || 'anonymous'}`,
|
||||
})
|
||||
|
||||
const queryResult = await queryKhhaoOrderList({
|
||||
baseUrl: payload.baseUrl,
|
||||
page: 1,
|
||||
limit: resolvePageSize(payload.limit),
|
||||
session,
|
||||
})
|
||||
|
||||
const createdAfter = String(payload.createdAfter || '').trim()
|
||||
const pages = Math.max(1, Math.min(Number(payload.pages || payload.page || 1) || 1, 5))
|
||||
const results = []
|
||||
const allItems = [...queryResult.items]
|
||||
|
||||
for (let page = 2; page <= pages; page += 1) {
|
||||
const nextPage = await queryKhhaoOrderList({
|
||||
baseUrl: payload.baseUrl,
|
||||
page,
|
||||
limit: resolvePageSize(payload.limit),
|
||||
session,
|
||||
})
|
||||
allItems.push(...nextPage.items)
|
||||
}
|
||||
|
||||
for (const item of allItems) {
|
||||
const preview = mapKhhaoOrderPreview(item)
|
||||
|
||||
if (createdAfter && !isKhhaoOrderAfter(preview.orderCreatedAt, createdAfter)) {
|
||||
results.push({
|
||||
platformOrderId: preview.platformOrderId,
|
||||
provider: 'khhao',
|
||||
platform: preview.platform || 'unknown',
|
||||
shopId: preview.shopId,
|
||||
shopName: preview.shopName,
|
||||
skuCode: preview.skuCode,
|
||||
itemTitle: preview.itemTitle,
|
||||
orderStatus: resolvePreviewOrderStatus(preview),
|
||||
payStatus: resolvePreviewPayStatus(preview),
|
||||
rawOrderCreatedAt: preview.orderCreatedAt,
|
||||
ignored: true,
|
||||
ignoreReason: 'before_sync_baseline',
|
||||
orderId: null,
|
||||
orderItemCount: 0,
|
||||
taskCount: 0,
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
const sourceEvent = mapKhhaoOrderToSourceEvent(item)
|
||||
const upsertResult = await upsertOrderFromSource(sourceEvent, { sourceLabel: 'khhao-sync' })
|
||||
|
||||
results.push({
|
||||
platformOrderId: preview.platformOrderId,
|
||||
provider: sourceEvent.provider,
|
||||
platform: sourceEvent.platform,
|
||||
shopId: sourceEvent.shopId,
|
||||
shopName: sourceEvent.shopName,
|
||||
skuCode: preview.skuCode,
|
||||
itemTitle: preview.itemTitle,
|
||||
orderStatus: sourceEvent.orderStatus,
|
||||
payStatus: sourceEvent.payStatus,
|
||||
rawOrderCreatedAt: preview.orderCreatedAt,
|
||||
ignored: Boolean(upsertResult.ignored),
|
||||
ignoreReason: String(upsertResult.ignoreReason || '').trim(),
|
||||
orderId: Number(upsertResult.order?.id || 0) || null,
|
||||
orderItemCount: Array.isArray(upsertResult.orderItems) ? upsertResult.orderItems.length : 0,
|
||||
taskCount: Array.isArray(upsertResult.tasks) ? upsertResult.tasks.length : 0,
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
baseUrl: session.baseUrl,
|
||||
page: 1,
|
||||
limit: resolvePageSize(payload.limit),
|
||||
total: queryResult.total,
|
||||
fetchedCount: allItems.length,
|
||||
syncedCount: results.filter((item) => !item.ignored && item.orderId).length,
|
||||
ignoredCount: results.filter((item) => item.ignored).length,
|
||||
results,
|
||||
}
|
||||
}
|
||||
|
||||
function resolvePageSize(value) {
|
||||
const parsed = Number(value)
|
||||
if (!Number.isInteger(parsed) || parsed <= 0) {
|
||||
return 20
|
||||
}
|
||||
|
||||
return Math.min(parsed, 50)
|
||||
}
|
||||
|
||||
function isKhhaoOrderAfter(orderCreatedAt, baselineIso) {
|
||||
const orderTime = parseKhhaoOrderCreatedAt(orderCreatedAt)
|
||||
const baselineTime = Date.parse(String(baselineIso || '').trim())
|
||||
|
||||
if (!Number.isFinite(orderTime) || !Number.isFinite(baselineTime)) {
|
||||
return true
|
||||
}
|
||||
|
||||
return orderTime > baselineTime
|
||||
}
|
||||
|
||||
function parseKhhaoOrderCreatedAt(value) {
|
||||
const text = String(value || '').trim()
|
||||
if (!text) {
|
||||
return NaN
|
||||
}
|
||||
|
||||
const isoText = text.replace(' ', 'T')
|
||||
return Date.parse(`${isoText}+08:00`)
|
||||
}
|
||||
|
||||
function resolvePreviewOrderStatus(preview) {
|
||||
return resolveKhhaoOrderStatus(preview?.status)
|
||||
}
|
||||
|
||||
function resolvePreviewPayStatus(preview) {
|
||||
return resolveKhhaoPayStatus(preview?.status)
|
||||
}
|
||||
@@ -1,216 +0,0 @@
|
||||
// @ts-check
|
||||
|
||||
import { createHttpError } from '../../../utils/http.js'
|
||||
import { logInfo, logWarn } from '../../../utils/logger.js'
|
||||
import { fetchKhhaoCaptcha } from './captcha-service.js'
|
||||
import {
|
||||
buildCookieHeader,
|
||||
buildKhhaoUrl,
|
||||
normalizeKhhaoCookieState,
|
||||
resolveKhhaoConfig,
|
||||
} from './shared.js'
|
||||
|
||||
/**
|
||||
* @param {{
|
||||
* username?: string
|
||||
* password?: string
|
||||
* baseUrl?: string
|
||||
* timeoutMs?: number
|
||||
* maxCaptchaAttempts?: number | string
|
||||
* includeImageBase64?: boolean
|
||||
* requestId?: string
|
||||
* }} [payload]
|
||||
*/
|
||||
export async function loginKhhaoSession(payload = {}) {
|
||||
const username = String(payload.username || '').trim()
|
||||
const password = String(payload.password || '').trim()
|
||||
const requestId = String(payload.requestId || '').trim()
|
||||
|
||||
if (!username) {
|
||||
throw createHttpError('khhao 登录缺少账号', {
|
||||
statusCode: 400,
|
||||
errorCode: 'khhao_login_missing_username',
|
||||
})
|
||||
}
|
||||
|
||||
if (!password) {
|
||||
throw createHttpError('khhao 登录缺少密码', {
|
||||
statusCode: 400,
|
||||
errorCode: 'khhao_login_missing_password',
|
||||
})
|
||||
}
|
||||
|
||||
const config = resolveKhhaoConfig(payload)
|
||||
const maxCaptchaAttempts = normalizeAttempts(payload.maxCaptchaAttempts)
|
||||
/** @type {Record<string, string>} */
|
||||
let cookieMap = {}
|
||||
let lastFailureMessage = ''
|
||||
|
||||
cookieMap = await openKhhaoLoginPage(config, cookieMap)
|
||||
|
||||
for (let attempt = 1; attempt <= maxCaptchaAttempts; attempt += 1) {
|
||||
const captcha = await fetchKhhaoCaptcha({
|
||||
...config,
|
||||
cookies: cookieMap,
|
||||
requestId: requestId ? `${requestId}-captcha-${attempt}` : `login-${attempt}`,
|
||||
includeImageBase64: Boolean(payload.includeImageBase64),
|
||||
})
|
||||
cookieMap = captcha.cookieMap
|
||||
|
||||
const result = await submitKhhaoLogin({
|
||||
config,
|
||||
cookieMap,
|
||||
username,
|
||||
password,
|
||||
captchaText: normalizeKhhaoCaptchaText(captcha.captchaText),
|
||||
requestId,
|
||||
})
|
||||
|
||||
cookieMap = result.cookieMap
|
||||
|
||||
if (result.success) {
|
||||
logInfo('[khhao/session]', 'khhao 登录成功', {
|
||||
requestId,
|
||||
username,
|
||||
attempt,
|
||||
cookieKeys: Object.keys(cookieMap),
|
||||
})
|
||||
|
||||
return {
|
||||
baseUrl: config.baseUrl,
|
||||
cookieMap,
|
||||
cookieHeader: buildCookieHeader(cookieMap),
|
||||
loggedInAt: new Date().toISOString(),
|
||||
username,
|
||||
attempt,
|
||||
captchaText: captcha.captchaText,
|
||||
captchaImageBase64: captcha.imageBase64,
|
||||
responseMessage: result.message,
|
||||
}
|
||||
}
|
||||
|
||||
lastFailureMessage = result.message
|
||||
logWarn('[khhao/session]', 'khhao 登录失败', {
|
||||
requestId,
|
||||
username,
|
||||
attempt,
|
||||
message: result.message,
|
||||
})
|
||||
|
||||
if (!shouldRetryLogin(result.message) || attempt >= maxCaptchaAttempts) {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
throw createHttpError(lastFailureMessage || 'khhao 登录失败', {
|
||||
statusCode: 401,
|
||||
errorCode: 'khhao_login_failed',
|
||||
})
|
||||
}
|
||||
|
||||
async function openKhhaoLoginPage(config, currentCookies) {
|
||||
const response = await fetchWithTimeout(buildKhhaoUrl(config.baseUrl, config.loginPath), {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
||||
},
|
||||
}, config.timeoutMs)
|
||||
|
||||
if (!response.ok) {
|
||||
throw createHttpError(`khhao 登录页打开失败,HTTP ${response.status}`, {
|
||||
statusCode: 502,
|
||||
errorCode: 'khhao_login_page_failed',
|
||||
})
|
||||
}
|
||||
|
||||
return normalizeKhhaoCookieState(currentCookies, response.headers)
|
||||
}
|
||||
|
||||
async function submitKhhaoLogin({ config, cookieMap, username, password, captchaText, requestId }) {
|
||||
const response = await fetchWithTimeout(buildKhhaoUrl(config.baseUrl, config.loginPath), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
accept: 'application/json, text/javascript, */*; q=0.01',
|
||||
'content-type': 'application/x-www-form-urlencoded; charset=UTF-8',
|
||||
origin: config.baseUrl,
|
||||
referer: buildKhhaoUrl(config.baseUrl, config.loginPath).toString(),
|
||||
'x-requested-with': 'XMLHttpRequest',
|
||||
...(buildCookieHeader(cookieMap) ? { cookie: buildCookieHeader(cookieMap) } : {}),
|
||||
},
|
||||
body: new URLSearchParams({
|
||||
username,
|
||||
password,
|
||||
img_code: captchaText,
|
||||
}).toString(),
|
||||
}, config.timeoutMs)
|
||||
|
||||
const rawText = await response.text()
|
||||
const cookieState = normalizeKhhaoCookieState(cookieMap, response.headers)
|
||||
|
||||
if (!response.ok) {
|
||||
throw createHttpError(`khhao 登录请求失败,HTTP ${response.status}`, {
|
||||
statusCode: 502,
|
||||
errorCode: 'khhao_login_request_failed',
|
||||
})
|
||||
}
|
||||
|
||||
const payload = tryParseJson(rawText)
|
||||
const errcode = Number(payload?.errcode ?? 1)
|
||||
const message = String(payload?.msg || '').trim() || 'khhao 登录失败'
|
||||
|
||||
return {
|
||||
success: errcode === 0,
|
||||
message,
|
||||
payload,
|
||||
cookieMap: cookieState,
|
||||
requestId,
|
||||
}
|
||||
}
|
||||
|
||||
function shouldRetryLogin(message) {
|
||||
const normalized = String(message || '').trim()
|
||||
return normalized.includes('验证码')
|
||||
}
|
||||
|
||||
function normalizeAttempts(value) {
|
||||
const parsed = Number(value)
|
||||
if (!Number.isInteger(parsed) || parsed <= 0) {
|
||||
return 3
|
||||
}
|
||||
|
||||
return Math.min(parsed, 5)
|
||||
}
|
||||
|
||||
function normalizeKhhaoCaptchaText(value) {
|
||||
return String(value || '').trim().toLowerCase()
|
||||
}
|
||||
|
||||
async function fetchWithTimeout(input, init, timeoutMs) {
|
||||
const controller = new AbortController()
|
||||
const timer = setTimeout(() => controller.abort(), timeoutMs)
|
||||
|
||||
try {
|
||||
return await fetch(input, {
|
||||
...init,
|
||||
signal: controller.signal,
|
||||
})
|
||||
} catch (error) {
|
||||
if (error instanceof Error && error.name === 'AbortError') {
|
||||
throw createHttpError(`khhao 请求超时(${timeoutMs}ms)`, {
|
||||
statusCode: 504,
|
||||
errorCode: 'khhao_request_timeout',
|
||||
})
|
||||
}
|
||||
throw error
|
||||
} finally {
|
||||
clearTimeout(timer)
|
||||
}
|
||||
}
|
||||
|
||||
function tryParseJson(text) {
|
||||
try {
|
||||
return JSON.parse(text)
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
@@ -1,113 +0,0 @@
|
||||
// @ts-check
|
||||
|
||||
import { runtimeConfig } from '../../../config/runtime.js'
|
||||
|
||||
/** @typedef {import('../../../types/runtime-config.js').RuntimeConfig} RuntimeConfig */
|
||||
|
||||
export function resolveKhhaoConfig(overrides = {}) {
|
||||
/** @type {RuntimeConfig['platforms']['khhao']} */
|
||||
const baseConfig = runtimeConfig.platforms?.khhao || {
|
||||
baseUrl: '',
|
||||
timeoutMs: 5000,
|
||||
loginPath: '/c/login/index.php',
|
||||
captchaPath: '/verify_img.php',
|
||||
orderListPath: '/c/payOrder/get.php',
|
||||
}
|
||||
|
||||
return {
|
||||
baseUrl: normalizeBaseUrl(overrides.baseUrl || baseConfig.baseUrl),
|
||||
timeoutMs: normalizePositiveInteger(overrides.timeoutMs || baseConfig.timeoutMs, 5000),
|
||||
loginPath: normalizePath(overrides.loginPath || baseConfig.loginPath, '/c/login/index.php'),
|
||||
captchaPath: normalizePath(overrides.captchaPath || baseConfig.captchaPath, '/verify_img.php'),
|
||||
orderListPath: normalizePath(overrides.orderListPath || baseConfig.orderListPath, '/c/payOrder/get.php'),
|
||||
}
|
||||
}
|
||||
|
||||
export function buildKhhaoUrl(baseUrl, pathname, searchParams = null) {
|
||||
const url = new URL(normalizePath(pathname, '/'), normalizeBaseUrl(baseUrl) || 'https://admin.khhao.com')
|
||||
|
||||
if (searchParams && typeof searchParams === 'object') {
|
||||
for (const [key, value] of Object.entries(searchParams)) {
|
||||
if (typeof value === 'undefined' || value === null || value === '') {
|
||||
continue
|
||||
}
|
||||
url.searchParams.set(key, String(value))
|
||||
}
|
||||
}
|
||||
|
||||
return url
|
||||
}
|
||||
|
||||
export function collectSetCookieHeaders(headers) {
|
||||
if (!headers || typeof headers !== 'object') {
|
||||
return []
|
||||
}
|
||||
|
||||
if (typeof headers.getSetCookie === 'function') {
|
||||
return headers.getSetCookie()
|
||||
}
|
||||
|
||||
const raw = headers.get('set-cookie')
|
||||
return raw ? [raw] : []
|
||||
}
|
||||
|
||||
export function mergeCookies(currentCookies = {}, setCookieHeaders = []) {
|
||||
const next = { ...currentCookies }
|
||||
|
||||
for (const header of Array.isArray(setCookieHeaders) ? setCookieHeaders : []) {
|
||||
const text = String(header || '').trim()
|
||||
if (!text) {
|
||||
continue
|
||||
}
|
||||
|
||||
const pair = text.split(';', 1)[0]
|
||||
const separatorIndex = pair.indexOf('=')
|
||||
if (separatorIndex <= 0) {
|
||||
continue
|
||||
}
|
||||
|
||||
const key = pair.slice(0, separatorIndex).trim()
|
||||
const value = pair.slice(separatorIndex + 1).trim()
|
||||
|
||||
if (!key) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (value) {
|
||||
next[key] = value
|
||||
} else {
|
||||
delete next[key]
|
||||
}
|
||||
}
|
||||
|
||||
return next
|
||||
}
|
||||
|
||||
export function buildCookieHeader(cookieMap = {}) {
|
||||
return Object.entries(cookieMap)
|
||||
.filter(([key, value]) => String(key || '').trim() && String(value || '').trim())
|
||||
.map(([key, value]) => `${key}=${value}`)
|
||||
.join('; ')
|
||||
}
|
||||
|
||||
export function normalizeKhhaoCookieState(currentCookies = {}, headers) {
|
||||
return mergeCookies(currentCookies, collectSetCookieHeaders(headers))
|
||||
}
|
||||
|
||||
function normalizeBaseUrl(value) {
|
||||
return String(value || '').trim().replace(/\/+$/, '')
|
||||
}
|
||||
|
||||
function normalizePath(value, fallback) {
|
||||
const normalized = String(value || '').trim()
|
||||
if (!normalized) {
|
||||
return fallback
|
||||
}
|
||||
|
||||
return normalized.startsWith('/') ? normalized : `/${normalized}`
|
||||
}
|
||||
|
||||
function normalizePositiveInteger(value, fallback) {
|
||||
const parsed = Number(value)
|
||||
return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback
|
||||
}
|
||||
@@ -1,80 +0,0 @@
|
||||
// @ts-check
|
||||
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
|
||||
import { PROJECT_ROOT } from '../../../config/runtime.js'
|
||||
|
||||
const KHHAO_SOURCES_FILE_PATH = path.join(PROJECT_ROOT, 'data', 'khhao-sources.json')
|
||||
|
||||
export function getKhhaoSourcesFilePath() {
|
||||
return KHHAO_SOURCES_FILE_PATH
|
||||
}
|
||||
|
||||
export function getKhhaoSourceConfig() {
|
||||
return loadKhhaoSourceConfigFromFile()
|
||||
}
|
||||
|
||||
export function saveKhhaoSourceConfig(rawValue) {
|
||||
const normalized = normalizeKhhaoSourceConfig(rawValue)
|
||||
fs.mkdirSync(path.dirname(KHHAO_SOURCES_FILE_PATH), { recursive: true })
|
||||
fs.writeFileSync(KHHAO_SOURCES_FILE_PATH, `${JSON.stringify(normalized, null, 2)}\n`, 'utf8')
|
||||
return normalized
|
||||
}
|
||||
|
||||
function loadKhhaoSourceConfigFromFile() {
|
||||
if (!fs.existsSync(KHHAO_SOURCES_FILE_PATH)) {
|
||||
return createDefaultKhhaoSourceConfig()
|
||||
}
|
||||
|
||||
try {
|
||||
const rawText = fs.readFileSync(KHHAO_SOURCES_FILE_PATH, 'utf8')
|
||||
return normalizeKhhaoSourceConfig(JSON.parse(rawText))
|
||||
} catch {
|
||||
return createDefaultKhhaoSourceConfig()
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeKhhaoSourceConfig(rawValue) {
|
||||
const source = isPlainObject(rawValue) ? rawValue : {}
|
||||
|
||||
return {
|
||||
enabled: typeof source.enabled === 'boolean' ? source.enabled : true,
|
||||
baseUrl: String(source.baseUrl || 'https://admin.khhao.com').trim() || 'https://admin.khhao.com',
|
||||
username: String(source.username || '').trim(),
|
||||
password: String(source.password || '').trim(),
|
||||
maxCaptchaAttempts: normalizePositiveInteger(source.maxCaptchaAttempts, 3),
|
||||
autoSync: normalizeAutoSyncConfig(source.autoSync),
|
||||
}
|
||||
}
|
||||
|
||||
function createDefaultKhhaoSourceConfig() {
|
||||
return {
|
||||
enabled: true,
|
||||
baseUrl: 'https://admin.khhao.com',
|
||||
username: '',
|
||||
password: '',
|
||||
maxCaptchaAttempts: 3,
|
||||
autoSync: normalizeAutoSyncConfig({}),
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeAutoSyncConfig(rawValue) {
|
||||
const source = isPlainObject(rawValue) ? rawValue : {}
|
||||
|
||||
return {
|
||||
enabled: typeof source.enabled === 'boolean' ? source.enabled : false,
|
||||
intervalMinutes: normalizePositiveInteger(source.intervalMinutes, 3),
|
||||
pages: normalizePositiveInteger(source.pages, 2),
|
||||
pageSize: normalizePositiveInteger(source.pageSize, 20),
|
||||
}
|
||||
}
|
||||
|
||||
function normalizePositiveInteger(value, fallback) {
|
||||
const parsed = Number(value)
|
||||
return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback
|
||||
}
|
||||
|
||||
function isPlainObject(value) {
|
||||
return Object.prototype.toString.call(value) === '[object Object]'
|
||||
}
|
||||
@@ -1,129 +0,0 @@
|
||||
// @ts-check
|
||||
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
|
||||
import { PROJECT_ROOT } from '../../../config/runtime.js'
|
||||
import { nowIso } from '../../../utils/time.js'
|
||||
|
||||
const KHHAO_SYNC_STATE_FILE_PATH = path.join(PROJECT_ROOT, 'data', 'khhao-sync-state.json')
|
||||
|
||||
export function getKhhaoSyncStateFilePath() {
|
||||
return KHHAO_SYNC_STATE_FILE_PATH
|
||||
}
|
||||
|
||||
export function getKhhaoSyncState() {
|
||||
return loadKhhaoSyncStateFromFile()
|
||||
}
|
||||
|
||||
export function saveKhhaoSyncState(rawValue) {
|
||||
const normalized = normalizeKhhaoSyncState(rawValue)
|
||||
fs.mkdirSync(path.dirname(KHHAO_SYNC_STATE_FILE_PATH), { recursive: true })
|
||||
fs.writeFileSync(KHHAO_SYNC_STATE_FILE_PATH, `${JSON.stringify(normalized, null, 2)}\n`, 'utf8')
|
||||
return normalized
|
||||
}
|
||||
|
||||
export function ensureKhhaoSyncBaseline() {
|
||||
const current = getKhhaoSyncState()
|
||||
if (String(current.syncFromCreatedAt || '').trim()) {
|
||||
return current
|
||||
}
|
||||
|
||||
return saveKhhaoSyncState({
|
||||
...current,
|
||||
syncFromCreatedAt: nowIso(),
|
||||
})
|
||||
}
|
||||
|
||||
function loadKhhaoSyncStateFromFile() {
|
||||
if (!fs.existsSync(KHHAO_SYNC_STATE_FILE_PATH)) {
|
||||
return createDefaultKhhaoSyncState()
|
||||
}
|
||||
|
||||
try {
|
||||
return normalizeKhhaoSyncState(JSON.parse(fs.readFileSync(KHHAO_SYNC_STATE_FILE_PATH, 'utf8')))
|
||||
} catch {
|
||||
return createDefaultKhhaoSyncState()
|
||||
}
|
||||
}
|
||||
|
||||
function createDefaultKhhaoSyncState() {
|
||||
return {
|
||||
syncFromCreatedAt: '',
|
||||
lastRunStartedAt: '',
|
||||
lastRunFinishedAt: '',
|
||||
lastRunStatus: 'idle',
|
||||
lastErrorMessage: '',
|
||||
fetchedCount: 0,
|
||||
syncedCount: 0,
|
||||
ignoredCount: 0,
|
||||
lastOrderCreatedAt: '',
|
||||
watchMode: 'normal',
|
||||
watchOrders: [],
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeKhhaoSyncState(rawValue) {
|
||||
const source = isPlainObject(rawValue) ? rawValue : {}
|
||||
|
||||
return {
|
||||
syncFromCreatedAt: String(source.syncFromCreatedAt || '').trim(),
|
||||
lastRunStartedAt: String(source.lastRunStartedAt || '').trim(),
|
||||
lastRunFinishedAt: String(source.lastRunFinishedAt || '').trim(),
|
||||
lastRunStatus: normalizeStatus(source.lastRunStatus),
|
||||
lastErrorMessage: String(source.lastErrorMessage || '').trim(),
|
||||
fetchedCount: normalizeNonNegativeInteger(source.fetchedCount),
|
||||
syncedCount: normalizeNonNegativeInteger(source.syncedCount),
|
||||
ignoredCount: normalizeNonNegativeInteger(source.ignoredCount),
|
||||
lastOrderCreatedAt: String(source.lastOrderCreatedAt || '').trim(),
|
||||
watchMode: normalizeWatchMode(source.watchMode),
|
||||
watchOrders: normalizeWatchOrders(source.watchOrders),
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeStatus(value) {
|
||||
const normalized = String(value || '').trim()
|
||||
return normalized || 'idle'
|
||||
}
|
||||
|
||||
function normalizeNonNegativeInteger(value) {
|
||||
const parsed = Number(value)
|
||||
return Number.isInteger(parsed) && parsed >= 0 ? parsed : 0
|
||||
}
|
||||
|
||||
function normalizeWatchMode(value) {
|
||||
return String(value || '').trim() === 'boosted' ? 'boosted' : 'normal'
|
||||
}
|
||||
|
||||
function normalizeWatchOrders(value) {
|
||||
return (Array.isArray(value) ? value : [])
|
||||
.map((item) => normalizeWatchOrder(item))
|
||||
.filter((item) => item.platformOrderId)
|
||||
}
|
||||
|
||||
function normalizeWatchOrder(value) {
|
||||
const source = isPlainObject(value) ? value : {}
|
||||
|
||||
return {
|
||||
platformOrderId: String(source.platformOrderId || '').trim(),
|
||||
shopId: String(source.shopId || '').trim(),
|
||||
shopName: String(source.shopName || '').trim(),
|
||||
itemTitle: String(source.itemTitle || '').trim(),
|
||||
payStatus: normalizeWatchPayStatus(source.payStatus),
|
||||
detectedAt: String(source.detectedAt || '').trim(),
|
||||
lastSeenAt: String(source.lastSeenAt || '').trim(),
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeWatchPayStatus(value) {
|
||||
const normalized = String(value || '').trim().toLowerCase()
|
||||
if (normalized === 'paid') {
|
||||
return 'paid'
|
||||
}
|
||||
|
||||
return normalized === 'unpaid' ? 'unpaid' : 'unknown'
|
||||
}
|
||||
|
||||
function isPlainObject(value) {
|
||||
return Object.prototype.toString.call(value) === '[object Object]'
|
||||
}
|
||||
@@ -30,10 +30,6 @@ export {}
|
||||
* @typedef {import('./admin-read-inputs.js').AdminWebhookEventListQueryInput} AdminWebhookEventRouteQuery
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {import('./admin-write-inputs.js').AdminKhhaoSourceConfigInput} AdminKhhaoSourceConfigRouteBody
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {import('./admin-write-inputs.js').AdminKuaishouEticketSourceConfigInput} AdminKuaishouEticketSourceConfigRouteBody
|
||||
*/
|
||||
@@ -54,14 +50,6 @@ export {}
|
||||
* @typedef {import('./admin-write-inputs.js').AdminKuaishouEticketShopInfoInput} AdminKuaishouEticketShopInfoRouteBody
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {import('./admin-write-inputs.js').AdminKhhaoTestLoginInput} AdminKhhaoTestLoginRouteBody
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {import('./admin-write-inputs.js').AdminKhhaoOrderQueryInput} AdminKhhaoOrderQueryRouteBody
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {import('./admin-write-inputs.js').AdminCloudtentaclesSourceConfigInput} AdminCloudtentaclesSourceConfigRouteBody
|
||||
*/
|
||||
|
||||
@@ -75,7 +75,6 @@ export {}
|
||||
* provider?: string
|
||||
* platform?: string
|
||||
* shopId?: string
|
||||
* khhaoShopId?: string
|
||||
* internalSkuCode?: string
|
||||
* internalSkuName?: string
|
||||
* externalSkuCode?: string
|
||||
@@ -103,22 +102,6 @@ export {}
|
||||
* }} AdminKuaishouCloudFulfillmentConfigInput
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {{
|
||||
* enabled?: boolean
|
||||
* baseUrl?: string
|
||||
* username?: string
|
||||
* password?: string
|
||||
* maxCaptchaAttempts?: number | string
|
||||
* autoSync?: {
|
||||
* enabled?: boolean
|
||||
* intervalMinutes?: number | string
|
||||
* pages?: number | string
|
||||
* pageSize?: number | string
|
||||
* }
|
||||
* }} AdminKhhaoSourceConfigInput
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {{
|
||||
* shopId?: string
|
||||
@@ -167,27 +150,6 @@ export {}
|
||||
* }} AdminKuaishouEticketConsumeInput
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {{
|
||||
* baseUrl?: string
|
||||
* username?: string
|
||||
* password?: string
|
||||
* maxCaptchaAttempts?: number | string
|
||||
* includeImageBase64?: boolean
|
||||
* }} AdminKhhaoTestLoginInput
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {{
|
||||
* baseUrl?: string
|
||||
* username?: string
|
||||
* password?: string
|
||||
* page?: number | string
|
||||
* limit?: number | string
|
||||
* maxCaptchaAttempts?: number | string
|
||||
* }} AdminKhhaoOrderQueryInput
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {{
|
||||
* enabled?: boolean
|
||||
@@ -303,7 +265,6 @@ export {}
|
||||
* platform?: string
|
||||
* shopId?: string
|
||||
* shopName?: string
|
||||
* khhaoShopId?: string
|
||||
* skuCode?: string
|
||||
* skuName?: string
|
||||
* profileKey?: string
|
||||
|
||||
@@ -98,13 +98,6 @@ export {}
|
||||
* timestampToleranceSeconds: number
|
||||
* cardsEncoding: string
|
||||
* }
|
||||
* khhao: {
|
||||
* baseUrl: string
|
||||
* timeoutMs: number
|
||||
* loginPath: string
|
||||
* captchaPath: string
|
||||
* orderListPath: string
|
||||
* }
|
||||
* cloudtentacles: {
|
||||
* baseUrl: string
|
||||
* timeoutMs: number
|
||||
|
||||
Reference in New Issue
Block a user