履约配置优化

This commit is contained in:
yml
2026-05-03 23:03:21 +08:00
parent 0f0253bffe
commit f07a4f8dcb
27 changed files with 2767 additions and 633 deletions
+26
View File
@@ -9,6 +9,7 @@ import {
dispatchAdminTaskKuaishouCloudFulfillment,
markAdminTaskManualReview,
prepareAdminTaskKuaishouCloudFulfillment,
refreshAdminTaskKuaishouCloudRoleInfo,
regenerateAdminTaskClaimLink,
redeemAdminTaskAssisted,
releaseAdminTaskInventoryBinding,
@@ -299,6 +300,31 @@ router.post('/tasks/:taskId/kuaishou-cloud/prepare', requireAdminRoles(['admin',
},
))
router.post('/tasks/:taskId/kuaishou-cloud/refresh-role-info', requireAdminRoles(['admin', 'operator', 'support']), createJsonHandler(
(req) => refreshAdminTaskKuaishouCloudRoleInfo(
(/** @type {AdminTaskRouteParams} */ (req.params)).taskId,
/** @type {AdminRouteAdminSession | null} */ (req.adminSession),
),
{
successMessage: '角色信息已刷新',
errorMessage: '刷新角色信息失败',
scope: '[admin/tasks/:taskId/kuaishou-cloud/refresh-role-info]',
audit: (req, data) => {
const result = /** @type {AdminTaskActionResponse} */ (data)
return {
action: 'task_kuaishou_cloud_role_info_refreshed',
targetType: 'task',
targetId: String((/** @type {AdminTaskRouteParams} */ (req.params)).taskId),
data: {
taskId: result.task.taskId,
taskNo: result.task.taskNo,
status: result.task.status,
},
}
},
},
))
router.post('/tasks/:taskId/kuaishou-cloud/dispatch', requireAdminRoles(['admin', 'operator']), createJsonHandler(
(req) => dispatchAdminTaskKuaishouCloudFulfillment(
(/** @type {AdminTaskRouteParams} */ (req.params)).taskId,
@@ -281,6 +281,16 @@ export function getAdminKhhaoSourceConfig() {
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(),
})) : [],
},
}
}
@@ -358,6 +368,16 @@ export function updateAdminKhhaoSourceConfig(payload = /** @type {AdminKhhaoSour
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(),
})) : [],
},
}
}
@@ -814,6 +834,7 @@ export function getAdminKuaishouCloudFulfillmentConfig() {
autoReturnNumberAfterDispatch: item.autoReturnNumberAfterDispatch === true,
autoConsumeAfterDispatch: item.autoConsumeAfterDispatch === true,
kuaishouConsumeShopId: String(item.kuaishouConsumeShopId || '').trim(),
kuaishouConsumeShopName: String(item.kuaishouConsumeShopName || '').trim(),
notes: String(item.notes || '').trim(),
})),
},
@@ -857,6 +878,7 @@ export async function updateAdminKuaishouCloudFulfillmentConfig(
autoReturnNumberAfterDispatch: item.autoReturnNumberAfterDispatch === true,
autoConsumeAfterDispatch: item.autoConsumeAfterDispatch === true,
kuaishouConsumeShopId: String(item.kuaishouConsumeShopId || '').trim(),
kuaishouConsumeShopName: String(item.kuaishouConsumeShopName || '').trim(),
notes: String(item.notes || '').trim(),
})),
},
@@ -297,6 +297,9 @@ export async function getAdminTaskDetail(taskId, session = null) {
&& String(kuaishouCloudFulfillment?.dispatch.status || 'pending').trim() === 'pending'
&& String(kuaishouCloudFulfillment?.returnNumber.status || 'pending').trim() === 'pending'
&& ['pending_binding_prepare', 'manual_review', 'failed'].includes(String(task.task_status || '').trim()),
canRefreshKuaishouCloudRoleInfo: viewerContext.canOperateAssistedTask
&& String(task.executor_key || '').trim() === 'kuaishou_ct_assisted'
&& Number(kuaishouCloudFulfillment?.binding.vnId || 0) > 0,
canDispatchKuaishouCloudFulfillment: viewerContext.canManageTaskLifecycle
&& String(task.executor_key || '').trim() === 'kuaishou_ct_assisted'
&& ['waiting_binding'].includes(String(task.task_status || '').trim()),
@@ -32,6 +32,7 @@ export function mapKuaishouCloudFulfillmentContext(value) {
const ticket = value.ticket && typeof value.ticket === 'object' ? value.ticket : {}
const binding = value.binding && typeof value.binding === 'object' ? value.binding : {}
const role = value.role && typeof value.role === 'object' ? value.role : {}
const purchase = value.purchase && typeof value.purchase === 'object' ? value.purchase : {}
const dispatch = value.dispatch && typeof value.dispatch === 'object' ? value.dispatch : {}
const returnNumber = value.returnNumber && typeof value.returnNumber === 'object' ? value.returnNumber : {}
@@ -61,6 +62,14 @@ export function mapKuaishouCloudFulfillmentContext(value) {
bindUrl: String(binding.bindUrl || '').trim(),
bindPreparedAt: binding.bindPreparedAt || null,
},
role: {
status: String(role.status || 'pending').trim() || 'pending',
name: String(role.name || '').trim(),
rid: String(role.rid || '').trim(),
refreshedAt: role.refreshedAt || null,
errorMessage: String(role.errorMessage || '').trim(),
rawInfo: role.rawInfo && typeof role.rawInfo === 'object' ? role.rawInfo : null,
},
purchase: {
autoBuyEnabled: purchase.autoBuyEnabled !== false,
minAssetReserve: Number(purchase.minAssetReserve || 0) || 0,
@@ -84,6 +93,7 @@ export function mapKuaishouCloudFulfillmentContext(value) {
consume: {
status: String(consume.status || 'pending').trim() || 'pending',
shopId: String(consume.shopId || '').trim(),
shopName: String(consume.shopName || '').trim(),
autoConsumeEnabled: consume.autoConsumeEnabled === true,
consumedAt: consume.consumedAt || null,
errorMessage: String(consume.errorMessage || '').trim(),
@@ -40,6 +40,7 @@ import {
import {
appointCloudtentaclesVirtualNumber,
backCloudtentaclesVirtualNumber,
getCloudtentaclesBindInfo,
fetchCloudtentaclesVirtualNumberCode,
generateCloudtentaclesLoginCode,
getCloudtentaclesBindUrl,
@@ -973,6 +974,85 @@ export async function dispatchAdminTaskKuaishouCloudFulfillment(taskId, payload
}
}
/** @returns {Promise<AdminTaskActionResponse>} */
/** @param {AdminEntityIdInput} taskId */
/** @param {AdminViewerSessionInput | null} [session] */
export async function refreshAdminTaskKuaishouCloudRoleInfo(taskId, session = null) {
const task = await getRequiredTask(taskId)
const now = nowIso()
const viewerContext = createAdminViewerContext(session)
if (!viewerContext.canOperateAssistedTask) {
throw createHttpError('当前账号没有权限刷新角色信息', {
statusCode: 403,
errorCode: 'admin_task_refresh_kuaishou_cloud_role_forbidden',
})
}
if (!isKuaishouCloudTask(task)) {
throw createHttpError('当前任务不是快手 cloud 履约任务', {
statusCode: 409,
errorCode: 'admin_task_not_kuaishou_cloud',
})
}
const taskContext = parseTaskContext(task)
const flow = normalizeKuaishouCloudFlow(taskContext.kuaishouCloudFulfillment)
if (!flow.binding.vnId || !flow.binding.vnKey) {
throw createHttpError('当前任务还没有可查询的绑定角色信息,请先准备绑定资源', {
statusCode: 409,
errorCode: 'admin_task_kuaishou_cloud_missing_bind_info_context',
})
}
const cloudContext = resolvePersistedCloudtentaclesContext()
const bindInfoResult = await getCloudtentaclesBindInfo({
...cloudContext,
key: flow.binding.vnKey,
id: flow.binding.vnId,
})
const bindInfo = normalizeKuaishouCloudRoleInfo(bindInfoResult.bindInfo)
const nextContext = {
...taskContext,
kuaishouCloudFulfillment: {
...flow,
role: {
status: bindInfo.name || bindInfo.rid ? 'ready' : 'pending',
name: bindInfo.name,
rid: bindInfo.rid,
refreshedAt: now,
errorMessage: bindInfo.name || bindInfo.rid ? '' : '当前还没有查询到角色信息,请让客户完成绑定后再刷新',
rawInfo: bindInfo.rawInfo,
},
},
}
const updatedTask = await updateTask(task.id, {
role_id: bindInfo.rid || task.role_id || '',
role_name: bindInfo.name || task.role_name || '',
context_json: JSON.stringify(nextContext),
updated_at: now,
})
await createTaskEvent(task.id, 'kuaishou_cloud_role_info_refreshed', {
roleName: bindInfo.name,
roleId: bindInfo.rid,
vnId: flow.binding.vnId,
refreshedBy: session
? {
userId: Number(session.userId || 0) || 0,
username: String(session.username || '').trim(),
role: String(session.role || '').trim(),
}
: null,
}, now)
return {
task: mapTaskActionPayload(updatedTask),
}
}
/** @returns {Promise<AdminTaskActionResponse>} */
/** @param {AdminEntityIdInput} taskId */
/** @param {AdminViewerSessionInput | null} [session] */
@@ -1085,6 +1165,7 @@ export async function returnNumberAdminTaskKuaishouCloudFulfillment(taskId, sess
...flow.consume,
status: consumeStatus,
shopId: shopId || flow.consume.shopId,
shopName: String(flow.consume.shopName || order?.shop_name || '').trim(),
autoConsumeEnabled: flow.consume.autoConsumeEnabled === true,
consumedAt,
errorMessage: consumeErrorMessage,
@@ -1202,6 +1283,7 @@ function resolvePersistedCloudtentaclesContext() {
function normalizeKuaishouCloudFlow(value) {
const source = value && typeof value === 'object' ? value : {}
const binding = source.binding && typeof source.binding === 'object' ? source.binding : {}
const role = source.role && typeof source.role === 'object' ? source.role : {}
const purchase = source.purchase && typeof source.purchase === 'object' ? source.purchase : {}
const dispatch = source.dispatch && typeof source.dispatch === 'object' ? source.dispatch : {}
const returnNumber = source.returnNumber && typeof source.returnNumber === 'object' ? source.returnNumber : {}
@@ -1235,6 +1317,14 @@ function normalizeKuaishouCloudFlow(value) {
bindUrl: String(binding.bindUrl || '').trim(),
bindPreparedAt: binding.bindPreparedAt || null,
},
role: {
status: String(role.status || 'pending').trim() || 'pending',
name: String(role.name || '').trim(),
rid: String(role.rid || '').trim(),
refreshedAt: role.refreshedAt || null,
errorMessage: String(role.errorMessage || '').trim(),
rawInfo: role.rawInfo && typeof role.rawInfo === 'object' ? role.rawInfo : null,
},
purchase: {
autoBuyEnabled: purchase.autoBuyEnabled !== false,
minAssetReserve: Number(purchase.minAssetReserve || 0) || 0,
@@ -1260,6 +1350,7 @@ function normalizeKuaishouCloudFlow(value) {
consume: {
status: String(consume.status || 'pending').trim() || 'pending',
shopId: String(consume.shopId || '').trim(),
shopName: String(consume.shopName || '').trim(),
autoConsumeEnabled: consume.autoConsumeEnabled === true,
consumedAt: consume.consumedAt || null,
errorMessage: String(consume.errorMessage || '').trim(),
@@ -1267,6 +1358,16 @@ function normalizeKuaishouCloudFlow(value) {
}
}
function normalizeKuaishouCloudRoleInfo(value) {
const rawInfo = value && typeof value === 'object' ? value : null
return {
name: String(rawInfo?.name || rawInfo?.roleName || rawInfo?.nickname || '').trim(),
rid: String(rawInfo?.rid || rawInfo?.roleId || rawInfo?.uid || '').trim(),
rawInfo,
}
}
function resolveKuaishouCloudBindingResources(flow, { skuItems = [], knapsackItems = [] } = {}) {
const normalizedSkuItems = Array.isArray(skuItems) ? skuItems.filter(isCloudSkuLikeItem) : []
const normalizedKnapsackItems = Array.isArray(knapsackItems) ? knapsackItems.filter(isCloudSkuLikeItem) : []
@@ -918,6 +918,8 @@ function mapClaimKuaishouCloudFulfillment(task, order) {
},
consume: {
status: String(consume.status || 'pending').trim() || 'pending',
shopId: String(consume.shopId || '').trim(),
shopName: String(consume.shopName || '').trim(),
consumedAt: consume.consumedAt || null,
errorMessage: String(consume.errorMessage || '').trim(),
},
@@ -161,6 +161,7 @@ function normalizeKuaishouCloudFlow(value) {
consume: {
status: String(consume.status || 'pending').trim() || 'pending',
shopId: String(consume.shopId || '').trim(),
shopName: String(consume.shopName || '').trim(),
},
}
}
@@ -116,6 +116,14 @@ export async function syncDeliveryTasksForOrder(order, orderItems) {
bindUrl: '',
bindPreparedAt: null,
},
role: {
status: 'pending',
name: '',
rid: '',
refreshedAt: null,
errorMessage: '',
rawInfo: null,
},
purchase: {
autoBuyEnabled: fulfillmentConfig.cloudtentacles?.autoBuyEnabled !== false,
minAssetReserve: Number(fulfillmentConfig.cloudtentacles?.minAssetReserve || 0) || 0,
@@ -141,6 +149,7 @@ export async function syncDeliveryTasksForOrder(order, orderItems) {
consume: {
status: 'pending',
shopId: String(fulfillmentConfig.kuaishouConsume?.shopId || kuaishouShopConfig.shopId || '').trim(),
shopName: String(fulfillmentConfig.kuaishouConsume?.shopName || kuaishouShopConfig.kshopName || order.shop_name || '').trim(),
autoConsumeEnabled: fulfillmentConfig.kuaishouConsume?.autoConsumeAfterDispatch === true,
consumedAt: null,
errorMessage: '',
@@ -52,6 +52,7 @@ export function mapKuaishouCloudFulfillmentItemsToBindings(config = {}) {
},
kuaishouConsume: {
shopId: String(item.kuaishouConsumeShopId || '').trim(),
shopName: String(item.kuaishouConsumeShopName || '').trim(),
autoConsumeAfterDispatch: item.autoConsumeAfterDispatch === true,
},
notes: String(item.notes || '').trim(),
@@ -137,6 +138,7 @@ function normalizeKuaishouCloudFulfillmentItem(rawValue) {
autoReturnNumberAfterDispatch: rawValue.autoReturnNumberAfterDispatch === true,
autoConsumeAfterDispatch: rawValue.autoConsumeAfterDispatch === true,
kuaishouConsumeShopId: String(rawValue.kuaishouConsumeShopId || '').trim(),
kuaishouConsumeShopName: String(rawValue.kuaishouConsumeShopName || '').trim(),
notes: String(rawValue.notes || '').trim(),
}
}
@@ -25,6 +25,7 @@ export function resolveCloudtentaclesConfig(overrides = {}) {
vnVerifCodePath: '/public/vn_verif_code',
vnVerifyLoginCodePath: '/vn/verify_login_code',
vnBindUrlPath: '/vn/bind_url',
vnBindInfoPath: '/vn/bind_info',
vnBackPath: '/vn/back',
publicKeyPem: '',
clientSource: 'ct-client',
@@ -57,6 +58,7 @@ export function resolveCloudtentaclesConfig(overrides = {}) {
'/vn/verify_login_code',
),
vnBindUrlPath: normalizePath(overrides.vnBindUrlPath || baseConfig.vnBindUrlPath, '/vn/bind_url'),
vnBindInfoPath: normalizePath(overrides.vnBindInfoPath || baseConfig.vnBindInfoPath, '/vn/bind_info'),
vnBackPath: normalizePath(overrides.vnBackPath || baseConfig.vnBackPath, '/vn/back'),
publicKeyPem: normalizePem(overrides.publicKeyPem || baseConfig.publicKeyPem),
clientSource: String(overrides.clientSource || baseConfig.clientSource || 'ct-client').trim() || 'ct-client',
@@ -159,6 +159,35 @@ export async function getCloudtentaclesBindUrl(payload = {}) {
}
}
export async function getCloudtentaclesBindInfo(payload = {}) {
const token = requireToken(payload.token, 'cloudtentacles 获取绑定信息缺少 token', 'cloudtentacles_vn_bind_info_missing_token')
const key = requireKey(payload.key, 'cloudtentacles 获取绑定信息缺少 key', 'cloudtentacles_vn_bind_info_missing_key')
const id = requireId(payload.id, 'cloudtentacles 获取绑定信息缺少 id', 'cloudtentacles_vn_bind_info_missing_id')
const config = resolveCloudtentaclesConfig(payload)
const result = await cloudtentaclesRequest(config.vnBindInfoPath, {
...config,
method: 'POST',
token,
body: [{ id, key }],
businessErrorStatusCode: 401,
businessErrorCode: 'cloudtentacles_vn_bind_info_failed',
})
const items = Array.isArray(result.payload?.data) ? result.payload.data : []
const matchedItem = items.find((item) => Number(item?.id || 0) === id) || items[0] || {}
const bindInfo = parseBindInfo(matchedItem?.bind_info)
return {
baseUrl: config.baseUrl,
key,
id,
bindInfo,
rawItem: isPlainObject(matchedItem) ? matchedItem : {},
raw: result.payload ?? null,
}
}
export async function backCloudtentaclesVirtualNumber(payload = {}) {
const token = requireToken(payload.token, 'cloudtentacles 退还号码缺少 token', 'cloudtentacles_vn_back_missing_token')
const key = requireKey(payload.key, 'cloudtentacles 退还号码缺少 key', 'cloudtentacles_vn_back_missing_key')
@@ -185,14 +214,14 @@ export async function backCloudtentaclesVirtualNumber(payload = {}) {
function mapVirtualNumberItem(item) {
const source = isPlainObject(item) ? item : {}
const bindInfoText = String(source.bind_info || '').trim()
const bindInfo = parseBindInfo(source.bind_info)
return {
id: Number(source.id || 0),
phone: String(source.phone || '').trim(),
status: Number(source.status || 0),
countTime: Number(source.count_time || 0),
bindInfo: tryParseJson(bindInfoText) || bindInfoText || null,
bindInfo,
raw: source,
}
}
@@ -241,6 +270,11 @@ function tryParseJson(text) {
}
}
function parseBindInfo(value) {
const bindInfoText = String(value || '').trim()
return tryParseJson(bindInfoText) || bindInfoText || null
}
function isPlainObject(value) {
return Boolean(value) && typeof value === 'object' && !Array.isArray(value)
}
@@ -8,6 +8,8 @@ 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)
@@ -22,6 +24,7 @@ export function stopKhhaoAutoSyncLoop() {
async function runKhhaoAutoSyncCycle() {
const source = getKhhaoSourceConfig()
const currentState = getKhhaoSyncState()
if (!source.enabled || !source.autoSync?.enabled) {
scheduleNextRun(resolveIntervalMs(source.autoSync?.intervalMinutes))
@@ -30,7 +33,7 @@ async function runKhhaoAutoSyncCycle() {
if (running) {
logWarn('[khhao/auto-sync]', '检测到上一次自动同步仍在运行,本轮跳过')
scheduleNextRun(resolveIntervalMs(source.autoSync?.intervalMinutes))
scheduleNextRun(resolveNextDelayMs(source, currentState))
return
}
@@ -63,6 +66,8 @@ async function runKhhaoAutoSyncCycle() {
})
const latestCreatedAt = resolveLatestOrderCreatedAt(result.results)
const nextWatchOrders = reconcileKhhaoWatchOrders(baselineState.watchOrders, result.results, startedAt)
const watchMode = nextWatchOrders.length > 0 ? 'boosted' : 'normal'
saveKhhaoSyncState({
...getKhhaoSyncState(),
@@ -75,6 +80,8 @@ async function runKhhaoAutoSyncCycle() {
syncedCount: result.syncedCount,
ignoredCount: result.ignoredCount,
lastOrderCreatedAt: latestCreatedAt,
watchMode,
watchOrders: nextWatchOrders,
})
logInfo('[khhao/auto-sync]', 'khhao 自动同步完成', {
@@ -82,6 +89,13 @@ async function runKhhaoAutoSyncCycle() {
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 自动同步失败')
@@ -96,7 +110,7 @@ async function runKhhaoAutoSyncCycle() {
logError('[khhao/auto-sync]', 'khhao 自动同步失败', error)
} finally {
running = false
scheduleNextRun(resolveIntervalMs(source.autoSync?.intervalMinutes))
scheduleNextRun(resolveNextDelayMs(source, getKhhaoSyncState()))
}
}
@@ -113,6 +127,80 @@ function resolveIntervalMs(value) {
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
@@ -0,0 +1,79 @@
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,7 +1,12 @@
// @ts-check
import { upsertOrderFromSource } from '../../order/order-service.js'
import { mapKhhaoOrderPreview, mapKhhaoOrderToSourceEvent } from './order-mapper-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'
@@ -57,8 +62,11 @@ export async function syncKhhaoOrders(payload = {}) {
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',
@@ -77,8 +85,11 @@ export async function syncKhhaoOrders(payload = {}) {
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(),
@@ -129,3 +140,11 @@ function parseKhhaoOrderCreatedAt(value) {
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)
}
@@ -58,6 +58,8 @@ function createDefaultKhhaoSyncState() {
syncedCount: 0,
ignoredCount: 0,
lastOrderCreatedAt: '',
watchMode: 'normal',
watchOrders: [],
}
}
@@ -74,6 +76,8 @@ function normalizeKhhaoSyncState(rawValue) {
syncedCount: normalizeNonNegativeInteger(source.syncedCount),
ignoredCount: normalizeNonNegativeInteger(source.ignoredCount),
lastOrderCreatedAt: String(source.lastOrderCreatedAt || '').trim(),
watchMode: normalizeWatchMode(source.watchMode),
watchOrders: normalizeWatchOrders(source.watchOrders),
}
}
@@ -87,6 +91,39 @@ function normalizeNonNegativeInteger(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]'
}
@@ -90,6 +90,7 @@ export {}
* autoReturnNumberAfterDispatch?: boolean
* autoConsumeAfterDispatch?: boolean
* kuaishouConsumeShopId?: string
* kuaishouConsumeShopName?: string
* notes?: string
* }} AdminKuaishouCloudFulfillmentItemInput
*/