refactor(fulfillment): 移除飞飞/乐玩通道,仅保留 affiliate_dash
新单只路由 affiliate_dash(未命中走 manual_dispatch);历史飞飞/乐玩任务保留只读 stub,避免订单重同步失败。同步清理 cloudtentacles 调度、Admin 写路径与相关 env。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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'
|
||||
}
|
||||
+3
-227
@@ -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,
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user