refactor(fulfillment): 移除飞飞/乐玩通道,仅保留 affiliate_dash

新单只路由 affiliate_dash(未命中走 manual_dispatch);历史飞飞/乐玩任务保留只读 stub,避免订单重同步失败。同步清理 cloudtentacles 调度、Admin 写路径与相关 env。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
yml2213
2026-09-14 17:29:47 +08:00
co-authored by Cursor
parent 0cbcb89dd8
commit b3aad0044c
155 changed files with 661 additions and 23351 deletions
@@ -19,7 +19,6 @@ import {
listKuaishouIndustryVouchersByOids,
listKuaishouIndustryVouchersByTaskId,
} from '../../repositories/kuaishou-industry-voucher-repo.js'
import { listCloudtentaclesSources } from '../platforms/cloudtentacles/source-config-service.js'
import { createHttpError } from '../../utils/http.js'
import { formatFenToAmount, normalizeFen } from '../../utils/money.js'
@@ -31,11 +30,7 @@ import {
} from './admin-query-utils.js'
import {
canCompleteManualDispatchTaskStatus,
canDispatchKuaishouCloudFulfillmentStatus,
canMarkManualReviewTaskStatus,
canPrepareKuaishouCloudFulfillmentStatus,
canRebindKuaishouCloudRoleStatus,
canReturnKuaishouCloudFulfillmentStatus,
canRetryTaskStatus,
} from '../../domain/task-status.js'
import {
@@ -399,30 +394,11 @@ export async function getAdminTaskDetail(
viewerContext.canManageTaskLifecycle &&
isManualDispatchTask(task) &&
canCompleteManualDispatchTaskStatus(task.task_status),
canPrepareKuaishouCloudFulfillment:
viewerContext.canManageTaskLifecycle &&
String(task.executor_key || '').trim() === 'kuaishou_ct_assisted' &&
String(kuaishouCloudFulfillment?.dispatch.status || 'pending').trim() === 'pending' &&
String(kuaishouCloudFulfillment?.returnNumber.status || 'pending').trim() === 'pending' &&
canPrepareKuaishouCloudFulfillmentStatus(task.task_status),
canRefreshKuaishouCloudRoleInfo:
viewerContext.canOperateAssistedTask &&
String(task.executor_key || '').trim() === 'kuaishou_ct_assisted' &&
Number(kuaishouCloudFulfillment?.binding.vnId || 0) > 0,
canRebindKuaishouCloudRole:
viewerContext.canOperateAssistedTask &&
String(task.executor_key || '').trim() === 'kuaishou_ct_assisted' &&
canRebindKuaishouCloudRoleStatus(task.task_status) &&
String(kuaishouCloudFulfillment?.dispatch.status || 'pending').trim() !== 'success' &&
Number(kuaishouCloudFulfillment?.binding.vnId || 0) > 0,
canDispatchKuaishouCloudFulfillment:
viewerContext.canManageTaskLifecycle &&
String(task.executor_key || '').trim() === 'kuaishou_ct_assisted' &&
canDispatchKuaishouCloudFulfillmentStatus(task.task_status),
canReturnKuaishouCloudFulfillment:
viewerContext.canManageTaskLifecycle &&
String(task.executor_key || '').trim() === 'kuaishou_ct_assisted' &&
canReturnKuaishouCloudFulfillmentStatus(task.task_status),
canPrepareKuaishouCloudFulfillment: false,
canRefreshKuaishouCloudRoleInfo: false,
canRebindKuaishouCloudRole: false,
canDispatchKuaishouCloudFulfillment: false,
canReturnKuaishouCloudFulfillment: false,
canResendKuaishouIndustryVoucherCode:
viewerContext.canOperateKuaishouIndustryVoucher && hasKuaishouIndustryVoucher,
canConsumeKuaishouIndustryVoucher:
@@ -517,17 +493,7 @@ function resolveKuaishouIndustryVoucherForTask(
}
function buildCloudSourceLabelMap() {
const labelMap = new Map<string, string>()
for (const source of listCloudtentaclesSources().sources) {
const key = String(source.key || '').trim()
if (!key) continue
const label = String(source.label || source.username || key).trim() || key
labelMap.set(key, label)
}
return labelMap
return new Map<string, string>()
}
export async function getAdminTaskScreenshotPath(
@@ -1,74 +0,0 @@
import test from 'node:test'
import assert from 'node:assert/strict'
import {
hasCloudtentaclesCredentialContextChanged,
isPlainObject,
pickFirstNonEmpty,
resolveCloudtentaclesAdminContext,
} from './context.js'
test('pickFirstNonEmpty returns first trimmed non-empty string value', () => {
assert.equal(pickFirstNonEmpty(['', ' ', ' value ', 'fallback']), 'value')
assert.equal(pickFirstNonEmpty([null, undefined, 0, '']), '')
})
test('isPlainObject only accepts non-array objects', () => {
assert.equal(isPlainObject({ a: 1 }), true)
assert.equal(isPlainObject([]), false)
assert.equal(isPlainObject(null), false)
assert.equal(isPlainObject('x'), false)
})
test('resolveCloudtentaclesAdminContext merges payload source and persisted session', () => {
const context = resolveCloudtentaclesAdminContext(
{
token: ' token-1 ',
deviceType: 3,
},
{
savedSource: {
baseUrl: ' https://saved.example.com ',
deviceId: ' device-saved ',
deviceType: 1,
},
persistedSession: {
token: 'token-persisted',
deviceId: 'device-persisted',
deviceType: 2,
},
},
)
assert.deepEqual(context, {
sourceKey: 'default',
baseUrl: 'https://saved.example.com',
token: 'token-1',
deviceId: 'device-saved',
deviceType: 3,
})
})
test('hasCloudtentaclesCredentialContextChanged detects normalized credential changes', () => {
assert.equal(
hasCloudtentaclesCredentialContextChanged(
{
baseUrl: ' https://a ',
username: 'u1',
phone: '13812345678',
deviceId: 'd1',
deviceType: 1,
},
{ baseUrl: 'https://a', username: 'u1', phone: '13812345678', deviceId: 'd1', deviceType: 1 },
),
false,
)
assert.equal(
hasCloudtentaclesCredentialContextChanged(
{ baseUrl: 'https://a', username: 'u1', phone: '13812345678', deviceId: 'd1', deviceType: 1 },
{ baseUrl: 'https://b', username: 'u1', phone: '13812345678', deviceId: 'd1', deviceType: 1 },
),
true,
)
})
@@ -1,57 +0,0 @@
export function pickFirstNonEmpty(values: unknown[]) {
for (const value of values) {
const normalized = String(value || '').trim()
if (normalized) {
return normalized
}
}
return ''
}
export function isPlainObject(value: unknown): value is JsonObject {
return Boolean(value) && typeof value === 'object' && !Array.isArray(value)
}
import { getCloudtentaclesSourceByKey } from '../../../platforms/cloudtentacles/source-config-service.js'
import { getCloudtentaclesSessionStateByKey } from '../../../platforms/cloudtentacles/session-state-service.js'
import type { JsonObject } from '../../../../types/json.js'
import {
normalizeCloudtentaclesDeviceId,
normalizeCloudtentaclesDeviceType,
} from '../../../platforms/cloudtentacles/defaults.js'
export function resolveCloudtentaclesAdminContext(
payload: JsonObject = {},
options: JsonObject = {},
) {
const sourceKey = String(payload.sourceKey || options.sourceKey || 'default').trim()
const savedSource = options.savedSource || getCloudtentaclesSourceByKey(sourceKey) || {}
const persistedSession =
options.persistedSession || getCloudtentaclesSessionStateByKey(sourceKey) || {}
const defaultBaseUrl = String(options.defaultBaseUrl || 'https://123.207.217.176').trim()
return {
sourceKey,
baseUrl: pickFirstNonEmpty([payload.baseUrl, savedSource.baseUrl, defaultBaseUrl]),
token: pickFirstNonEmpty([payload.token, persistedSession.token]),
deviceId: normalizeCloudtentaclesDeviceId(
pickFirstNonEmpty([payload.deviceId, savedSource.deviceId, persistedSession.deviceId]),
),
deviceType: normalizeCloudtentaclesDeviceType(
payload.deviceType ?? savedSource.deviceType ?? persistedSession.deviceType,
),
}
}
export function hasCloudtentaclesCredentialContextChanged(
current: JsonObject = {},
next: JsonObject = {},
) {
return (
String(current.baseUrl || '').trim() !== String(next.baseUrl || '').trim() ||
String(current.username || '').trim() !== String(next.username || '').trim() ||
String(current.phone || '').trim() !== String(next.phone || '').trim() ||
String(current.deviceId || '').trim() !== String(next.deviceId || '').trim() ||
Number(current.deviceType || 0) !== Number(next.deviceType || 0)
)
}
@@ -1,883 +0,0 @@
import type { JsonObject } from '../../../../types/json.js'
import {
getCloudtentaclesSourceByKey,
getCloudtentaclesSourcesFilePath,
listCloudtentaclesSources,
saveCloudtentaclesSourceByKey,
saveCloudtentaclesSourcesList,
deleteCloudtentaclesSourceByKey,
} from '../../../platforms/cloudtentacles/source-config-service.js'
import {
clearCloudtentaclesSessionStateByKey,
deleteCloudtentaclesSessionStateByKey,
getCloudtentaclesSessionFilePath,
getCloudtentaclesSessionStateByKey,
saveCloudtentaclesSessionStateByKey,
getAllCloudtentaclesSessionStates,
pruneCloudtentaclesSessionStates,
} from '../../../platforms/cloudtentacles/session-state-service.js'
import {
loginCloudtentaclesSession,
sendCloudtentaclesSmsCode,
validateCloudtentaclesSession,
} from '../../../platforms/cloudtentacles/session-service.js'
import {
buyCloudtentaclesSku,
getCloudtentaclesAsset,
getCloudtentaclesCategories,
listCloudtentaclesSku,
useCloudtentaclesSku,
} from '../../../platforms/cloudtentacles/catalog-service.js'
import { getCloudtentaclesKnapsack } from '../../../platforms/cloudtentacles/knapsack-service.js'
import { listCloudtentaclesDeliveryRecords } from '../../../platforms/cloudtentacles/record-service.js'
import {
appointCloudtentaclesVirtualNumber,
backCloudtentaclesVirtualNumber,
fetchCloudtentaclesVirtualNumberCode,
generateCloudtentaclesLoginCode,
getCloudtentaclesBindUrl,
listCloudtentaclesVirtualNumbers,
verifyCloudtentaclesLoginCode,
} from '../../../platforms/cloudtentacles/virtual-number-service.js'
import { runCloudtentaclesFullDebugFlow } from '../../../platforms/cloudtentacles/debug-flow-service.js'
import {
getCloudtentaclesOverrideRuleConfig,
getCloudtentaclesOverrideRuleFilePath,
saveCloudtentaclesOverrideRuleConfig,
} from '../../../order/cloudtentacles-override-rule-service.js'
import { hasCloudtentaclesCredentialContextChanged, pickFirstNonEmpty } from './context.js'
import {
buildAdminCloudtentaclesLoginResult,
buildAdminCloudtentaclesPersistedSessionPayload,
buildAdminCloudtentaclesValidateResult,
resolveAdminCloudtentaclesCredentialPayload,
resolveAdminCloudtentaclesSessionPayload,
} from './payloads.js'
import { normalizeAdminCloudtentaclesSourceConfigPayload } from './writes.js'
import {
mapAdminCloudtentaclesSession,
mapAdminCloudtentaclesSourceConfig,
maskPhone,
} from './mappers.js'
import { createHttpError } from '../../../../utils/http.js'
import { query as dbQuery } from '../../../../db/client.js'
import { parseJsonObject } from '../../../../utils/task-json.js'
import type {
AdminCloudtentaclesCatalogQueryInput,
AdminCloudtentaclesDeliveryRecordQueryInput,
AdminCloudtentaclesFullFlowInput,
AdminCloudtentaclesOverrideRuleConfigInput,
AdminCloudtentaclesSendSmsCodeInput,
AdminCloudtentaclesSkuBuyInput,
AdminCloudtentaclesSkuUseInput,
AdminCloudtentaclesSourceConfigInput,
AdminCloudtentaclesTestLoginInput,
AdminCloudtentaclesValidateSessionInput,
AdminCloudtentaclesVirtualNumberInput,
} from '../../../../types/admin/write-inputs.js'
type LocalCloudtentaclesTask = {
taskId: number
taskNo: string
platformOrderId: string
orderShopId: string
orderShopName: string
taskStatus: string
deliveryStatus: string
resultCode: string
resultMessage: string
createdAt: string
updatedAt: string
skuCode: string
skuName: string
sourceKey: string
vnId: number
vnPhone: string
roleName: string
roleId: string
localDispatchStatus: string
localDispatchAt: string
consumeShopId: string
consumeShopName: string
}
type LocalCloudtentaclesTaskRow = {
id: number
task_no: string
platform_order_id: string
shop_id: string
shop_name: string
task_status: string
delivery_status: string
result_code: string
result_message: string
context_json: unknown
created_at: string
updated_at: string
sku_code?: string
sku_name?: string
kuaishou_source_key?: string
kuaishou_vn_phone?: string
kuaishou_role_name?: string
kuaishou_role_id?: string
kuaishou_dispatch_status?: string
kuaishou_consume_status?: string
}
export function listAdminCloudtentaclesSources() {
const list = listCloudtentaclesSources()
const sessions = getAllCloudtentaclesSessionStates()
const sources = Array.isArray(list.sources)
? list.sources.map((s) => mapAdminCloudtentaclesSourceConfig(s))
: []
const mappedSessions: JsonObject = {}
for (const [key, session] of Object.entries(sessions.sessions || {})) {
mappedSessions[key] = mapAdminCloudtentaclesSession(session)
}
return {
filePath: getCloudtentaclesSourcesFilePath(),
sessionFilePath: getCloudtentaclesSessionFilePath(),
enabled: list.enabled,
sources,
sessions: mappedSessions,
}
}
export function listAdminCloudtentaclesRecordSources() {
const list = listCloudtentaclesSources()
const sessions = getAllCloudtentaclesSessionStates()
const sources = Array.isArray(list.sources) ? list.sources : []
return {
sources: sources.map((source) => {
const key = String(source.key || '').trim()
const session = (sessions.sessions?.[key] || {}) as JsonObject
return {
key,
label: String(source.label || source.username || key || '未命名账号').trim(),
enabled: source.enabled !== false,
hasToken: Boolean(String(session.token || '').trim()),
loggedInAt: String(session.loggedInAt || '').trim(),
}
}),
}
}
export function getAdminCloudtentaclesOverrideRules() {
return {
filePath: getCloudtentaclesOverrideRuleFilePath(),
...getCloudtentaclesOverrideRuleConfig(),
}
}
export async function updateAdminCloudtentaclesOverrideRules(
payload: AdminCloudtentaclesOverrideRuleConfigInput = {},
) {
return {
filePath: getCloudtentaclesOverrideRuleFilePath(),
...(await saveCloudtentaclesOverrideRuleConfig(payload)),
}
}
export async function updateAdminCloudtentaclesSourceConfig(
payload: AdminCloudtentaclesSourceConfigInput = {},
) {
// 批量保存:payload.sources 是数组时替换整份账号列表。
if (Array.isArray(payload.sources)) {
const normalizedList = payload.sources.map((item) =>
normalizeAdminCloudtentaclesSourceConfigPayload(item),
)
const savedList = await saveCloudtentaclesSourcesList({
enabled: payload.enabled !== false,
sources: normalizedList,
})
const sessions = await pruneCloudtentaclesSessionStates(normalizedList.map((item) => item.key))
const sources = Array.isArray(savedList.sources)
? savedList.sources.map((s) => mapAdminCloudtentaclesSourceConfig(s))
: []
const mappedSessions: JsonObject = {}
for (const [key, session] of Object.entries(sessions.sessions || {})) {
mappedSessions[key] = mapAdminCloudtentaclesSession(session)
}
return {
filePath: getCloudtentaclesSourcesFilePath(),
sessionFilePath: getCloudtentaclesSessionFilePath(),
enabled: savedList.enabled,
sources,
sessions: mappedSessions,
}
}
// 单账号保存。
const sourceKey = String(payload.sourceKey || '').trim() || 'default'
const current = getCloudtentaclesSourceByKey(sourceKey)
const normalized = normalizeAdminCloudtentaclesSourceConfigPayload({
...payload,
sourceKey,
})
const saved = await saveCloudtentaclesSourceByKey(sourceKey, normalized)
const shouldClearSession = hasCloudtentaclesCredentialContextChanged(current || undefined, saved)
const session = shouldClearSession
? (await clearCloudtentaclesSessionStateByKey(sourceKey),
getCloudtentaclesSessionStateByKey(sourceKey))
: getCloudtentaclesSessionStateByKey(sourceKey)
return {
filePath: getCloudtentaclesSourcesFilePath(),
sessionFilePath: getCloudtentaclesSessionFilePath(),
source: mapAdminCloudtentaclesSourceConfig(saved),
session: mapAdminCloudtentaclesSession(session || undefined),
}
}
export async function deleteAdminCloudtentaclesSource(sourceKey: string) {
const key = String(sourceKey || '').trim()
if (!key) {
throw createHttpError('cloudtentacles sourceKey 不能为空', {
statusCode: 400,
errorCode: 'cloudtentacles_source_key_required',
})
}
await deleteCloudtentaclesSessionStateByKey(key)
await deleteCloudtentaclesSourceByKey(key)
}
export async function sendAdminCloudtentaclesSmsCode(
payload: AdminCloudtentaclesSendSmsCodeInput = {},
) {
const result = await sendCloudtentaclesSmsCode(
resolveAdminCloudtentaclesCredentialPayload(payload),
)
return {
...result,
phoneMasked: maskPhone(result.phone),
}
}
export async function testAdminCloudtentaclesLogin(
payload: AdminCloudtentaclesTestLoginInput = {},
) {
const sourceKey = String(payload.sourceKey || '').trim() || 'default'
const savedSource = getCloudtentaclesSourceByKey(sourceKey) || {}
const credentialContext = resolveAdminCloudtentaclesCredentialPayload(payload, { savedSource })
const session = await loginCloudtentaclesSession({
...credentialContext,
code: String(payload.code || '').trim(),
})
const savedSession = await saveCloudtentaclesSessionStateByKey(
sourceKey,
buildAdminCloudtentaclesPersistedSessionPayload(session, {
deviceId: credentialContext.deviceId,
deviceType: credentialContext.deviceType,
}),
)
return buildAdminCloudtentaclesLoginResult(session, savedSession, sourceKey)
}
export async function validateAdminCloudtentaclesSession(
payload: AdminCloudtentaclesValidateSessionInput = {},
) {
const sourceKey = String(payload.sourceKey || '').trim() || 'default'
const savedSource: JsonObject = getCloudtentaclesSourceByKey(sourceKey) || {}
const persistedSession: JsonObject = getCloudtentaclesSessionStateByKey(sourceKey) || {}
const sessionContext = resolveAdminCloudtentaclesSessionPayload(payload, {
savedSource,
persistedSession,
})
const session = await validateCloudtentaclesSession(sessionContext)
const savedSession = await saveCloudtentaclesSessionStateByKey(
sourceKey,
buildAdminCloudtentaclesPersistedSessionPayload(session, {
username: pickFirstNonEmpty([persistedSession.username, savedSource.username]),
phone: pickFirstNonEmpty([persistedSession.phone, savedSource.phone]),
deviceId: sessionContext.deviceId,
deviceType: sessionContext.deviceType,
}),
)
return buildAdminCloudtentaclesValidateResult(session, savedSession, sourceKey)
}
export async function getAdminCloudtentaclesAsset(
payload: AdminCloudtentaclesCatalogQueryInput = {},
) {
return getCloudtentaclesAsset(resolveAdminCloudtentaclesSessionPayload(payload))
}
export async function getAdminCloudtentaclesCategories(
payload: AdminCloudtentaclesCatalogQueryInput = {},
) {
return getCloudtentaclesCategories(resolveAdminCloudtentaclesSessionPayload(payload))
}
export async function getAdminCloudtentaclesSkuList(
payload: AdminCloudtentaclesCatalogQueryInput = {},
) {
return listCloudtentaclesSku(resolveAdminCloudtentaclesSessionPayload(payload))
}
export async function buyAdminCloudtentaclesSku(payload: AdminCloudtentaclesSkuBuyInput = {}) {
const context = resolveAdminCloudtentaclesSessionPayload(payload)
return buyCloudtentaclesSku({
...context,
id: payload.id,
count: payload.count,
})
}
export async function useAdminCloudtentaclesSku(payload: AdminCloudtentaclesSkuUseInput = {}) {
const context = resolveAdminCloudtentaclesSessionPayload(payload)
return useCloudtentaclesSku({
...context,
id: payload.id,
virtualNumberId: payload.virtualNumberId,
phone: payload.phone,
})
}
export async function getAdminCloudtentaclesKnapsack(
payload: AdminCloudtentaclesCatalogQueryInput = {},
) {
return getCloudtentaclesKnapsack(resolveAdminCloudtentaclesSessionPayload(payload))
}
export async function listAdminCloudtentaclesDeliveryRecords(
payload: AdminCloudtentaclesDeliveryRecordQueryInput = {},
) {
const platformOrderId = normalizeText(payload.platformOrderId)
const preferredSourceKey = normalizeText(payload.sourceKey)
const orderMatchedTasks = platformOrderId
? await listLocalCloudtentaclesTasks({
platformOrderId,
limit: 50,
})
: []
const orderSourceKey =
orderMatchedTasks.find((task) => !preferredSourceKey || task.sourceKey === preferredSourceKey)
?.sourceKey || orderMatchedTasks.find((task) => task.sourceKey)?.sourceKey
const resolvedSourceKey = orderSourceKey || preferredSourceKey || 'default'
const context = resolveAdminCloudtentaclesSessionPayload({
...payload,
sourceKey: resolvedSourceKey,
})
const recordWindow = resolveRecordQueryWindow(payload, orderMatchedTasks)
const recordPage = platformOrderId ? 1 : payload.page
const recordSize = platformOrderId ? 1000 : payload.size
const query = {
...context,
...pickDefined({
page: recordPage,
size: recordSize,
startDate: recordWindow.startDate,
endDate: recordWindow.endDate,
recordCode: payload.recordCode,
}),
}
const [records, skuList] = await Promise.all([
listCloudtentaclesDeliveryRecords(query),
listCloudtentaclesSku(context),
])
const skuImageByName = new Map<
string,
{
skuId: number
skuName: string
skuImage: string
}
>()
for (const sku of Array.isArray(skuList.items) ? skuList.items : []) {
const name = normalizeSkuName(sku.name)
const skuImage = String(sku.image || '').trim()
if (!name || !skuImage) {
continue
}
skuImageByName.set(name, {
skuId: Number(sku.id || 0),
skuName: String(sku.name || '').trim(),
skuImage,
})
}
const recordItems: JsonObject[] = Array.isArray(records.items) ? records.items : []
const localTasks =
platformOrderId || recordItems.length === 0
? orderMatchedTasks
: await listLocalCloudtentaclesTasks({
sourceKey: resolvedSourceKey,
startDate: recordWindow.startDate,
endDate: recordWindow.endDate,
limit: 800,
})
const enrichedItems = recordItems
.map((record) => {
const localMatch = platformOrderId
? findStrictLocalTaskForOrderRecord(record, localTasks)
: findBestLocalTaskForRecord(record, localTasks)
return {
...record,
...(skuImageByName.get(normalizeSkuName(record.name)) || {
skuId: 0,
skuName: '',
skuImage: '',
}),
...mapLocalTaskRecordEnhancement(localMatch?.task || null, localMatch?.confidence || ''),
}
})
.filter((record) => {
if (!platformOrderId) {
return true
}
return normalizeText(record.platformOrderId) === platformOrderId
})
const fallbackItems = platformOrderId
? orderMatchedTasks
.filter(
(task) => !enrichedItems.some((record) => Number(record.taskId || 0) === task.taskId),
)
.map((task) => ({
...createLocalTaskFallbackRecord(task),
...(skuImageByName.get(normalizeSkuName(task.skuName)) || {
skuId: 0,
skuName: '',
skuImage: '',
}),
...mapLocalTaskRecordEnhancement(task, 'local'),
}))
: []
const finalItems = platformOrderId ? [...enrichedItems, ...fallbackItems] : enrichedItems
return {
...records,
sourceKey: resolvedSourceKey,
queryStartDate: recordWindow.startDate,
queryEndDate: recordWindow.endDate,
total: platformOrderId ? finalItems.length : records.total,
page: platformOrderId ? 1 : records.page,
size: platformOrderId ? recordSize : records.size,
localOrderMatched: platformOrderId ? orderMatchedTasks.length > 0 : false,
items: finalItems,
}
}
export async function listAdminCloudtentaclesVirtualNumbers(
payload: AdminCloudtentaclesVirtualNumberInput = {},
) {
const context = resolveAdminCloudtentaclesSessionPayload(payload)
return listCloudtentaclesVirtualNumbers({
...context,
key: payload.key,
})
}
export async function appointAdminCloudtentaclesVirtualNumber(
payload: AdminCloudtentaclesVirtualNumberInput = {},
) {
const context = resolveAdminCloudtentaclesSessionPayload(payload)
return appointCloudtentaclesVirtualNumber({
...context,
key: payload.key,
})
}
export async function generateAdminCloudtentaclesLoginCode(
payload: AdminCloudtentaclesVirtualNumberInput = {},
) {
const context = resolveAdminCloudtentaclesSessionPayload(payload)
return generateCloudtentaclesLoginCode({
...context,
key: payload.key,
id: payload.id,
})
}
export async function fetchAdminCloudtentaclesVirtualNumberCode(
payload: AdminCloudtentaclesVirtualNumberInput = {},
) {
const context = resolveAdminCloudtentaclesSessionPayload(payload)
return fetchCloudtentaclesVirtualNumberCode({
...context,
key: payload.key,
phone: payload.phone,
})
}
export async function verifyAdminCloudtentaclesLoginCode(
payload: AdminCloudtentaclesVirtualNumberInput = {},
) {
const context = resolveAdminCloudtentaclesSessionPayload(payload)
return verifyCloudtentaclesLoginCode({
...context,
key: payload.key,
id: payload.id,
code: payload.code,
})
}
export async function getAdminCloudtentaclesBindUrl(
payload: AdminCloudtentaclesVirtualNumberInput = {},
) {
const context = resolveAdminCloudtentaclesSessionPayload(payload)
return getCloudtentaclesBindUrl({
...context,
key: payload.key,
id: payload.id,
})
}
export async function backAdminCloudtentaclesVirtualNumber(
payload: AdminCloudtentaclesVirtualNumberInput = {},
) {
const context = resolveAdminCloudtentaclesSessionPayload(payload)
return backCloudtentaclesVirtualNumber({
...context,
key: payload.key,
id: payload.id,
})
}
export async function runAdminCloudtentaclesFullFlow(
payload: AdminCloudtentaclesFullFlowInput = {},
) {
const context = resolveAdminCloudtentaclesSessionPayload(payload)
return runCloudtentaclesFullDebugFlow({
...context,
skuId: payload.skuId,
skuCount: payload.skuCount,
vnKey: payload.vnKey,
})
}
function pickDefined(values: JsonObject = {}) {
return Object.fromEntries(
Object.entries(values).filter(([, value]) => typeof value !== 'undefined'),
)
}
function normalizeSkuName(value: unknown) {
return String(value || '')
.trim()
.replace(/\s+/g, '')
.toLowerCase()
}
function normalizeText(value: unknown) {
return String(value || '').trim()
}
function normalizePhone(value: unknown) {
return normalizeText(value).replace(/\D+/g, '')
}
function normalizeNumber(value: unknown) {
const parsed = Number(value)
return Number.isFinite(parsed) && parsed > 0 ? parsed : 0
}
function asRecord(value: unknown): JsonObject {
return value && typeof value === 'object' && !Array.isArray(value) ? (value as JsonObject) : {}
}
function pickFirstNonEmptyText(values: unknown[]) {
for (const value of values) {
const normalized = normalizeText(value)
if (normalized) {
return normalized
}
}
return ''
}
function resolveRecordQueryWindow(
payload: AdminCloudtentaclesDeliveryRecordQueryInput = {},
tasks: LocalCloudtentaclesTask[] = [],
) {
if (tasks.length === 0) {
return {
startDate: payload.startDate,
endDate: payload.endDate,
}
}
const timestamps = tasks
.flatMap((task) => [task.createdAt, task.updatedAt, task.localDispatchAt])
.map((value) => new Date(value).getTime())
.filter((value) => Number.isFinite(value))
if (timestamps.length === 0) {
return {
startDate: payload.startDate,
endDate: payload.endDate,
}
}
const hourMs = 60 * 60 * 1000
return {
startDate: new Date(Math.min(...timestamps) - hourMs).toISOString(),
endDate: new Date(Math.max(...timestamps) + 6 * hourMs).toISOString(),
}
}
async function listLocalCloudtentaclesTasks(
options: {
platformOrderId?: string | undefined
sourceKey?: string | undefined
startDate?: string | undefined
endDate?: string | undefined
limit?: number
} = {},
): Promise<LocalCloudtentaclesTask[]> {
const filters: string[] = ["ft.executor_key = 'kuaishou_ct_assisted'"]
const params: unknown[] = []
const platformOrderId = normalizeText(options.platformOrderId)
const sourceKey = normalizeText(options.sourceKey)
const startDate = normalizeText(options.startDate)
const endDate = normalizeText(options.endDate)
if (platformOrderId) {
params.push(`%${platformOrderId}%`)
filters.push(`ft.platform_order_id ILIKE $${params.length}`)
}
if (sourceKey) {
params.push(sourceKey)
filters.push(
`COALESCE(NULLIF(kcts.source_key, ''), ft.context_json #>> '{kuaishouCloudFulfillment,binding,resolvedSourceKey}', '') = $${params.length}`,
)
}
if (startDate) {
params.push(startDate)
filters.push(`ft.updated_at >= ($${params.length})::timestamptz - interval '1 day'`)
}
if (endDate) {
params.push(endDate)
filters.push(`ft.created_at <= ($${params.length})::timestamptz + interval '1 day'`)
}
params.push(Math.min(Math.max(Number(options.limit || 500), 1), 1000))
const result = await dbQuery<LocalCloudtentaclesTaskRow>(
`
SELECT
ft.id,
ft.task_no,
ft.platform_order_id,
ft.shop_id,
ft.shop_name,
ft.task_status,
ft.delivery_status,
ft.result_code,
ft.result_message,
ft.context_json,
ft.created_at,
ft.updated_at,
oi.sku_code,
oi.sku_name,
kcts.source_key AS kuaishou_source_key,
kcts.vn_phone AS kuaishou_vn_phone,
kcts.role_name AS kuaishou_role_name,
kcts.role_id AS kuaishou_role_id,
kcts.dispatch_status AS kuaishou_dispatch_status,
kcts.consume_status AS kuaishou_consume_status
FROM fulfillment_tasks ft
LEFT JOIN order_items oi ON oi.id = ft.order_item_id
LEFT JOIN kuaishou_cloud_task_states kcts ON kcts.task_id = ft.id
WHERE ${filters.join(' AND ')}
ORDER BY ft.id DESC
LIMIT $${params.length}
`,
params,
)
return result.rows.map(mapLocalCloudtentaclesTask)
}
function mapLocalCloudtentaclesTask(row: LocalCloudtentaclesTaskRow): LocalCloudtentaclesTask {
const context = parseJsonObject(row.context_json)
const flow = asRecord(context.kuaishouCloudFulfillment)
const binding = asRecord(flow.binding)
const role = asRecord(flow.role)
const dispatch = asRecord(flow.dispatch)
const consume = asRecord(flow.consume)
return {
taskId: Number(row.id || 0),
taskNo: normalizeText(row.task_no),
platformOrderId: normalizeText(row.platform_order_id),
orderShopId: normalizeText(row.shop_id),
orderShopName: normalizeText(row.shop_name),
taskStatus: normalizeText(row.task_status),
deliveryStatus: normalizeText(row.delivery_status),
resultCode: normalizeText(row.result_code),
resultMessage: normalizeText(row.result_message),
createdAt: normalizeText(row.created_at),
updatedAt: normalizeText(row.updated_at),
skuCode: normalizeText(row.sku_code),
skuName: pickFirstNonEmptyText([row.sku_name, flow.internalSkuName]),
sourceKey: pickFirstNonEmptyText([row.kuaishou_source_key, binding.resolvedSourceKey]),
vnId: normalizeNumber(binding.vnId),
vnPhone: pickFirstNonEmptyText([row.kuaishou_vn_phone, binding.vnPhone]),
roleName: pickFirstNonEmptyText([row.kuaishou_role_name, role.name, binding.roleName]),
roleId: pickFirstNonEmptyText([row.kuaishou_role_id, role.rid, binding.roleId]),
localDispatchStatus: pickFirstNonEmptyText([row.kuaishou_dispatch_status, dispatch.status]),
localDispatchAt: normalizeText(dispatch.dispatchAt),
consumeShopId: normalizeText(consume.shopId),
consumeShopName: normalizeText(consume.shopName),
}
}
function findBestLocalTaskForRecord(record: JsonObject, tasks: LocalCloudtentaclesTask[] = []) {
let best: { task: LocalCloudtentaclesTask; score: number; confidence: string } | null = null
for (const task of tasks) {
const score = scoreLocalTaskRecordMatch(record, task)
if (score <= 0 || (best && score <= best.score)) {
continue
}
best = {
task,
score,
confidence: score >= 80 ? 'high' : score >= 45 ? 'medium' : 'low',
}
}
return best && best.score >= 35 ? best : null
}
function findStrictLocalTaskForOrderRecord(
record: JsonObject,
tasks: LocalCloudtentaclesTask[] = [],
) {
let best: { task: LocalCloudtentaclesTask; score: number; confidence: string } | null = null
for (const task of tasks) {
if (!isSameDispatchedResource(record, task)) {
continue
}
const score = scoreLocalTaskRecordMatch(record, task)
if (score <= 0 || (best && score <= best.score)) {
continue
}
best = {
task,
score,
confidence: score >= 80 ? 'high' : 'medium',
}
}
return best
}
function isSameDispatchedResource(record: JsonObject, task: LocalCloudtentaclesTask) {
const recordVnId = normalizeNumber(record.virtualNumberId)
const recordPhone = normalizePhone(record.phone)
const taskPhone = normalizePhone(task.vnPhone)
if (recordVnId > 0 && task.vnId > 0 && recordVnId === task.vnId) {
return true
}
return Boolean(recordPhone && taskPhone && recordPhone === taskPhone)
}
function scoreLocalTaskRecordMatch(record: JsonObject, task: LocalCloudtentaclesTask) {
let score = 0
const recordVnId = normalizeNumber(record.virtualNumberId)
const recordPhone = normalizePhone(record.phone)
const taskPhone = normalizePhone(task.vnPhone)
const recordName = normalizeSkuName(record.name)
const taskSkuName = normalizeSkuName(task.skuName)
const recordRoleName = normalizeSkuName(record.fulfillUser)
const taskRoleName = normalizeSkuName(task.roleName)
if (recordVnId > 0 && task.vnId > 0 && recordVnId === task.vnId) {
score += 50
}
if (recordPhone && taskPhone && recordPhone === taskPhone) {
score += 30
}
if (recordName && taskSkuName && recordName === taskSkuName) {
score += 20
}
if (recordRoleName && taskRoleName && recordRoleName.includes(taskRoleName)) {
score += 10
}
const recordTime = new Date(record.createdAt).getTime()
const dispatchTime = new Date(task.localDispatchAt || task.updatedAt || task.createdAt).getTime()
if (Number.isFinite(recordTime) && Number.isFinite(dispatchTime)) {
const distanceHours = Math.abs(recordTime - dispatchTime) / (60 * 60 * 1000)
if (distanceHours <= 6) {
score += 15
} else if (distanceHours <= 24) {
score += 6
}
}
return score
}
function mapLocalTaskRecordEnhancement(task: LocalCloudtentaclesTask | null, confidence = '') {
return {
platformOrderId: task?.platformOrderId || '',
taskId: task?.taskId || 0,
taskNo: task?.taskNo || '',
orderShopId: task?.orderShopId || '',
orderShopName: task?.orderShopName || '',
consumeShopId: task?.consumeShopId || '',
consumeShopName: task?.consumeShopName || '',
roleName: task?.roleName || '',
roleId: task?.roleId || '',
localTaskStatus: task?.taskStatus || '',
localDeliveryStatus: task?.deliveryStatus || '',
localDispatchStatus: task?.localDispatchStatus || '',
localDispatchAt: task?.localDispatchAt || '',
localResultCode: task?.resultCode || '',
localResultMessage: task?.resultMessage || '',
matchConfidence: confidence,
}
}
function createLocalTaskFallbackRecord(task: LocalCloudtentaclesTask) {
return {
createdAt: task.localDispatchAt || task.updatedAt || task.createdAt,
recordId: '',
virtualNumberId: task.vnId,
userId: 0,
count: 1,
cdk: '',
exchangeUrl: '',
name: task.skuName,
phone: task.vnPhone,
status: mapLocalDispatchStatusToCloudStatus(task.localDispatchStatus),
fulfillUser: task.roleName,
operatorName: '',
raw: {},
}
}
function mapLocalDispatchStatusToCloudStatus(status: string) {
const normalized = normalizeText(status)
if (normalized === 'success') {
return 4
}
if (normalized === 'failed') {
return 3
}
if (normalized === 'pending') {
return 2
}
return 0
}
@@ -1,41 +0,0 @@
import test from 'node:test'
import assert from 'node:assert/strict'
import { mapAdminCloudtentaclesSession, maskPhone, maskSecret } from './mappers.js'
test('maskSecret preserves edges while hiding middle characters', () => {
assert.equal(maskSecret('abcdef1234567890'), 'abcdef****567890')
assert.equal(maskSecret('12345678'), '12****78')
assert.equal(maskSecret(''), '')
})
test('maskPhone hides middle digits for mobile numbers', () => {
assert.equal(maskPhone('13812345678'), '138****5678')
assert.equal(maskPhone('123456'), '123456')
assert.equal(maskPhone(''), '')
})
test('mapAdminCloudtentaclesSession derives masked fields and token presence', () => {
assert.deepEqual(
mapAdminCloudtentaclesSession({
token: 'abcdef1234567890',
baseUrl: ' https://123.207.217.176 ',
username: ' admin ',
phone: '13812345678',
loggedInAt: ' 2026-05-04T10:00:00.000Z ',
deviceId: ' dev-1 ',
deviceType: '2',
}),
{
token: 'abcdef1234567890',
tokenMasked: 'abcdef****567890',
baseUrl: 'https://123.207.217.176',
username: 'admin',
phoneMasked: '138****5678',
loggedInAt: '2026-05-04T10:00:00.000Z',
deviceId: 'dev-1',
deviceType: 2,
hasToken: true,
},
)
})
@@ -1,45 +0,0 @@
import type { JsonObject } from '../../../../types/json.js'
import {
maskPhone as maskPhoneValue,
maskSecret as maskSecretValue,
} from '../../../../utils/masking.js'
import {
normalizeCloudtentaclesDeviceId,
normalizeCloudtentaclesDeviceType,
} from '../../../platforms/cloudtentacles/defaults.js'
export function maskSecret(value: unknown) {
return maskSecretValue(value)
}
export function maskPhone(value: unknown) {
return maskPhoneValue(value, { maskShort: false })
}
export function mapAdminCloudtentaclesSourceConfig(config: JsonObject = {}) {
return {
key: String(config.key || '').trim(),
label: String(config.label || '').trim(),
enabled: config.enabled !== false,
baseUrl: String(config.baseUrl || '').trim(),
username: String(config.username || '').trim(),
password: String(config.password || '').trim(),
phone: String(config.phone || '').trim(),
deviceId: normalizeCloudtentaclesDeviceId(config.deviceId),
deviceType: normalizeCloudtentaclesDeviceType(config.deviceType),
}
}
export function mapAdminCloudtentaclesSession(session: JsonObject = {}) {
return {
token: String(session.token || '').trim(),
tokenMasked: maskSecret(session.token),
baseUrl: String(session.baseUrl || '').trim(),
username: String(session.username || '').trim(),
phoneMasked: maskPhone(session.phone),
loggedInAt: String(session.loggedInAt || '').trim(),
deviceId: normalizeCloudtentaclesDeviceId(session.deviceId),
deviceType: normalizeCloudtentaclesDeviceType(session.deviceType),
hasToken: Boolean(String(session.token || '').trim()),
}
}
@@ -1,212 +0,0 @@
import test from 'node:test'
import assert from 'node:assert/strict'
import {
buildAdminCloudtentaclesLoginResult,
buildAdminCloudtentaclesPersistedSessionPayload,
buildAdminCloudtentaclesSessionSummary,
buildAdminCloudtentaclesValidateResult,
resolveAdminCloudtentaclesCredentialPayload,
resolveAdminCloudtentaclesSessionPayload,
} from './payloads.js'
test('resolveAdminCloudtentaclesCredentialPayload merges payload and saved source defaults', () => {
assert.deepEqual(
resolveAdminCloudtentaclesCredentialPayload(
{
baseUrl: ' https://override.example.com ',
phone: ' 13800138000 ',
deviceId: ' custom-device ',
},
{
savedSource: {
baseUrl: 'https://saved.example.com',
username: 'saved-user',
password: 'saved-pass',
phone: '13900000000',
deviceId: 'saved-device',
deviceType: 2,
},
},
),
{
sourceKey: 'default',
baseUrl: 'https://override.example.com',
username: 'saved-user',
password: 'saved-pass',
phone: '13800138000',
deviceId: 'custom-device',
deviceType: 2,
},
)
})
test('resolveAdminCloudtentaclesSessionPayload merges payload source and persisted session defaults', () => {
assert.deepEqual(
resolveAdminCloudtentaclesSessionPayload(
{
token: ' payload-token ',
},
{
savedSource: {
baseUrl: 'https://saved.example.com',
deviceId: 'saved-device',
deviceType: 3,
},
persistedSession: {
token: 'session-token',
deviceId: 'session-device',
deviceType: 5,
},
},
),
{
sourceKey: 'default',
baseUrl: 'https://saved.example.com',
token: 'payload-token',
deviceId: 'saved-device',
deviceType: 3,
},
)
})
test('buildAdminCloudtentaclesPersistedSessionPayload prefers explicit overrides', () => {
assert.deepEqual(
buildAdminCloudtentaclesPersistedSessionPayload(
{
token: ' token-1 ',
baseUrl: ' https://cloud.example.com ',
username: ' session-user ',
phone: ' 13900000000 ',
loggedInAt: ' 2026-05-04T12:00:00.000Z ',
deviceId: ' session-device ',
deviceType: 9,
},
{
username: 'saved-user',
phone: '13800138000',
deviceId: 'saved-device',
deviceType: 3,
},
),
{
token: 'token-1',
baseUrl: 'https://cloud.example.com',
username: 'saved-user',
phone: '13800138000',
loggedInAt: '2026-05-04T12:00:00.000Z',
deviceId: 'saved-device',
deviceType: 3,
},
)
})
test('buildAdminCloudtentaclesSessionSummary masks token and reports permission count', () => {
assert.deepEqual(
buildAdminCloudtentaclesSessionSummary(
{
token: 'abcdef1234567890',
permissions: ['asset', 'sku'],
},
{
token: 'persisted-token',
},
),
{
sourceKey: 'default',
tokenMasked: 'abcdef****567890',
permissionCount: 2,
permissions: ['asset', 'sku'],
persisted: true,
},
)
})
test('buildAdminCloudtentaclesLoginResult assembles login response payload', () => {
assert.deepEqual(
buildAdminCloudtentaclesLoginResult(
{
token: 'abcdef1234567890',
baseUrl: ' https://cloud.example.com ',
username: ' test-user ',
phone: '13812345678',
loggedInAt: ' 2026-05-04T12:00:00.000Z ',
responseMessage: ' success ',
permissions: ['asset'],
userInfo: { id: 1 },
asset: { balance: 10 },
},
{
token: 'persisted-token',
},
),
{
sourceKey: 'default',
baseUrl: 'https://cloud.example.com',
username: 'test-user',
phoneMasked: '138****5678',
loggedInAt: '2026-05-04T12:00:00.000Z',
responseMessage: 'success',
token: 'abcdef1234567890',
session: {
sourceKey: 'default',
tokenMasked: 'abcdef****567890',
permissionCount: 1,
permissions: ['asset'],
persisted: true,
},
userInfo: { id: 1 },
asset: { balance: 10 },
},
)
})
test('buildAdminCloudtentaclesValidateResult assembles validate response payload', () => {
assert.deepEqual(
buildAdminCloudtentaclesValidateResult(
{
token: 'abcdef1234567890',
baseUrl: ' https://cloud.example.com ',
loggedInAt: ' 2026-05-04T12:00:00.000Z ',
permissions: ['asset', 'sku'],
userInfo: { id: 2 },
asset: { balance: 20 },
},
{},
),
{
sourceKey: 'default',
baseUrl: 'https://cloud.example.com',
loggedInAt: '2026-05-04T12:00:00.000Z',
session: {
sourceKey: 'default',
tokenMasked: 'abcdef****567890',
permissionCount: 2,
permissions: ['asset', 'sku'],
persisted: false,
},
userInfo: { id: 2 },
asset: { balance: 20 },
},
)
})
test('buildAdminCloudtentaclesSessionSummary preserves sourceKey', () => {
assert.deepEqual(
buildAdminCloudtentaclesSessionSummary(
{
token: 'abcdef1234567890',
permissions: [],
},
{},
'account2',
),
{
sourceKey: 'account2',
tokenMasked: 'abcdef****567890',
permissionCount: 0,
permissions: [],
persisted: false,
},
)
})
@@ -1,120 +0,0 @@
import { getCloudtentaclesSourceByKey } from '../../../platforms/cloudtentacles/source-config-service.js'
import { getCloudtentaclesSessionStateByKey } from '../../../platforms/cloudtentacles/session-state-service.js'
import type { JsonObject } from '../../../../types/json.js'
import {
normalizeCloudtentaclesDeviceId,
normalizeCloudtentaclesDeviceType,
} from '../../../platforms/cloudtentacles/defaults.js'
import { pickFirstNonEmpty, resolveCloudtentaclesAdminContext } from './context.js'
import { maskPhone, maskSecret } from './mappers.js'
const DEFAULT_CLOUDTENTACLES_BASE_URL = 'https://123.207.217.176'
function _resolveSourceKey(payload: JsonObject = {}) {
return String(payload.sourceKey || '').trim() || 'default'
}
export function resolveAdminCloudtentaclesCredentialPayload(
payload: JsonObject = {},
options: JsonObject = {},
) {
const sourceKey = _resolveSourceKey(payload)
const savedSource = options.savedSource || getCloudtentaclesSourceByKey(sourceKey) || {}
const defaultBaseUrl = String(options.defaultBaseUrl || DEFAULT_CLOUDTENTACLES_BASE_URL).trim()
return {
sourceKey,
baseUrl: pickFirstNonEmpty([payload.baseUrl, savedSource.baseUrl, defaultBaseUrl]),
username: pickFirstNonEmpty([payload.username, savedSource.username]),
password: pickFirstNonEmpty([payload.password, savedSource.password]),
phone: pickFirstNonEmpty([payload.phone, savedSource.phone]),
deviceId: normalizeCloudtentaclesDeviceId(
pickFirstNonEmpty([payload.deviceId, savedSource.deviceId]),
),
deviceType: normalizeCloudtentaclesDeviceType(payload.deviceType ?? savedSource.deviceType),
}
}
export function resolveAdminCloudtentaclesSessionPayload(
payload: JsonObject = {},
options: JsonObject = {},
) {
const sourceKey = _resolveSourceKey(payload)
const savedSource = options.savedSource || getCloudtentaclesSourceByKey(sourceKey) || {}
const persistedSession =
options.persistedSession || getCloudtentaclesSessionStateByKey(sourceKey) || {}
return resolveCloudtentaclesAdminContext(payload, {
savedSource,
persistedSession,
sourceKey,
defaultBaseUrl: options.defaultBaseUrl || DEFAULT_CLOUDTENTACLES_BASE_URL,
})
}
export function buildAdminCloudtentaclesPersistedSessionPayload(
session: JsonObject = {},
options: JsonObject = {},
) {
return {
token: String(session.token || '').trim(),
baseUrl: String(session.baseUrl || '').trim(),
username: pickFirstNonEmpty([options.username, session.username]),
phone: pickFirstNonEmpty([options.phone, session.phone]),
loggedInAt: String(session.loggedInAt || '').trim(),
deviceId: normalizeCloudtentaclesDeviceId(
pickFirstNonEmpty([options.deviceId, session.deviceId]),
),
deviceType: normalizeCloudtentaclesDeviceType(options.deviceType ?? session.deviceType),
}
}
export function buildAdminCloudtentaclesSessionSummary(
session: JsonObject = {},
savedSession: JsonObject = {},
sourceKey = 'default',
) {
const permissions = Array.isArray(session.permissions) ? session.permissions : []
return {
sourceKey,
tokenMasked: maskSecret(session.token),
permissionCount: permissions.length,
permissions,
persisted: Boolean(savedSession.token),
}
}
export function buildAdminCloudtentaclesLoginResult(
session: JsonObject = {},
savedSession: JsonObject = {},
sourceKey = 'default',
) {
return {
sourceKey,
baseUrl: String(session.baseUrl || '').trim(),
username: String(session.username || '').trim(),
phoneMasked: maskPhone(session.phone),
loggedInAt: String(session.loggedInAt || '').trim(),
responseMessage: String(session.responseMessage || '').trim(),
token: String(session.token || '').trim(),
session: buildAdminCloudtentaclesSessionSummary(session, savedSession, sourceKey),
userInfo: session.userInfo,
asset: session.asset,
}
}
export function buildAdminCloudtentaclesValidateResult(
session: JsonObject = {},
savedSession: JsonObject = {},
sourceKey = 'default',
) {
return {
sourceKey,
baseUrl: String(session.baseUrl || '').trim(),
loggedInAt: String(session.loggedInAt || '').trim(),
session: buildAdminCloudtentaclesSessionSummary(session, savedSession, sourceKey),
userInfo: session.userInfo,
asset: session.asset,
}
}
@@ -1,20 +0,0 @@
import type { JsonObject } from '../../../../types/json.js'
import {
normalizeCloudtentaclesDeviceId,
normalizeCloudtentaclesDeviceType,
} from '../../../platforms/cloudtentacles/defaults.js'
export function normalizeAdminCloudtentaclesSourceConfigPayload(payload: JsonObject = {}) {
const sourceKey = String(payload.sourceKey || payload.key || '').trim() || 'default'
return {
key: sourceKey,
label: String(payload.label || '').trim(),
enabled: payload.enabled !== false,
baseUrl: String(payload.baseUrl || '').trim() || 'https://123.207.217.176',
username: String(payload.username || '').trim(),
password: String(payload.password || '').trim(),
phone: String(payload.phone || '').trim(),
deviceId: normalizeCloudtentaclesDeviceId(payload.deviceId),
deviceType: normalizeCloudtentaclesDeviceType(payload.deviceType),
}
}
@@ -4,7 +4,7 @@ import {
saveFulfillmentRoutingConfig,
} from '../../fulfillment/routing-config-service.js'
import { resolveOrderItemForFulfillment } from '../../fulfillment/product-resolution-service.js'
import { normalizeCloudtentaclesMatchName } from '../../order/cloudtentacles-match-utils.js'
import { normalizeProductMatchName } from '../../order/product-match-utils.js'
import type { JsonRecord } from '../../../types/json.js'
export function getAdminFulfillmentRoutingConfig() {
@@ -40,12 +40,11 @@ export async function previewAdminFulfillmentRouting(payload: JsonRecord = {}) {
return {
productName,
normalizedProductName: normalizeCloudtentaclesMatchName(productName),
normalizedProductName: normalizeProductMatchName(productName),
resolvedSkuCode: item.skuCode,
resolvedSkuName: item.skuName,
isConfigured: item.isConfigured,
fulfillmentRoute: snapshot.fulfillmentRoute || null,
cloudtentacles: snapshot.cloudtentacles || null,
kuaishouFeifei: snapshot.kuaishouFeifei || null,
affiliateDash: snapshot.affiliateDash || null,
}
}
@@ -1,224 +0,0 @@
import { getKuaishouFeifeiConfig } from '../../platforms/kuaishou-feifei/config.js'
import type { JsonObject } from '../../../types/json.js'
import {
createKuaishouFeifeiOrder,
listKuaishouFeifeiProducts,
queryKuaishouFeifeiOrder,
type KuaishouFeifeiProductListResult,
} from '../../platforms/kuaishou-feifei/order-service.js'
import { resolveKuaishouFeifeiProductByName } from '../../platforms/kuaishou-feifei/product-rule-service.js'
import {
getKuaishouFeifeiConfigFilePath,
getKuaishouFeifeiSourceConfig,
hasKuaishouFeifeiConfigFile,
normalizeKuaishouFeifeiSourceConfig,
saveKuaishouFeifeiSourceConfig,
} from '../../platforms/kuaishou-feifei/source-config-service.js'
import { normalizeCloudtentaclesMatchName } from '../../order/cloudtentacles-match-utils.js'
import { createHttpError } from '../../../utils/http.js'
export function getAdminKuaishouFeifeiConfig() {
const source = getAdminEditableKuaishouFeifeiConfig()
const effective = getKuaishouFeifeiConfig()
return {
filePath: getKuaishouFeifeiConfigFilePath(),
source,
effective: mapEffectiveKuaishouFeifeiConfig(effective),
}
}
export async function updateAdminKuaishouFeifeiConfig(payload: JsonObject = {}) {
const saved = await saveKuaishouFeifeiSourceConfig(payload)
const effective = getKuaishouFeifeiConfig()
return {
filePath: getKuaishouFeifeiConfigFilePath(),
source: saved,
effective: mapEffectiveKuaishouFeifeiConfig(effective),
}
}
export function matchAdminKuaishouFeifeiProduct(payload: JsonObject = {}) {
const productName = String(payload.productName || '').trim()
const source = getAdminEditableKuaishouFeifeiConfig()
const match = resolveKuaishouFeifeiProductByName(productName, {
enabled: source.enabled,
rules: source.productRules,
})
return {
productName,
normalizedProductName: normalizeCloudtentaclesMatchName(productName),
matched: Boolean(match),
match,
}
}
export async function listAdminKuaishouFeifeiProducts(payload: JsonObject = {}) {
return listKuaishouFeifeiProducts({
page: Number(payload.page || 1) || 1,
perPage: Number(payload.perPage || payload.per_page || 20) || 20,
status: String(payload.status || 'on_sale').trim(),
supplyProductName: String(
payload.supplyProductName || payload.supply_product_name || '',
).trim(),
})
}
export async function syncAdminKuaishouFeifeiProductRules(payload: JsonObject = {}) {
const source = getAdminEditableKuaishouFeifeiConfig()
const status = String(payload.status || 'on_sale').trim() || 'on_sale'
const supplyProductName = String(
payload.supplyProductName || payload.supply_product_name || '',
).trim()
const products = await listAllKuaishouFeifeiProducts({
status,
supplyProductName,
})
const productRules = products
.map((product) => {
const productName = String(product.name || '').trim()
const productCode = String(product.productCode || '').trim()
if (!productName || !productCode) {
return null
}
return {
id: normalizeCloudtentaclesMatchName(productName) || productCode,
enabled: product.status !== 'off_sale' && product.supplyStatus !== 'off_sale',
productName,
normalizedProductName: normalizeCloudtentaclesMatchName(productName),
productCode,
skuName: productName,
notes: `feifei 商品目录同步:${product.channel || 'unknown'}`,
}
})
.filter((rule): rule is NonNullable<typeof rule> => Boolean(rule))
const saved = await saveKuaishouFeifeiSourceConfig({
...source,
productRules,
})
const effective = getKuaishouFeifeiConfig()
return {
filePath: getKuaishouFeifeiConfigFilePath(),
source: saved,
effective: mapEffectiveKuaishouFeifeiConfig(effective),
sync: {
productCount: products.length,
ruleCount: saved.productRules.length,
status,
supplyProductName,
},
}
}
export async function createAdminKuaishouFeifeiTestOrder(payload: JsonObject = {}) {
const platformOrderNo = String(payload.platformOrderNo || payload.platform_order_no || '').trim()
const productCode = String(payload.productCode || payload.product_code || '').trim()
if (!platformOrderNo) {
throw createHttpError('测试业务单号不能为空', {
statusCode: 400,
errorCode: 'kuaishou_feifei_missing_platform_order_no',
})
}
if (!productCode) {
throw createHttpError('feifei 商品编码不能为空', {
statusCode: 400,
errorCode: 'kuaishou_feifei_missing_product_code',
})
}
const orderInput: Parameters<typeof createKuaishouFeifeiOrder>[0] = {
platformOrderNo,
productCode,
platformBuyNum: Number(payload.platformBuyNum || payload.platform_buy_num || 1) || 1,
playerAccount: String(payload.playerAccount || payload.player_account || '').trim(),
playerGameRegion: String(payload.playerGameRegion || payload.player_game_region || '').trim(),
playerGameSrv: String(payload.playerGameSrv || payload.player_game_srv || '').trim(),
playerGameRole: String(payload.playerGameRole || payload.player_game_role || '').trim(),
submitPlayer: payload.submitPlayer === true || payload.submit_player === true,
notifyUrl: String(payload.notifyUrl || payload.notify_url || '').trim(),
}
const platformAmount = normalizeOptionalNumber(payload.platformAmount ?? payload.platform_amount)
if (platformAmount !== undefined) {
orderInput.platformAmount = platformAmount
}
return createKuaishouFeifeiOrder(orderInput)
}
export async function queryAdminKuaishouFeifeiTestOrder(payload: JsonObject = {}) {
const platformOrderNo = String(payload.platformOrderNo || payload.platform_order_no || '').trim()
const orderNo = String(payload.orderNo || payload.order_no || '').trim()
if (!platformOrderNo && !orderNo) {
throw createHttpError('请填写 feifei 平台单号或测试业务单号', {
statusCode: 400,
errorCode: 'kuaishou_feifei_missing_order_query',
})
}
return queryKuaishouFeifeiOrder({
platformOrderNo,
orderNo,
})
}
function getAdminEditableKuaishouFeifeiConfig() {
if (hasKuaishouFeifeiConfigFile()) {
return getKuaishouFeifeiSourceConfig()
}
return normalizeKuaishouFeifeiSourceConfig(getKuaishouFeifeiConfig())
}
function mapEffectiveKuaishouFeifeiConfig(config: ReturnType<typeof getKuaishouFeifeiConfig>) {
return {
enabled: config.enabled !== false,
baseUrl: config.baseUrl,
timeoutMs: config.timeoutMs,
notifyUrl: config.notifyUrl,
hasAppKey: Boolean(config.appKey),
hasAppSecret: Boolean(config.appSecret),
productRuleCount: config.productRules.length,
}
}
async function listAllKuaishouFeifeiProducts(input: { status: string; supplyProductName: string }) {
const perPage = 100
const firstPage = await listKuaishouFeifeiProducts({
page: 1,
perPage,
status: input.status,
supplyProductName: input.supplyProductName,
})
const products = [...firstPage.items]
const total = Number(firstPage.total || products.length) || products.length
const pageCount = Math.max(1, Math.ceil(total / perPage))
for (let page = 2; page <= pageCount; page += 1) {
const result: KuaishouFeifeiProductListResult = await listKuaishouFeifeiProducts({
page,
perPage,
status: input.status,
supplyProductName: input.supplyProductName,
})
products.push(...result.items)
}
return products
}
function normalizeOptionalNumber(value: unknown) {
if (value == null || String(value).trim() === '') {
return undefined
}
const numberValue = Number(value)
return Number.isFinite(numberValue) ? numberValue : undefined
}
@@ -5,8 +5,6 @@ import {
saveNotificationConfig,
} from '../../notification/config-service.js'
import { sendInternalNotification } from '../../notification/notification-service.js'
import { listCloudtentaclesSources } from '../../platforms/cloudtentacles/source-config-service.js'
import { getAllCloudtentaclesSessionStates } from '../../platforms/cloudtentacles/session-state-service.js'
import {
getScheduledJobsConfig,
getScheduledJobsFilePath,
@@ -17,7 +15,7 @@ import {
reloadScheduledJobs,
runScheduledJobNow,
} from '../../scheduler/scheduler-service.js'
import { maskPhone, maskSecret } from './cloudtentacles/mappers.js'
import { maskSecret } from '../../../utils/masking.js'
import type {
AdminNotificationConfigInput,
@@ -59,7 +57,7 @@ export function getAdminScheduledJobsConfig() {
filePath: getScheduledJobsFilePath(),
source: mapAdminScheduledJobsConfig(config),
runtime: getScheduledJobRuntimeStates(),
cloudtentaclesAccounts: listAdminCloudtentaclesMonitorAccounts(),
cloudtentaclesAccounts: [] as Array<Record<string, never>>,
}
}
@@ -71,7 +69,7 @@ export async function updateAdminScheduledJobsConfig(payload: AdminScheduledJobs
filePath: getScheduledJobsFilePath(),
source: mapAdminScheduledJobsConfig(saved),
runtime: getScheduledJobRuntimeStates(),
cloudtentaclesAccounts: listAdminCloudtentaclesMonitorAccounts(),
cloudtentaclesAccounts: [] as Array<Record<string, never>>,
}
}
@@ -121,10 +119,6 @@ function mapAdminNotificationConfig(config: JsonObject = {}) {
}
function mapAdminScheduledJobsConfig(config: JsonObject = {}) {
const cloudtentaclesAccountMap = new Map(
listAdminCloudtentaclesMonitorAccounts().map((item) => [item.sourceKey, item]),
)
return {
enabled: config.enabled !== false,
jobs: (Array.isArray(config.jobs) ? config.jobs : []).map((item) => ({
@@ -135,115 +129,15 @@ function mapAdminScheduledJobsConfig(config: JsonObject = {}) {
cooldownSeconds: Number(item.cooldownSeconds || 1800),
config: {
assetThreshold: normalizeNonNegativeNumber(item.config?.assetThreshold, 500),
accounts: mapScheduledJobCloudtentaclesAccounts(
item.config?.accounts,
normalizeNonNegativeNumber(item.config?.assetThreshold, 500),
cloudtentaclesAccountMap,
),
accounts: Array.isArray(item.config?.accounts) ? item.config.accounts : [],
pageSize: Number(item.config?.pageSize || 0) || undefined,
scanLimit: Number(item.config?.scanLimit || 0) || undefined,
},
})),
}
}
function listAdminCloudtentaclesMonitorAccounts() {
const sourcesConfig = listCloudtentaclesSources()
const sessionsConfig = getAllCloudtentaclesSessionStates()
const sessions: Record<string, JsonObject> = sessionsConfig.sessions || {}
return (Array.isArray(sourcesConfig.sources) ? sourcesConfig.sources : [])
.map((source) => {
const sourceKey = String(source.key || '').trim()
const session = sessions[sourceKey] || {}
const label = String(source.label || source.username || sourceKey).trim() || sourceKey
return {
sourceKey,
label,
enabled: source.enabled !== false,
username: String(source.username || '').trim(),
phoneMasked: maskPhone(source.phone || session.phone),
hasToken: Boolean(String(session.token || '').trim()),
loggedInAt: String(session.loggedInAt || '').trim(),
}
})
.filter((item) => item.sourceKey)
}
function mapScheduledJobCloudtentaclesAccounts(
rawAccounts: unknown,
defaultAssetThreshold: number,
cloudtentaclesAccountMap: Map<string, JsonObject>,
) {
const accounts = Array.isArray(rawAccounts) ? rawAccounts : []
const configuredMap = new Map(
accounts
.map((item) => {
const source = isPlainObject(item) ? item : {}
const sourceKey = String(source.sourceKey || source.key || '').trim()
if (!sourceKey) {
return null
}
const option = cloudtentaclesAccountMap.get(sourceKey) || {}
return [
sourceKey,
{
sourceKey,
label: String(source.label || option.label || sourceKey).trim(),
enabled: source.enabled !== false,
assetThreshold: normalizeNonNegativeNumber(
source.assetThreshold ?? source.threshold,
defaultAssetThreshold,
),
},
] as const
})
.filter(Boolean) as Array<
readonly [
string,
{
sourceKey: string
label: string
enabled: boolean
assetThreshold: number
},
]
>,
)
// 读配置时合并全部 kuaishou-lewan 账号,避免前端/任务只看到历史 default。
const merged = Array.from(cloudtentaclesAccountMap.values())
.map((option) => {
const sourceKey = String(option.sourceKey || '').trim()
const configured = configuredMap.get(sourceKey)
return {
sourceKey,
label: String(configured?.label || option.label || sourceKey).trim(),
enabled: configured ? configured.enabled !== false : option.enabled !== false,
assetThreshold: normalizeNonNegativeNumber(
configured?.assetThreshold,
defaultAssetThreshold,
),
}
})
.filter((item) => item.sourceKey)
const mergedKeys = new Set(merged.map((item) => item.sourceKey))
for (const [sourceKey, configured] of configuredMap.entries()) {
if (mergedKeys.has(sourceKey)) {
continue
}
merged.push(configured)
}
return merged
}
function normalizeNonNegativeNumber(value: unknown, fallback: number) {
const parsed = Number(value)
return Number.isFinite(parsed) && parsed >= 0 ? parsed : fallback
}
function isPlainObject(value: unknown): value is JsonObject {
return Boolean(value) && typeof value === 'object' && !Array.isArray(value)
}
@@ -1,76 +0,0 @@
import { createHttpError } from '../../../utils/http.js'
import type { HttpErrorLike } from '../../../utils/http.js'
import type { JsonObject } from '../../../types/json.js'
import { resolvePersistedCloudtentaclesContextBySourceKeys } from '../../fulfillment/kuaishou-cloud/cloudtentacles-context.js'
export type { JsonObject }
export {
KUAISHOU_CLOUD_FIXED_VN_KEY,
isKuaishouCloudTask,
normalizeKuaishouCloudFlow,
normalizeKuaishouCloudRoleInfo,
resolveKuaishouCloudBindUrlExpiresAt,
} from '../../fulfillment/kuaishou-cloud/domain.js'
export {
prepareKuaishouCloudBindResourceWithFallback,
resolveKuaishouCloudBindingResources,
resolveKuaishouCloudVnKeyCandidates,
} from '../../fulfillment/kuaishou-cloud/binding-resources.js'
export type CloudtentaclesContext = {
baseUrl: string
token: string
deviceId: string
deviceType: number
resolvedSourceKey: string
}
export type CloudSkuLikeItem = {
id?: number | string
name?: string
price?: number | string
count?: number | string
[key: string]: unknown
}
export type { KuaishouCloudFlow } from '../../fulfillment/kuaishou-cloud/domain.js'
export type KuaishouCloudBindingResources = {
skuId: number
skuName: string
vnKey: string
skuItem: CloudSkuLikeItem | null
knapsackItem: CloudSkuLikeItem | null
resolvedByName: boolean
}
export type PreparedKuaishouCloudBindResource = {
vnKey: string
vnId: number
vnPhone: string
bindUrl: string
}
export function resolvePersistedCloudtentaclesContext(
sourceKeys: unknown[],
): CloudtentaclesContext {
try {
return resolvePersistedCloudtentaclesContextBySourceKeys(sourceKeys)
} catch (error) {
if (isMissingCloudtentaclesTokenError(error)) {
throw createHttpError('当前 cloudtentacles 没有可用 token,请先到平台配置完成登录校验', {
statusCode: 409,
errorCode: 'admin_task_kuaishou_cloud_missing_cloud_token',
cause: error,
})
}
throw error
}
}
function isMissingCloudtentaclesTokenError(error: unknown): boolean {
const current = error as HttpErrorLike | null
return String(current?.errorCode || '').trim() === 'kuaishou_cloud_missing_cloud_token'
}
@@ -18,15 +18,6 @@ import { nowIso } from '../../../utils/time.js'
import { TASK_STATUS } from '../../../domain/task-status.js'
import { createAdminViewerContext, parseTaskContext } from '../admin-read-shared-helpers.js'
import { getRequiredTask, mapTaskActionPayload } from '../admin-task-read-helpers.js'
import {
dispatchFulfillmentTask,
prepareFulfillmentBinding,
rebindFulfillmentRole,
refreshFulfillmentRole,
returnFulfillmentNumber,
} from '../../fulfillment/executors/registry.js'
import { isKuaishouCloudTask, normalizeKuaishouCloudFlow } from './kuaishou-cloud-helpers.js'
import type {
AdminEntityIdInput,
AdminViewerSessionInput,
@@ -35,195 +26,6 @@ import type { AdminTaskKuaishouIndustryConsumeInput } from '../../../types/admin
import type { AdminTaskActionResponse } from '../../../types/admin/write-models.js'
import type { KuaishouIndustryVoucherRow, TaskRow } from '../../../types/repository/rows.js'
export async function prepareAdminTaskKuaishouCloudFulfillment(
taskId: AdminEntityIdInput,
session: AdminViewerSessionInput | null = null,
): Promise<AdminTaskActionResponse> {
const task = await getRequiredTask(taskId)
const viewerContext = createAdminViewerContext(session)
if (!viewerContext.canManageTaskLifecycle) {
throw createHttpError('当前账号没有权限准备绑定资源', {
statusCode: 403,
errorCode: 'admin_task_prepare_kuaishou_cloud_forbidden',
})
}
if (!isKuaishouCloudTask(task)) {
throw createHttpError('当前任务不是 kuaishou-lewan 履约任务', {
statusCode: 409,
errorCode: 'admin_task_not_kuaishou_cloud',
})
}
const result = await prepareFulfillmentBinding(task, {
source: 'admin_task_prepare',
actor: buildAdminActionActor(session),
force: true,
})
if (!result?.task) {
throw createHttpError('当前任务不支持准备绑定资源', {
statusCode: 409,
errorCode: 'admin_task_prepare_not_supported',
})
}
return {
task: mapTaskActionPayload(result.task),
claimUrl: String(result.claimUrl || ''),
token: String(result.token || ''),
}
}
export async function dispatchAdminTaskKuaishouCloudFulfillment(
taskId: AdminEntityIdInput,
session: AdminViewerSessionInput | null = null,
): Promise<AdminTaskActionResponse> {
const task = await getRequiredTask(taskId)
const viewerContext = createAdminViewerContext(session)
if (!viewerContext.canManageTaskLifecycle) {
throw createHttpError('当前账号没有权限执行发货', {
statusCode: 403,
errorCode: 'admin_task_dispatch_kuaishou_cloud_forbidden',
})
}
if (!isKuaishouCloudTask(task)) {
throw createHttpError('当前任务不是 kuaishou-lewan 履约任务', {
statusCode: 409,
errorCode: 'admin_task_not_kuaishou_cloud',
})
}
const result = await dispatchFulfillmentTask(task, {
source: 'admin_task_dispatch',
actor: buildAdminActionActor(session),
autoFinalize: false,
})
if (!result?.task) {
throw createHttpError('当前任务不支持 executor 发货', {
statusCode: 409,
errorCode: 'admin_task_dispatch_not_supported',
})
}
return {
task: mapTaskActionPayload(result.task),
}
}
export async function refreshAdminTaskKuaishouCloudRoleInfo(
taskId: AdminEntityIdInput,
session: AdminViewerSessionInput | null = null,
): Promise<AdminTaskActionResponse> {
const task = await getRequiredTask(taskId)
const viewerContext = createAdminViewerContext(session)
if (!viewerContext.canOperateAssistedTask) {
throw createHttpError('当前账号没有权限刷新角色信息', {
statusCode: 403,
errorCode: 'admin_task_refresh_kuaishou_cloud_role_forbidden',
})
}
if (!isKuaishouCloudTask(task)) {
throw createHttpError('当前任务不是 kuaishou-lewan 履约任务', {
statusCode: 409,
errorCode: 'admin_task_not_kuaishou_cloud',
})
}
const result = await refreshFulfillmentRole(task, {
source: 'admin_task_refresh_role',
actor: buildAdminActionActor(session),
forceProbe: true,
recordEvent: true,
})
if (!result?.task) {
throw createHttpError('当前任务不支持刷新角色信息', {
statusCode: 409,
errorCode: 'admin_task_refresh_not_supported',
})
}
return {
task: mapTaskActionPayload(result.task),
}
}
export async function rebindAdminTaskKuaishouCloudRole(
taskId: AdminEntityIdInput,
session: AdminViewerSessionInput | null = null,
): Promise<AdminTaskActionResponse> {
const task = await getRequiredTask(taskId)
const viewerContext = createAdminViewerContext(session)
// 与刷新角色一致:客服可换绑协助处理 UID 绑错,不开放完整任务生命周期
if (!viewerContext.canOperateAssistedTask) {
throw createHttpError('当前账号没有权限换绑角色', {
statusCode: 403,
errorCode: 'admin_task_rebind_kuaishou_cloud_forbidden',
})
}
const result = await rebindFulfillmentRole(task, {
source: 'admin_task_rebind_role',
actor: buildAdminActionActor(session),
})
if (!result?.task) {
throw createHttpError('当前任务不支持换绑角色', {
statusCode: 409,
errorCode: 'admin_task_rebind_not_supported',
})
}
return {
task: mapTaskActionPayload(result.task),
claimUrl: String(result.claimUrl || ''),
token: String(result.token || ''),
}
}
export async function returnNumberAdminTaskKuaishouCloudFulfillment(
taskId: AdminEntityIdInput,
session: AdminViewerSessionInput | null = null,
): Promise<AdminTaskActionResponse> {
const task = await getRequiredTask(taskId)
const viewerContext = createAdminViewerContext(session)
if (!viewerContext.canManageTaskLifecycle) {
throw createHttpError('当前账号没有权限退还号码', {
statusCode: 403,
errorCode: 'admin_task_return_kuaishou_cloud_forbidden',
})
}
if (!isKuaishouCloudTask(task)) {
throw createHttpError('当前任务不是 kuaishou-lewan 履约任务', {
statusCode: 409,
errorCode: 'admin_task_not_kuaishou_cloud',
})
}
// admin 退号 = 清理虚拟号占位,不核销电子凭证
const result = await returnFulfillmentNumber(task, {
source: 'admin_task_return_number',
actor: buildAdminActionActor(session),
consumeIndustryVoucher: false,
})
if (!result?.task) {
throw createHttpError('当前任务不支持退还号码', {
statusCode: 409,
errorCode: 'admin_task_return_not_supported',
})
}
return {
task: mapTaskActionPayload(result.task),
}
}
export async function resendAdminTaskKuaishouIndustryVoucherCode(
taskId: AdminEntityIdInput,
session: AdminViewerSessionInput | null = null,
@@ -250,7 +52,7 @@ export async function resendAdminTaskKuaishouIndustryVoucherCode(
if (!result.success) {
const message = String(result.error || '电子凭证发码回调重发失败').trim()
const failedTask = await updateTask(task.id, {
await updateTask(task.id, {
task_status: TASK_STATUS.MANUAL_REVIEW,
result_code: 'kuaishou_industry_send_callback_resend_failed',
result_message: message,
@@ -268,7 +70,6 @@ export async function resendAdminTaskKuaishouIndustryVoucherCode(
},
now,
)
void failedTask
throw createHttpError(message, {
statusCode: 409,
errorCode: 'admin_task_kuaishou_industry_resend_failed',
@@ -405,7 +206,7 @@ export async function consumeAdminTaskKuaishouIndustryVoucher(
if (!result.ok || !result.voucher) {
const message = String(result.errorMessage || '电子凭证核销失败').trim()
const failedTask = await updateTask(task.id, {
await updateTask(task.id, {
task_status: TASK_STATUS.MANUAL_REVIEW,
result_code: 'kuaishou_industry_manual_consume_failed',
result_message: message,
@@ -423,7 +224,6 @@ export async function consumeAdminTaskKuaishouIndustryVoucher(
},
now,
)
void failedTask
throw createHttpError(message, {
statusCode: 409,
errorCode: 'admin_task_kuaishou_industry_consume_failed',
@@ -436,30 +236,6 @@ export async function consumeAdminTaskKuaishouIndustryVoucher(
})
const consumeTargetTask = attachedConsumeTask || task
const taskContext = parseTaskContext(consumeTargetTask)
const hasCloudFulfillmentContext = Boolean(
taskContext.kuaishouCloudFulfillment &&
typeof taskContext.kuaishouCloudFulfillment === 'object' &&
!Array.isArray(taskContext.kuaishouCloudFulfillment),
)
const flow = hasCloudFulfillmentContext
? normalizeKuaishouCloudFlow(taskContext.kuaishouCloudFulfillment)
: null
const nextContext = {
...taskContext,
...(flow
? {
kuaishouCloudFulfillment: {
...flow,
consume: {
...flow.consume,
status: 'success',
consumedAt: result.voucher.consumed_at || now,
errorMessage: '',
},
},
}
: {}),
}
const updatedTask = await updateTask(consumeTargetTask.id, {
task_status: TASK_STATUS.COMPLETED,
delivery_status: 'delivered',
@@ -467,7 +243,7 @@ export async function consumeAdminTaskKuaishouIndustryVoucher(
result_message: '电子凭证已手动核销完成',
redeemed_at: result.voucher.consumed_at || now,
last_error: '',
context_json: JSON.stringify(nextContext),
context_json: JSON.stringify(taskContext),
updated_at: now,
})