履约配置优化

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
@@ -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]'
}