统一领取流:Step1 填 UID,按平台分叉履约
领取页改为统一 UID 入口;lewan 强制角色 ID 匹配后才发货核销,feifei 将 uid 拼入 H5 并去掉 claim 短链,对外仍返回本站 claimUrl。
This commit is contained in:
@@ -31,16 +31,13 @@ test('resolveTaskDeliveryLink 兼容历史 kuaishou-industry cloud 任务', asyn
|
||||
})
|
||||
})
|
||||
|
||||
test('resolveTaskDeliveryLink 为 feifei 任务返回已保存短链', async () => {
|
||||
test('resolveTaskDeliveryLink 为 feifei 任务返回本站领取链接', async () => {
|
||||
const result = await resolveTaskDeliveryLink(createTask({
|
||||
executor_key: 'kuaishou_feifei',
|
||||
primary_claim_token: 'feifei-token',
|
||||
primary_claim_expires_at: '2026-07-09T12:00:00.000Z',
|
||||
context_json: {
|
||||
kuaishouFeifei: {
|
||||
shortLink: {
|
||||
code: 'AbCd1234',
|
||||
url: 'https://ks.khhao.com/s/AbCd1234',
|
||||
targetUrl: 'https://feifei.example.com/h5/bind?code=very-long',
|
||||
},
|
||||
h5: {
|
||||
rechargeUrl: 'https://feifei.example.com/h5/bind?code=very-long',
|
||||
},
|
||||
@@ -49,8 +46,8 @@ test('resolveTaskDeliveryLink 为 feifei 任务返回已保存短链', async ()
|
||||
}))
|
||||
|
||||
assert.deepEqual(result, {
|
||||
claimUrl: 'https://ks.khhao.com/s/AbCd1234',
|
||||
expireTime: '',
|
||||
claimUrl: buildClaimUrl('feifei-token'),
|
||||
expireTime: '2026-07-09T12:00:00.000Z',
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
import { buildClaimUrl } from '../../claim/claim-service.js'
|
||||
import { TASK_STATUS } from '../../../domain/task-status.js'
|
||||
import {
|
||||
ensureKuaishouFeifeiClaimShortLink,
|
||||
prepareKuaishouFeifeiTask,
|
||||
resolveKuaishouFeifeiClaimUrl,
|
||||
} from '../kuaishou-feifei/index.js'
|
||||
import { parseTaskContext } from '../../../utils/task-json.js'
|
||||
import { prepareKuaishouFeifeiTask } from '../kuaishou-feifei/index.js'
|
||||
import {
|
||||
FULFILLMENT_EXECUTOR_KEYS,
|
||||
type FulfillmentDeliveryLink,
|
||||
@@ -24,7 +20,19 @@ async function preparePaidTask(
|
||||
deps: FulfillmentPrepareDeps,
|
||||
): Promise<TaskRow | null> {
|
||||
try {
|
||||
return await prepareKuaishouFeifeiTask(task)
|
||||
let workingTask = task
|
||||
if (!task.primary_claim_token_id && !String(task.claim_token || '').trim()) {
|
||||
const claimToken = await deps.createTaskClaimToken(task.id)
|
||||
workingTask =
|
||||
(await deps.updateTask(task.id, {
|
||||
claim_token: claimToken.token,
|
||||
claim_expires_at: claimToken.expired_at,
|
||||
user_action_status: 'pending_claim',
|
||||
updated_at: deps.nowIso(),
|
||||
})) || task
|
||||
}
|
||||
|
||||
return await prepareKuaishouFeifeiTask(workingTask)
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'kuaishou-feifei 订单创建失败'
|
||||
const updatedTask = await deps.updateTask(task.id, {
|
||||
@@ -44,19 +52,19 @@ async function preparePaidTask(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 对外(91 等)统一返回本站领取链接,不再返回 feifei H5 或短链。
|
||||
*/
|
||||
async function resolveDeliveryLink(task: TaskRow): Promise<FulfillmentDeliveryLink | null> {
|
||||
let claimUrl = await ensureKuaishouFeifeiClaimShortLink(task)
|
||||
if (!claimUrl) {
|
||||
const context = parseTaskContext(task)
|
||||
claimUrl = resolveKuaishouFeifeiClaimUrl(context.kuaishouFeifei)
|
||||
}
|
||||
const primaryToken = String(task.primary_claim_token || task.claim_token || '').trim()
|
||||
const expireTime = String(task.primary_claim_expires_at || task.claim_expires_at || '').trim()
|
||||
|
||||
if (!claimUrl) {
|
||||
if (!primaryToken) {
|
||||
return null
|
||||
}
|
||||
|
||||
return {
|
||||
claimUrl,
|
||||
expireTime: '',
|
||||
claimUrl: buildClaimUrl(primaryToken),
|
||||
expireTime,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,11 @@ import { updateTask } from "../../../repositories/task-repo.js";
|
||||
import type { TaskRow } from "../../../types/repository/rows.js";
|
||||
import { createHttpError } from "../../../utils/http.js";
|
||||
import { getCloudtentaclesBindInfo } from "../../platforms/cloudtentacles/virtual-number-service.js";
|
||||
import {
|
||||
getClaimIdentityFromContext,
|
||||
isClaimUidMatched,
|
||||
normalizeClaimUid,
|
||||
} from "../../claim/claim-identity.js";
|
||||
import {
|
||||
normalizeKuaishouCloudFlow,
|
||||
normalizeKuaishouCloudRoleInfo,
|
||||
@@ -31,6 +36,7 @@ export async function syncKuaishouCloudRoleInfoBeforeDispatch(
|
||||
const flow = normalizeKuaishouCloudFlow(
|
||||
input.flow || taskContext.kuaishouCloudFulfillment
|
||||
);
|
||||
const expectedUid = getClaimIdentityFromContext(taskContext).expectedUid;
|
||||
|
||||
if (!flow.binding.vnId || !flow.binding.vnKey) {
|
||||
throw createHttpError("当前任务缺少可同步角色的虚拟号信息,暂时不能发货", {
|
||||
@@ -62,40 +68,70 @@ export async function syncKuaishouCloudRoleInfoBeforeDispatch(
|
||||
const confirmedRoleId = String(
|
||||
flow.binding.roleId || flow.role.rid || task.role_id || ""
|
||||
).trim();
|
||||
const roleMatches = isSameKuaishouCloudRole(
|
||||
{
|
||||
name: confirmedRoleName,
|
||||
rid: confirmedRoleId,
|
||||
},
|
||||
{
|
||||
name: roleInfo.name,
|
||||
rid: roleInfo.rid,
|
||||
const liveBoundUid = normalizeClaimUid(roleInfo.rid);
|
||||
|
||||
// 优先:声明 UID 与云端当前绑定角色一致
|
||||
if (expectedUid) {
|
||||
if (!isClaimUidMatched(expectedUid, liveBoundUid)) {
|
||||
await createTaskEvent(
|
||||
task.id,
|
||||
"kuaishou_cloud_dispatch_uid_mismatch",
|
||||
{
|
||||
source,
|
||||
vnId: flow.binding.vnId,
|
||||
expectedUid,
|
||||
cloudtentaclesRoleName: roleInfo.name,
|
||||
cloudtentaclesRoleId: roleInfo.rid,
|
||||
actor: input.actor || null,
|
||||
},
|
||||
now
|
||||
);
|
||||
|
||||
throw createHttpError(
|
||||
`cloudtentacles 当前绑定角色 ID(${roleInfo.rid})与用户填写 UID(${expectedUid})不一致,不能发货`,
|
||||
{
|
||||
statusCode: 409,
|
||||
errorCode: `${errorCodePrefix}_dispatch_uid_mismatch`,
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
if (!roleMatches) {
|
||||
await createTaskEvent(
|
||||
task.id,
|
||||
"kuaishou_cloud_dispatch_role_mismatch",
|
||||
} else {
|
||||
// 兼容旧任务:无 expectedUid 时仍比对「确认角色」
|
||||
const roleMatches = isSameKuaishouCloudRole(
|
||||
{
|
||||
source,
|
||||
vnId: flow.binding.vnId,
|
||||
confirmedRoleName,
|
||||
confirmedRoleId,
|
||||
cloudtentaclesRoleName: roleInfo.name,
|
||||
cloudtentaclesRoleId: roleInfo.rid,
|
||||
actor: input.actor || null,
|
||||
name: confirmedRoleName,
|
||||
rid: confirmedRoleId,
|
||||
},
|
||||
now
|
||||
);
|
||||
|
||||
throw createHttpError(
|
||||
`cloudtentacles 当前绑定角色(${roleInfo.name}/${roleInfo.rid})与客户确认角色(${confirmedRoleName || "-"}/${confirmedRoleId || "-"})不一致,请重新刷新角色后再发货`,
|
||||
{
|
||||
statusCode: 409,
|
||||
errorCode: `${errorCodePrefix}_dispatch_role_mismatch`,
|
||||
name: roleInfo.name,
|
||||
rid: roleInfo.rid,
|
||||
}
|
||||
);
|
||||
|
||||
if (!roleMatches) {
|
||||
await createTaskEvent(
|
||||
task.id,
|
||||
"kuaishou_cloud_dispatch_role_mismatch",
|
||||
{
|
||||
source,
|
||||
vnId: flow.binding.vnId,
|
||||
confirmedRoleName,
|
||||
confirmedRoleId,
|
||||
cloudtentaclesRoleName: roleInfo.name,
|
||||
cloudtentaclesRoleId: roleInfo.rid,
|
||||
actor: input.actor || null,
|
||||
},
|
||||
now
|
||||
);
|
||||
|
||||
throw createHttpError(
|
||||
`cloudtentacles 当前绑定角色(${roleInfo.name}/${roleInfo.rid})与客户确认角色(${confirmedRoleName || "-"}/${confirmedRoleId || "-"})不一致,请重新刷新角色后再发货`,
|
||||
{
|
||||
statusCode: 409,
|
||||
errorCode: `${errorCodePrefix}_dispatch_role_mismatch`,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const nextFlow = normalizeKuaishouCloudFlow({
|
||||
|
||||
@@ -4,6 +4,7 @@ import assert from 'node:assert/strict'
|
||||
import {
|
||||
buildFeifeiPlatformOrderNo,
|
||||
resolveKuaishouFeifeiClaimUrl,
|
||||
resolveKuaishouFeifeiH5UrlWithUid,
|
||||
resolveKuaishouFeifeiPlatformAmount,
|
||||
} from './index.js'
|
||||
import type { TaskRow } from '../../../types/repository/rows.js'
|
||||
@@ -31,7 +32,7 @@ test('resolveKuaishouFeifeiClaimUrl does not expose local claim token fallback',
|
||||
)
|
||||
})
|
||||
|
||||
test('resolveKuaishouFeifeiClaimUrl prefers internal short link', () => {
|
||||
test('resolveKuaishouFeifeiClaimUrl prefers direct h5 over historical short link url', () => {
|
||||
assert.equal(
|
||||
resolveKuaishouFeifeiClaimUrl({
|
||||
shortLink: {
|
||||
@@ -43,10 +44,24 @@ test('resolveKuaishouFeifeiClaimUrl prefers internal short link', () => {
|
||||
rechargeUrl: 'https://feifei.example.com/h5/bind?code=very-long',
|
||||
},
|
||||
}),
|
||||
'https://ks.khhao.com/s/AbCd1234',
|
||||
'https://feifei.example.com/h5/bind?code=very-long',
|
||||
)
|
||||
})
|
||||
|
||||
test('resolveKuaishouFeifeiH5UrlWithUid appends uid query', () => {
|
||||
const next = resolveKuaishouFeifeiH5UrlWithUid(
|
||||
{
|
||||
h5: {
|
||||
rechargeUrl: 'http://skin-exchange.yiquyou.icu/h5/bind?code=eyJpdi&product_name=demo',
|
||||
},
|
||||
},
|
||||
'166909256',
|
||||
)
|
||||
const url = new URL(next)
|
||||
assert.equal(url.searchParams.get('uid'), '166909256')
|
||||
assert.equal(url.searchParams.get('code'), 'eyJpdi')
|
||||
})
|
||||
|
||||
test('buildFeifeiPlatformOrderNo prefers source order number', () => {
|
||||
assert.equal(
|
||||
buildFeifeiPlatformOrderNo(createTask({
|
||||
|
||||
@@ -7,17 +7,14 @@ import { logIntegration } from '../../../utils/logger.js'
|
||||
import { parseTaskContext } from '../../../utils/task-json.js'
|
||||
import { nowIso } from '../../../utils/time.js'
|
||||
import { TASK_STATUS } from '../../../domain/task-status.js'
|
||||
import { appendUidToUrl, getClaimIdentityFromContext } from '../../claim/claim-identity.js'
|
||||
import {
|
||||
createKuaishouFeifeiOrder,
|
||||
queryKuaishouFeifeiOrder,
|
||||
} from '../../platforms/kuaishou-feifei/order-service.js'
|
||||
import { consumeKuaishouIndustryVouchersForTask } from '../../platforms/kuaishou-industry/voucher-service.js'
|
||||
import { buildKuaishouIndustryVoucherContext } from '../../platforms/kuaishou-industry/voucher-binding-service.js'
|
||||
import {
|
||||
SHORT_LINK_SOURCE_KUAISHOU_FEIFEI_CLAIM,
|
||||
ensureShortLinkForTarget,
|
||||
normalizeShortLinkPayload,
|
||||
} from '../../short-links/short-link-service.js'
|
||||
import { normalizeShortLinkPayload } from '../../short-links/short-link-service.js'
|
||||
import type { TaskRow } from '../../../types/repository/rows.js'
|
||||
|
||||
type JsonObject = Record<string, any>
|
||||
@@ -40,15 +37,13 @@ export async function prepareKuaishouFeifeiTask(task: TaskRow) {
|
||||
|
||||
const existingClaimUrl = resolveKuaishouFeifeiClaimUrl(flow)
|
||||
if (flow.orderNo && existingClaimUrl) {
|
||||
const nextFlow = await ensureKuaishouFeifeiFlowShortLink(flow, task)
|
||||
const nextContext = {
|
||||
...taskContext,
|
||||
kuaishouFeifei: nextFlow,
|
||||
}
|
||||
return updateTask(task.id, {
|
||||
task_status: TASK_STATUS.LINK_GENERATED,
|
||||
user_action_status: 'pending_claim',
|
||||
context_json: JSON.stringify(nextContext),
|
||||
context_json: JSON.stringify({
|
||||
...taskContext,
|
||||
kuaishouFeifei: flow,
|
||||
}),
|
||||
updated_at: now,
|
||||
})
|
||||
}
|
||||
@@ -80,11 +75,11 @@ export async function prepareKuaishouFeifeiTask(task: TaskRow) {
|
||||
})
|
||||
}
|
||||
|
||||
const nextFlow = await ensureKuaishouFeifeiFlowShortLink(mergeKuaishouFeifeiOrder(flow, order, {
|
||||
const nextFlow = mergeKuaishouFeifeiOrder(flow, order, {
|
||||
platformOrderNo,
|
||||
claimUrl: feifeiClaimUrl,
|
||||
syncedAt: now,
|
||||
}), task)
|
||||
})
|
||||
const updatedTask = await updateTask(task.id, {
|
||||
task_status: TASK_STATUS.LINK_GENERATED,
|
||||
user_action_status: 'pending_claim',
|
||||
@@ -137,10 +132,10 @@ export async function syncKuaishouFeifeiTaskStatus(task: TaskRow) {
|
||||
let resultCode = task.result_code
|
||||
let resultMessage = task.result_message
|
||||
let lastError = task.last_error
|
||||
const nextFlow = await ensureKuaishouFeifeiFlowShortLink(mergeKuaishouFeifeiOrder(flow, order, {
|
||||
const nextFlow = mergeKuaishouFeifeiOrder(flow, order, {
|
||||
syncedAt: now,
|
||||
claimUrl: resolveKuaishouFeifeiOrderClaimUrl(order) || resolveKuaishouFeifeiClaimUrl(flow),
|
||||
}), task)
|
||||
})
|
||||
let nextIndustryVoucher = taskContext.kuaishouIndustryVoucher
|
||||
|
||||
if (order.rechargeStatus === 30) {
|
||||
@@ -230,6 +225,7 @@ export function normalizeKuaishouFeifeiFlow(value: unknown) {
|
||||
rechargeResultMessage: String(source.rechargeResultMessage || '').trim(),
|
||||
claimUrl: String(source.claimUrl || '').trim(),
|
||||
consumeStatus: String(source.consumeStatus || 'pending').trim(),
|
||||
// 历史字段保留读取,新流程不再生成 shortLink
|
||||
shortLink: normalizeShortLinkPayload(source.shortLink),
|
||||
h5: {
|
||||
entryUrl: String(h5.entryUrl || '').trim(),
|
||||
@@ -240,38 +236,38 @@ export function normalizeKuaishouFeifeiFlow(value: unknown) {
|
||||
}
|
||||
}
|
||||
|
||||
/** 平台原始 H5 链接(不拼 uid、不用短链)。 */
|
||||
export function resolveKuaishouFeifeiClaimUrl(value: unknown) {
|
||||
const flow = normalizeKuaishouFeifeiFlow(value)
|
||||
if (flow.shortLink?.url) {
|
||||
return flow.shortLink.url
|
||||
}
|
||||
|
||||
return resolveKuaishouFeifeiDirectClaimUrl(flow)
|
||||
}
|
||||
|
||||
export async function ensureKuaishouFeifeiClaimShortLink(task: TaskRow) {
|
||||
if (!isKuaishouFeifeiTask(task)) {
|
||||
/** 在已有 expectedUid 时返回拼好 uid 的 H5 链接。 */
|
||||
export function resolveKuaishouFeifeiH5UrlWithUid(
|
||||
value: unknown,
|
||||
expectedUid?: unknown,
|
||||
) {
|
||||
const directUrl = resolveKuaishouFeifeiClaimUrl(value)
|
||||
if (!directUrl) {
|
||||
return ''
|
||||
}
|
||||
|
||||
const taskContext = parseTaskContext(task)
|
||||
const flow = normalizeKuaishouFeifeiFlow(taskContext.kuaishouFeifei)
|
||||
const nextFlow = await ensureKuaishouFeifeiFlowShortLink(flow, task)
|
||||
const claimUrl = resolveKuaishouFeifeiClaimUrl(nextFlow)
|
||||
|
||||
if (JSON.stringify(flow.shortLink || null) === JSON.stringify(nextFlow.shortLink || null)) {
|
||||
return claimUrl
|
||||
const uid = String(expectedUid || '').trim()
|
||||
if (!uid) {
|
||||
return directUrl
|
||||
}
|
||||
|
||||
await updateTask(task.id, {
|
||||
context_json: JSON.stringify({
|
||||
...taskContext,
|
||||
kuaishouFeifei: nextFlow,
|
||||
}),
|
||||
updated_at: nowIso(),
|
||||
})
|
||||
return appendUidToUrl(directUrl, uid)
|
||||
}
|
||||
|
||||
return claimUrl
|
||||
export function resolveKuaishouFeifeiH5UrlWithTaskUid(
|
||||
task: Partial<TaskRow> | null | undefined,
|
||||
flowValue?: unknown,
|
||||
) {
|
||||
const context = parseTaskContext(task)
|
||||
const identity = getClaimIdentityFromContext(context)
|
||||
const flow = flowValue === undefined ? context.kuaishouFeifei : flowValue
|
||||
return resolveKuaishouFeifeiH5UrlWithUid(flow, identity.expectedUid)
|
||||
}
|
||||
|
||||
function mergeKuaishouFeifeiOrder(
|
||||
@@ -303,30 +299,6 @@ function mergeKuaishouFeifeiOrder(
|
||||
}
|
||||
}
|
||||
|
||||
async function ensureKuaishouFeifeiFlowShortLink(
|
||||
flow: ReturnType<typeof normalizeKuaishouFeifeiFlow>,
|
||||
task: TaskRow,
|
||||
) {
|
||||
const targetUrl = resolveKuaishouFeifeiDirectClaimUrl(flow)
|
||||
if (!targetUrl) {
|
||||
return flow
|
||||
}
|
||||
|
||||
if (flow.shortLink?.url && flow.shortLink.targetUrl === targetUrl) {
|
||||
return flow
|
||||
}
|
||||
|
||||
const shortLink = await ensureShortLinkForTarget(targetUrl, {
|
||||
source: SHORT_LINK_SOURCE_KUAISHOU_FEIFEI_CLAIM,
|
||||
taskId: task.id,
|
||||
})
|
||||
|
||||
return {
|
||||
...flow,
|
||||
shortLink,
|
||||
}
|
||||
}
|
||||
|
||||
function resolveKuaishouFeifeiDirectClaimUrl(
|
||||
flow: ReturnType<typeof normalizeKuaishouFeifeiFlow>,
|
||||
) {
|
||||
@@ -335,6 +307,7 @@ function resolveKuaishouFeifeiDirectClaimUrl(
|
||||
return h5ClaimUrl
|
||||
}
|
||||
|
||||
// 兼容历史 shortLink 中的原始 target
|
||||
if (flow.shortLink?.targetUrl) {
|
||||
return flow.shortLink.targetUrl
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user