偿还 lewan 工程债:模块拆分、默认角色降级与短链收口

将 kuaishou-cloud 大入口拆为 prepare/rebind/probe/refresh 等模块;默认角色仅作诊断;补充兑换状态单测;短链明确只读兼容历史链接。
This commit is contained in:
yml2213
2026-07-10 14:01:29 +08:00
parent c70c3ff6a5
commit d6b0e45c7b
15 changed files with 1508 additions and 1190 deletions
@@ -0,0 +1,480 @@
import { createTaskEvent } from '../../../repositories/task-event-repo.js'
import { updateTask } from '../../../repositories/task-repo.js'
import { buildClaimUrl } from '../../claim/claim-service.js'
import { listCloudtentaclesSku } from '../../platforms/cloudtentacles/catalog-service.js'
import { getCloudtentaclesKnapsack } from '../../platforms/cloudtentacles/knapsack-service.js'
import { backCloudtentaclesVirtualNumber } from '../../platforms/cloudtentacles/virtual-number-service.js'
import { notifyKuaishouCloudBindUrlRefreshFailed } from '../../notification/domain-notifications.js'
import { createHttpError } from '../../../utils/http.js'
import { nowIso } from '../../../utils/time.js'
import { TASK_STATUS, normalizeTaskStatus } from '../../../domain/task-status.js'
import {
KUAISHOU_CLOUD_FIXED_VN_KEY,
isKuaishouCloudBindUrlFresh,
isKuaishouCloudTask,
maskPhone,
normalizeKuaishouCloudFlow,
resolveKuaishouCloudBindUrlExpiresAt,
type JsonObject,
} from './domain.js'
import {
prepareKuaishouCloudBindResourceWithFallback,
resolveKuaishouCloudBindingResources,
resolveKuaishouCloudVnKeyCandidates,
} from './binding-resources.js'
import {
buildCloudtentaclesSourceSelectionEventDetail,
selectCloudtentaclesSourceForFulfillment,
} from './account-selector.js'
import { resolvePersistedCloudtentaclesContextBySourceKeys } from './cloudtentacles-context.js'
import {
getTaskClaimExpiresAt,
normalizeActor,
parseTaskContext,
} from './task-context.js'
import { resolveKuaishouCloudDeliveryPlan } from './delivery-plan.js'
import { ensureTaskClaimLink } from './ensure-claim-link.js'
import {
buildPendingRoleWithDefaultSnapshot,
captureKuaishouCloudDefaultRoleSnapshot,
} from './role-state.js'
import type { TaskRow } from '../../../types/repository/rows.js'
export async function prepareKuaishouCloudFulfillmentTask(task: TaskRow, options: JsonObject = {}) {
if (!isKuaishouCloudTask(task)) {
throw createHttpError('当前任务不是 kuaishou-lewan 履约任务', {
statusCode: 409,
errorCode: 'kuaishou_cloud_task_invalid',
})
}
const now = nowIso()
const actor = normalizeActor(options.actor)
const force = options.force === true
const taskContext = parseTaskContext(task)
const flow = normalizeKuaishouCloudFlow(taskContext.kuaishouCloudFulfillment)
const claimLinkState = await ensureTaskClaimLink(task)
if (
!force &&
flow.binding.prepareStatus === 'ready' &&
flow.binding.vnId > 0 &&
flow.binding.vnPhone &&
flow.binding.bindUrl
) {
if (!isKuaishouCloudBindUrlFresh(flow)) {
return refreshKuaishouCloudTaskBindUrl(task, {
source: options.source || 'system_refresh_expired_bind_url',
actor,
claimLinkState,
})
}
let readyTask = task
if (normalizeTaskStatus(task.task_status) !== TASK_STATUS.WAITING_BINDING) {
readyTask =
(await updateTask(task.id, {
task_status: TASK_STATUS.WAITING_BINDING,
claim_token: claimLinkState.token || task.claim_token || '',
claim_expires_at: claimLinkState.expiredAt || getTaskClaimExpiresAt(task),
updated_at: now,
})) || task
}
return {
task: readyTask,
claimUrl: claimLinkState.claimUrl,
token: claimLinkState.token,
flow,
}
}
const selectedCloud = await selectCloudtentaclesSourceForFulfillment(flow)
try {
const cloudContext = selectedCloud.context
const [knapsack, skuList] = await Promise.all([
getCloudtentaclesKnapsack(cloudContext),
listCloudtentaclesSku(cloudContext),
])
const resolvedBinding = resolveKuaishouCloudBindingResources(flow, {
skuItems: Array.isArray(skuList.items) ? skuList.items : [],
knapsackItems: Array.isArray(knapsack.items) ? knapsack.items : [],
})
const vnKeyCandidates = resolveKuaishouCloudVnKeyCandidates({
flow,
binding: resolvedBinding,
})
if (!resolvedBinding.skuId || vnKeyCandidates.length === 0) {
throw createHttpError('当前任务缺少可用 cloud 资源,无法自动准备绑定', {
statusCode: 409,
errorCode: 'kuaishou_cloud_missing_binding_config',
})
}
const flowWithResolvedBinding = {
...flow,
binding: {
...flow.binding,
skuId: resolvedBinding.skuId,
skuName: resolvedBinding.skuName,
vnKey: KUAISHOU_CLOUD_FIXED_VN_KEY,
},
}
const deliveryPlan = resolveKuaishouCloudDeliveryPlan(flowWithResolvedBinding, {
skuItems: Array.isArray(skuList.items) ? skuList.items : [],
knapsackItems: Array.isArray(knapsack.items) ? knapsack.items : [],
})
if (deliveryPlan.items.length === 0) {
throw createHttpError('当前任务缺少 cloud 发货物品配置', {
statusCode: 409,
errorCode: 'kuaishou_cloud_missing_delivery_items',
})
}
const usedKnapsack = deliveryPlan.items.every((item) => item.missingCount <= 0)
if (!usedKnapsack && !flowWithResolvedBinding.purchase.autoBuyEnabled) {
throw createHttpError('背包中没有现成库存,且当前配置未开启自动购买', {
statusCode: 409,
errorCode: 'kuaishou_cloud_auto_buy_disabled',
})
}
const missingSku = deliveryPlan.items.find((item) => item.missingCount > 0 && !item.skuItem)
if (missingSku) {
throw createHttpError(`cloudtentacles 未找到 SKU ${missingSku.cloudSkuId}`, {
statusCode: 404,
errorCode: 'kuaishou_cloud_sku_not_found',
})
}
const purchaseTriggered = false
const assetBefore = 0
const assetAfter = 0
const preparedBinding = await prepareKuaishouCloudBindResourceWithFallback({
cloudContext,
vnKeyCandidates,
})
const defaultRoleSnapshot = await captureKuaishouCloudDefaultRoleSnapshot({
cloudContext,
preparedBinding,
now,
})
const nextContext = {
...taskContext,
kuaishouCloudFulfillment: {
...flowWithResolvedBinding,
binding: {
...flowWithResolvedBinding.binding,
resolvedSourceKey: cloudContext.resolvedSourceKey,
vnKey: preparedBinding.vnKey,
prepareStatus: 'ready',
vnId: preparedBinding.vnId,
vnPhone: preparedBinding.vnPhone,
bindUrl: preparedBinding.bindUrl,
bindPreparedAt: now,
bindExpiresAt: resolveKuaishouCloudBindUrlExpiresAt(now),
bindProbeAt: null as null,
bindProbeStatus: 'pending',
bindProbeMessage: '',
roleName: '',
roleId: '',
},
role: buildPendingRoleWithDefaultSnapshot(defaultRoleSnapshot),
purchase: {
...flowWithResolvedBinding.purchase,
usedKnapsack,
purchaseTriggered,
assetBefore,
assetAfter,
items: deliveryPlan.items.map((item) => ({
cloudSkuId: item.cloudSkuId,
cloudSkuName: item.cloudSkuName,
requiredCount: item.quantity,
knapsackCount: item.knapsackCount,
purchasedCount: 0,
})),
purchaseAt: flowWithResolvedBinding.purchase.purchaseAt,
},
},
}
const updatedTask = await updateTask(task.id, {
task_status: TASK_STATUS.WAITING_BINDING,
user_action_status: 'pending_claim',
claim_token: claimLinkState.token || task.claim_token || '',
claim_expires_at: claimLinkState.expiredAt || getTaskClaimExpiresAt(task),
role_id: '',
role_name: '',
last_error: '',
context_json: JSON.stringify(nextContext),
updated_at: now,
})
await createTaskEvent(
task.id,
'kuaishou_cloud_binding_prepared',
{
source: String(options.source || 'system').trim() || 'system',
skuId: flowWithResolvedBinding.binding.skuId,
skuName: flowWithResolvedBinding.binding.skuName,
vnKey: preparedBinding.vnKey,
vnId: preparedBinding.vnId,
vnPhoneMasked: maskPhone(preparedBinding.vnPhone),
purchaseTriggered,
usedKnapsack,
deliveryItems: deliveryPlan.items.map((item) => ({
cloudSkuId: item.cloudSkuId,
cloudSkuName: item.cloudSkuName,
quantity: item.quantity,
knapsackCount: item.knapsackCount,
purchasedCount: 0,
})),
resolvedByName: resolvedBinding.resolvedByName,
accountSelection: buildCloudtentaclesSourceSelectionEventDetail(selectedCloud),
defaultRoleName: defaultRoleSnapshot.defaultName,
defaultRoleId: defaultRoleSnapshot.defaultRid,
defaultRoleCaptureStatus: defaultRoleSnapshot.defaultCaptureStatus,
actor,
},
now,
)
return {
task: updatedTask,
claimUrl: claimLinkState.claimUrl,
token: claimLinkState.token,
flow: normalizeKuaishouCloudFlow(nextContext.kuaishouCloudFulfillment),
}
} finally {
selectedCloud.release()
}
}
export async function refreshKuaishouCloudTaskBindUrl(task: TaskRow, options: JsonObject = {}) {
if (!isKuaishouCloudTask(task)) {
throw createHttpError('当前任务不是 kuaishou-lewan 履约任务', {
statusCode: 409,
errorCode: 'kuaishou_cloud_task_invalid',
})
}
const now = nowIso()
const actor = normalizeActor(options.actor)
const taskContext = parseTaskContext(task)
const flow = normalizeKuaishouCloudFlow(taskContext.kuaishouCloudFulfillment)
if (!flow.binding.vnId || !flow.binding.vnKey) {
throw createHttpError('当前任务缺少可刷新绑定链接的虚拟号信息', {
statusCode: 409,
errorCode: 'kuaishou_cloud_missing_bind_url_context',
})
}
const cloudContext = resolvePersistedCloudtentaclesContextBySourceKeys([
flow.binding.resolvedSourceKey,
...flow.binding.cloudSourceKeys,
])
const oldVnKey = flow.binding.vnKey
const oldVnId = flow.binding.vnId
const oldVnPhone = flow.binding.vnPhone
await backCloudtentaclesVirtualNumber({
...cloudContext,
key: oldVnKey,
id: oldVnId,
})
await createTaskEvent(
task.id,
'kuaishou_cloud_expired_bind_number_returned',
{
source: String(options.source || 'system').trim() || 'system',
vnKey: oldVnKey,
vnId: oldVnId,
vnPhoneMasked: maskPhone(oldVnPhone),
actor,
},
now,
)
let preparedBinding
try {
preparedBinding = await prepareKuaishouCloudBindResourceWithFallback({
cloudContext,
vnKeyCandidates: resolveKuaishouCloudVnKeyCandidates({
flow,
binding: flow.binding,
}),
})
if (!String(preparedBinding.bindUrl || '').trim()) {
throw createHttpError('cloudtentacles 未返回新的绑定链接', {
statusCode: 502,
errorCode: 'kuaishou_cloud_empty_bind_url',
})
}
} catch (error) {
const failedTask = await markKuaishouCloudBindUrlRefreshFailed(task, {
taskContext,
flow,
now,
actor,
error,
})
return {
task: failedTask,
claimUrl: buildClaimUrl(String(task.primary_claim_token || task.claim_token || '')),
token: String(task.primary_claim_token || task.claim_token || ''),
flow: normalizeKuaishouCloudFlow(parseTaskContext(failedTask).kuaishouCloudFulfillment),
}
}
const defaultRoleSnapshot =
normalizeTaskStatus(task.task_status) === TASK_STATUS.ROLE_CONFIRMED
? null
: await captureKuaishouCloudDefaultRoleSnapshot({
cloudContext,
preparedBinding,
now,
})
const nextContext = {
...taskContext,
kuaishouCloudFulfillment: {
...flow,
binding: {
...flow.binding,
prepareStatus: 'ready',
vnKey: preparedBinding.vnKey,
vnId: preparedBinding.vnId,
vnPhone: preparedBinding.vnPhone,
bindUrl: preparedBinding.bindUrl,
bindPreparedAt: now,
bindExpiresAt: resolveKuaishouCloudBindUrlExpiresAt(now),
bindProbeAt: null as null,
bindProbeStatus: 'pending',
bindProbeMessage: '',
roleName:
normalizeTaskStatus(task.task_status) === TASK_STATUS.ROLE_CONFIRMED
? flow.binding.roleName
: '',
roleId:
normalizeTaskStatus(task.task_status) === TASK_STATUS.ROLE_CONFIRMED
? flow.binding.roleId
: '',
},
role:
normalizeTaskStatus(task.task_status) === TASK_STATUS.ROLE_CONFIRMED
? flow.role
: buildPendingRoleWithDefaultSnapshot(defaultRoleSnapshot!),
},
}
const claimLinkState = options.claimLinkState || (await ensureTaskClaimLink(task))
const updatedTask = await updateTask(task.id, {
task_status:
normalizeTaskStatus(task.task_status) === TASK_STATUS.ROLE_CONFIRMED
? task.task_status
: TASK_STATUS.WAITING_BINDING,
claim_token: claimLinkState.token || task.claim_token || '',
claim_expires_at: claimLinkState.expiredAt || getTaskClaimExpiresAt(task),
role_id:
normalizeTaskStatus(task.task_status) === TASK_STATUS.ROLE_CONFIRMED ? task.role_id : '',
role_name:
normalizeTaskStatus(task.task_status) === TASK_STATUS.ROLE_CONFIRMED ? task.role_name : '',
last_error: '',
context_json: JSON.stringify(nextContext),
updated_at: now,
})
await createTaskEvent(
task.id,
'kuaishou_cloud_bind_url_refreshed',
{
source: String(options.source || 'system').trim() || 'system',
oldVnId,
oldVnPhoneMasked: maskPhone(oldVnPhone),
vnKey: preparedBinding.vnKey,
vnId: preparedBinding.vnId,
vnPhoneMasked: maskPhone(preparedBinding.vnPhone),
actor,
},
now,
)
return {
task: updatedTask,
claimUrl: claimLinkState.claimUrl,
token: claimLinkState.token,
flow: normalizeKuaishouCloudFlow(nextContext.kuaishouCloudFulfillment),
}
}
async function markKuaishouCloudBindUrlRefreshFailed(
task: TaskRow,
{ taskContext, flow, now, actor, error }: JsonObject = {},
) {
const errorMessage =
error instanceof Error ? error.message : String(error || '新绑定链接准备失败')
const nextContext = {
...taskContext,
kuaishouCloudFulfillment: {
...flow,
binding: {
...flow.binding,
prepareStatus: 'pending',
vnId: 0,
vnPhone: '',
bindUrl: '',
bindPreparedAt: null,
bindExpiresAt: null,
bindProbeAt: now,
bindProbeStatus: 'refresh_failed',
bindProbeMessage: errorMessage,
roleName: '',
roleId: '',
},
role: {
status: 'pending',
name: '',
rid: '',
refreshedAt: now,
errorMessage: '绑定链接已过期,旧号码已退还,新链接准备失败,请稍后刷新或联系客服处理',
rawInfo: null,
},
},
}
const updatedTask = await updateTask(task.id, {
task_status: TASK_STATUS.PENDING_BINDING_PREPARE,
role_id: '',
role_name: '',
last_error: `绑定链接过期,旧号码已退还,新链接准备失败:${errorMessage}`,
context_json: JSON.stringify(nextContext),
updated_at: now,
})
await createTaskEvent(
task.id,
'kuaishou_cloud_bind_url_refresh_failed',
{
source: 'system_refresh_expired_bind_url',
errorMessage,
actor,
},
now,
)
await notifyKuaishouCloudBindUrlRefreshFailed({
task: updatedTask,
errorMessage,
})
return updatedTask
}