统一领取流:Step1 填 UID,按平台分叉履约
领取页改为统一 UID 入口;lewan 强制角色 ID 匹配后才发货核销,feifei 将 uid 拼入 H5 并去掉 claim 短链,对外仍返回本站 claimUrl。
This commit is contained in:
@@ -83,11 +83,18 @@ function assertMigrationFilesOrdered() {
|
||||
|
||||
const versions = files.map((name) => Number(name.slice(0, 3)))
|
||||
for (let index = 1; index < versions.length; index += 1) {
|
||||
if (versions[index] < versions[index - 1]) {
|
||||
throw new Error(`迁移序号乱序: ${files[index - 1]} 之后出现 ${files[index]}`)
|
||||
const current = versions[index]
|
||||
const previous = versions[index - 1]
|
||||
const currentFile = files[index]
|
||||
const previousFile = files[index - 1]
|
||||
if (current === undefined || previous === undefined || !currentFile || !previousFile) {
|
||||
continue
|
||||
}
|
||||
if (versions[index] === versions[index - 1]) {
|
||||
throw new Error(`迁移序号重复: ${files[index - 1]} 与 ${files[index]}`)
|
||||
if (current < previous) {
|
||||
throw new Error(`迁移序号乱序: ${previousFile} 之后出现 ${currentFile}`)
|
||||
}
|
||||
if (current === previous) {
|
||||
throw new Error(`迁移序号重复: ${previousFile} 与 ${currentFile}`)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
getKuaishouCloudClaimDetail,
|
||||
rebindKuaishouCloudClaimRole,
|
||||
redeemKuaishouCloudClaim,
|
||||
submitClaimUid,
|
||||
} from '../services/claim/kuaishou-cloud-claim-service.js'
|
||||
import { buildNotFoundPayload, createRouteHandler } from '../utils/http.js'
|
||||
|
||||
@@ -32,6 +33,16 @@ router.get(
|
||||
}),
|
||||
)
|
||||
|
||||
router.post(
|
||||
'/:token/uid',
|
||||
claimWriteRateLimit,
|
||||
createRouteHandler((req) => submitClaimUid(req.params.token, req.body || {}), {
|
||||
successMessage: 'UID 已提交',
|
||||
errorMessage: '提交 UID 失败',
|
||||
scope: '[claims/:token/uid]',
|
||||
}),
|
||||
)
|
||||
|
||||
router.post(
|
||||
'/:token/kuaishou-cloud/confirm-role',
|
||||
claimWriteRateLimit,
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
listKuaishouIndustryVouchersByTaskId,
|
||||
} from '../../repositories/kuaishou-industry-voucher-repo.js'
|
||||
import { listCloudtentaclesSources } from '../platforms/cloudtentacles/source-config-service.js'
|
||||
import { resolveKuaishouFeifeiClaimUrl } from '../fulfillment/kuaishou-feifei/index.js'
|
||||
|
||||
import { createHttpError } from '../../utils/http.js'
|
||||
import { formatFenToAmount, normalizeFen } from '../../utils/money.js'
|
||||
import {
|
||||
@@ -242,10 +242,8 @@ export async function getAdminTaskDetail(
|
||||
const taskContext = parseTaskContext(task)
|
||||
const taskState = parseTaskState(task)
|
||||
const viewerContext = createAdminViewerContext(session)
|
||||
const directClaimUrl = String(task.executor_key || '').trim() === 'kuaishou_feifei'
|
||||
? resolveKuaishouFeifeiClaimUrl(taskContext.kuaishouFeifei)
|
||||
: ''
|
||||
const claimUrl = directClaimUrl || (claimToken ? buildClaimUrl(claimToken.token) : '')
|
||||
// 统一对外领取入口为本站 claim 链接;平台 H5 仅作内部上下文
|
||||
const claimUrl = claimToken ? buildClaimUrl(claimToken.token) : ''
|
||||
const screenshotUrl = await resolveAdminTaskScreenshotUrl(task, viewerContext)
|
||||
const cloudSourceLabelMap = buildCloudSourceLabelMap()
|
||||
const kuaishouCloudFulfillment =
|
||||
@@ -300,12 +298,12 @@ export async function getAdminTaskDetail(
|
||||
quantity: orderItem.quantity,
|
||||
}
|
||||
: null,
|
||||
claimToken: claimToken || directClaimUrl
|
||||
claimToken: claimToken
|
||||
? {
|
||||
primaryClaimTokenId: claimToken?.id || 0,
|
||||
token: viewerContext.canViewSensitiveTaskData ? (claimToken?.token || '') : '',
|
||||
status: claimToken?.status || 'external',
|
||||
expiredAt: claimToken?.expired_at || '',
|
||||
primaryClaimTokenId: claimToken.id,
|
||||
token: viewerContext.canViewSensitiveTaskData ? claimToken.token || '' : '',
|
||||
status: claimToken.status || '',
|
||||
expiredAt: claimToken.expired_at || '',
|
||||
claimUrl,
|
||||
}
|
||||
: null,
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import test from 'node:test'
|
||||
|
||||
import {
|
||||
appendUidToUrl,
|
||||
assertValidClaimUid,
|
||||
isClaimUidMatched,
|
||||
normalizeClaimUid,
|
||||
} from './claim-identity.js'
|
||||
|
||||
test('normalizeClaimUid trims and removes spaces', () => {
|
||||
assert.equal(normalizeClaimUid(' 166 909 256 '), '166909256')
|
||||
})
|
||||
|
||||
test('assertValidClaimUid accepts common game uid', () => {
|
||||
assert.equal(assertValidClaimUid('166909256'), '166909256')
|
||||
})
|
||||
|
||||
test('assertValidClaimUid rejects empty value', () => {
|
||||
assert.throws(() => assertValidClaimUid(''), /请输入游戏 UID/)
|
||||
})
|
||||
|
||||
test('isClaimUidMatched compares normalized values', () => {
|
||||
assert.equal(isClaimUidMatched('166909256', '166909256'), true)
|
||||
assert.equal(isClaimUidMatched('166909256', '999'), false)
|
||||
})
|
||||
|
||||
test('appendUidToUrl sets uid query param', () => {
|
||||
const source =
|
||||
'http://skin-exchange.yiquyou.icu/h5/bind?code=eyJpdi&product_name=%E5%A5%97%E8%A3%85'
|
||||
const next = appendUidToUrl(source, '166909256')
|
||||
const url = new URL(next)
|
||||
assert.equal(url.searchParams.get('uid'), '166909256')
|
||||
assert.equal(url.searchParams.get('code'), 'eyJpdi')
|
||||
})
|
||||
|
||||
test('appendUidToUrl overwrites existing uid', () => {
|
||||
const next = appendUidToUrl('https://example.com/h5/bind?code=abc&uid=old', 'new-uid')
|
||||
assert.equal(new URL(next).searchParams.get('uid'), 'new-uid')
|
||||
})
|
||||
@@ -0,0 +1,199 @@
|
||||
import { createHttpError } from '../../utils/http.js'
|
||||
import { parseTaskContext } from '../../utils/task-json.js'
|
||||
import { isTaskFinalStatus, normalizeTaskStatus, TASK_STATUS } from '../../domain/task-status.js'
|
||||
import type { TaskRow } from '../../types/repository/rows.js'
|
||||
|
||||
export type ClaimIdentity = {
|
||||
expectedUid: string
|
||||
submittedAt: string | null
|
||||
source: string
|
||||
}
|
||||
|
||||
type JsonObject = Record<string, any>
|
||||
|
||||
const CLAIM_UID_MAX_LENGTH = 64
|
||||
|
||||
export function normalizeClaimUid(value: unknown): string {
|
||||
return String(value || '')
|
||||
.trim()
|
||||
.replace(/\s+/g, '')
|
||||
}
|
||||
|
||||
export function assertValidClaimUid(value: unknown): string {
|
||||
const uid = normalizeClaimUid(value)
|
||||
if (!uid) {
|
||||
throw createHttpError('请输入游戏 UID', {
|
||||
statusCode: 400,
|
||||
errorCode: 'claim_uid_required',
|
||||
})
|
||||
}
|
||||
|
||||
if (uid.length > CLAIM_UID_MAX_LENGTH) {
|
||||
throw createHttpError(`UID 长度不能超过 ${CLAIM_UID_MAX_LENGTH} 个字符`, {
|
||||
statusCode: 400,
|
||||
errorCode: 'claim_uid_too_long',
|
||||
})
|
||||
}
|
||||
|
||||
if (!/^[A-Za-z0-9_\-]+$/.test(uid)) {
|
||||
throw createHttpError('UID 仅支持字母、数字、下划线和中划线', {
|
||||
statusCode: 400,
|
||||
errorCode: 'claim_uid_invalid',
|
||||
})
|
||||
}
|
||||
|
||||
return uid
|
||||
}
|
||||
|
||||
export function normalizeClaimIdentity(value: unknown): ClaimIdentity {
|
||||
const source = isPlainObject(value) ? value : {}
|
||||
return {
|
||||
expectedUid: normalizeClaimUid(source.expectedUid || source.uid),
|
||||
submittedAt: source.submittedAt ? String(source.submittedAt) : null,
|
||||
source: String(source.source || '').trim(),
|
||||
}
|
||||
}
|
||||
|
||||
export function getClaimIdentityFromContext(context: unknown): ClaimIdentity {
|
||||
const source = isPlainObject(context) ? context : {}
|
||||
return normalizeClaimIdentity(source.claimIdentity)
|
||||
}
|
||||
|
||||
export function getClaimIdentityFromTask(task: Partial<TaskRow> | null | undefined): ClaimIdentity {
|
||||
return getClaimIdentityFromContext(parseTaskContext(task))
|
||||
}
|
||||
|
||||
export function hasClaimExpectedUid(taskOrContext: unknown): boolean {
|
||||
if (taskOrContext && typeof taskOrContext === 'object' && 'context_json' in (taskOrContext as object)) {
|
||||
return Boolean(getClaimIdentityFromTask(taskOrContext as TaskRow).expectedUid)
|
||||
}
|
||||
return Boolean(getClaimIdentityFromContext(taskOrContext).expectedUid)
|
||||
}
|
||||
|
||||
export function isClaimUidMatched(expectedUid: unknown, boundUid: unknown): boolean {
|
||||
const expected = normalizeClaimUid(expectedUid)
|
||||
const bound = normalizeClaimUid(boundUid)
|
||||
if (!expected || !bound) {
|
||||
return false
|
||||
}
|
||||
return expected === bound
|
||||
}
|
||||
|
||||
export function resolveBoundRoleUid(flowLike: unknown): string {
|
||||
const flow = isPlainObject(flowLike) ? flowLike : {}
|
||||
const binding = isPlainObject(flow.binding) ? flow.binding : {}
|
||||
const role = isPlainObject(flow.role) ? flow.role : {}
|
||||
return normalizeClaimUid(
|
||||
binding.roleId || role.rid || role.roleId || binding.uid || role.uid || '',
|
||||
)
|
||||
}
|
||||
|
||||
export function assertClaimExpectedUidReady(task: Partial<TaskRow> | null | undefined): string {
|
||||
const expectedUid = getClaimIdentityFromTask(task).expectedUid
|
||||
if (!expectedUid) {
|
||||
throw createHttpError('请先填写游戏 UID', {
|
||||
statusCode: 409,
|
||||
errorCode: 'claim_uid_not_submitted',
|
||||
})
|
||||
}
|
||||
return expectedUid
|
||||
}
|
||||
|
||||
export function assertBoundUidMatchesExpected(
|
||||
task: Partial<TaskRow> | null | undefined,
|
||||
flowLike: unknown,
|
||||
options: { errorCodePrefix?: string } = {},
|
||||
): string {
|
||||
const expectedUid = assertClaimExpectedUidReady(task)
|
||||
const boundUid = resolveBoundRoleUid(flowLike)
|
||||
const prefix = String(options.errorCodePrefix || 'claim').trim() || 'claim'
|
||||
|
||||
if (!boundUid) {
|
||||
throw createHttpError('尚未识别到绑定角色 ID,请完成绑定后刷新再试', {
|
||||
statusCode: 409,
|
||||
errorCode: `${prefix}_bound_uid_missing`,
|
||||
})
|
||||
}
|
||||
|
||||
if (!isClaimUidMatched(expectedUid, boundUid)) {
|
||||
throw createHttpError(
|
||||
`绑定角色 ID(${boundUid})与填写的 UID(${expectedUid})不一致,请重新绑定正确角色`,
|
||||
{
|
||||
statusCode: 409,
|
||||
errorCode: `${prefix}_uid_mismatch`,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
return expectedUid
|
||||
}
|
||||
|
||||
export function canUpdateClaimUid(task: Partial<TaskRow> | null | undefined): boolean {
|
||||
const status = normalizeTaskStatus(task?.task_status)
|
||||
if (isTaskFinalStatus(status)) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (
|
||||
status === TASK_STATUS.REDEEMING ||
|
||||
status === TASK_STATUS.DISPATCHED_PENDING_RETURN ||
|
||||
status === TASK_STATUS.COMPLETED ||
|
||||
status === TASK_STATUS.REDEEMED
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
const context = parseTaskContext(task)
|
||||
const flow = isPlainObject(context.kuaishouCloudFulfillment)
|
||||
? context.kuaishouCloudFulfillment
|
||||
: {}
|
||||
const dispatch = isPlainObject(flow.dispatch) ? flow.dispatch : {}
|
||||
if (String(dispatch.status || '').trim() === 'success') {
|
||||
return false
|
||||
}
|
||||
|
||||
const feifei = isPlainObject(context.kuaishouFeifei) ? context.kuaishouFeifei : {}
|
||||
const rechargeStatus = Number(feifei.rechargeStatus || 0) || 0
|
||||
if (rechargeStatus === 30) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 uid 拼到目标 H5 链接 query 中(覆盖已有 uid 参数)。
|
||||
*/
|
||||
export function appendUidToUrl(targetUrl: unknown, uid: unknown): string {
|
||||
const rawUrl = String(targetUrl || '').trim()
|
||||
const normalizedUid = normalizeClaimUid(uid)
|
||||
if (!rawUrl || !normalizedUid) {
|
||||
return rawUrl
|
||||
}
|
||||
|
||||
try {
|
||||
const url = new URL(rawUrl)
|
||||
url.searchParams.set('uid', normalizedUid)
|
||||
return url.toString()
|
||||
} catch {
|
||||
const separator = rawUrl.includes('?') ? '&' : '?'
|
||||
if (/([?&])uid=/i.test(rawUrl)) {
|
||||
return rawUrl.replace(/([?&])uid=[^&]*/i, `$1uid=${encodeURIComponent(normalizedUid)}`)
|
||||
}
|
||||
return `${rawUrl}${separator}uid=${encodeURIComponent(normalizedUid)}`
|
||||
}
|
||||
}
|
||||
|
||||
export function buildClaimIdentityPayload(value: unknown) {
|
||||
const identity = normalizeClaimIdentity(value)
|
||||
return {
|
||||
expectedUid: identity.expectedUid,
|
||||
submittedAt: identity.submittedAt,
|
||||
source: identity.source,
|
||||
ready: Boolean(identity.expectedUid),
|
||||
}
|
||||
}
|
||||
|
||||
function isPlainObject(value: unknown): value is JsonObject {
|
||||
return Object.prototype.toString.call(value) === '[object Object]'
|
||||
}
|
||||
@@ -8,6 +8,8 @@ import { parseTaskContext as parseTaskContextValue } from '../../utils/task-json
|
||||
import { nowIso } from '../../utils/time.js'
|
||||
import { TASK_STATUS, isTaskFinalStatus } from '../../domain/task-status.js'
|
||||
import { buildClaimUrl } from './claim-service.js'
|
||||
import { buildClaimIdentityPayload, getClaimIdentityFromContext } from './claim-identity.js'
|
||||
import { resolveKuaishouFeifeiH5UrlWithUid } from '../fulfillment/kuaishou-feifei/index.js'
|
||||
import type { ClaimTokenRow, OrderItemRow, OrderRow, TaskRow } from '../../types/repository/rows.js'
|
||||
|
||||
export const CLAIM_TERMINAL_STATUSES = new Set([TASK_STATUS.EXPIRED, TASK_STATUS.CLOSED])
|
||||
@@ -89,6 +91,8 @@ export function buildClaimDetailPayload({ claimToken, task, order, orderItem }:
|
||||
return buildKuaishouFeifeiClaimDetailPayload({ claimToken, task, order, orderItem })
|
||||
}
|
||||
|
||||
const taskContext = parseTaskContext(task)
|
||||
const claimIdentity = buildClaimIdentityPayload(taskContext.claimIdentity)
|
||||
const kuaishouCloudSource = resolveClaimKuaishouCloudSource(task)
|
||||
const kuaishouCloudFulfillment = mapClaimKuaishouCloudFulfillment(task, order)
|
||||
const displaySkuName = resolveClaimOrderItemDisplaySkuName(
|
||||
@@ -102,6 +106,7 @@ export function buildClaimDetailPayload({ claimToken, task, order, orderItem }:
|
||||
tokenStatus: claimToken.status,
|
||||
claimUrl: buildClaimUrl(claimToken.token),
|
||||
flowType: 'kuaishou_cloud',
|
||||
claimIdentity,
|
||||
task: {
|
||||
taskId: task.id,
|
||||
taskNo: task.task_no,
|
||||
@@ -149,7 +154,9 @@ export function buildClaimDetailPayload({ claimToken, task, order, orderItem }:
|
||||
|
||||
function buildKuaishouFeifeiClaimDetailPayload({ claimToken, task, order, orderItem }: ClaimContext) {
|
||||
const context = parseTaskContext(task)
|
||||
const flow = mapClaimKuaishouFeifeiFulfillment(context.kuaishouFeifei)
|
||||
const claimIdentity = buildClaimIdentityPayload(context.claimIdentity)
|
||||
const expectedUid = getClaimIdentityFromContext(context).expectedUid
|
||||
const flow = mapClaimKuaishouFeifeiFulfillment(context.kuaishouFeifei, expectedUid)
|
||||
const product = {
|
||||
title: String(flow.productName || orderItem.sku_name || orderItem.sku_code || '').trim(),
|
||||
skuCode: String(orderItem.sku_code || '').trim(),
|
||||
@@ -166,6 +173,7 @@ function buildKuaishouFeifeiClaimDetailPayload({ claimToken, task, order, orderI
|
||||
tokenStatus: claimToken.status,
|
||||
claimUrl: buildClaimUrl(claimToken.token),
|
||||
flowType: 'kuaishou_feifei',
|
||||
claimIdentity,
|
||||
task: {
|
||||
taskId: task.id,
|
||||
taskNo: task.task_no,
|
||||
@@ -211,9 +219,12 @@ function buildKuaishouFeifeiClaimDetailPayload({ claimToken, task, order, orderI
|
||||
}
|
||||
}
|
||||
|
||||
function mapClaimKuaishouFeifeiFulfillment(value: unknown) {
|
||||
function mapClaimKuaishouFeifeiFulfillment(value: unknown, expectedUid = '') {
|
||||
const source = isPlainObject(value) ? value : {}
|
||||
const h5 = isPlainObject(source.h5) ? source.h5 : {}
|
||||
const entryUrl = String(h5.entryUrl || '').trim()
|
||||
const rechargeUrl = String(h5.rechargeUrl || '').trim()
|
||||
const h5UrlWithUid = resolveKuaishouFeifeiH5UrlWithUid(source, expectedUid)
|
||||
|
||||
return {
|
||||
flowType: 'kuaishou_feifei',
|
||||
@@ -227,9 +238,10 @@ function mapClaimKuaishouFeifeiFulfillment(value: unknown) {
|
||||
claimUrl: String(source.claimUrl || '').trim(),
|
||||
consumeStatus: String(source.consumeStatus || 'pending').trim(),
|
||||
h5: {
|
||||
entryUrl: String(h5.entryUrl || '').trim(),
|
||||
rechargeUrl: String(h5.rechargeUrl || '').trim(),
|
||||
entryUrl,
|
||||
rechargeUrl,
|
||||
},
|
||||
h5UrlWithUid: expectedUid ? h5UrlWithUid : '',
|
||||
lastSyncedAt: source.lastSyncedAt || null,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { createTaskEvent } from '../../repositories/task-event-repo.js'
|
||||
import { getTaskById, updateTask, updateTaskStatusIfCurrent } from '../../repositories/task-repo.js'
|
||||
import { createHttpError } from '../../utils/http.js'
|
||||
import { parseTaskContext as parseTaskContextValue } from '../../utils/task-json.js'
|
||||
import { addHours, nowIso } from '../../utils/time.js'
|
||||
import {
|
||||
TASK_STATUS,
|
||||
@@ -11,14 +12,19 @@ import {
|
||||
} from '../../domain/task-status.js'
|
||||
import {
|
||||
dispatchKuaishouCloudFulfillmentTask,
|
||||
hasKuaishouCloudCustomerRole,
|
||||
hasKuaishouCloudDefaultRoleSnapshot,
|
||||
normalizeKuaishouCloudFlow,
|
||||
prepareKuaishouCloudFulfillmentTask,
|
||||
rebindKuaishouCloudTaskRole,
|
||||
refreshKuaishouCloudTaskRoleInfo,
|
||||
} from '../fulfillment/kuaishou-cloud/index.js'
|
||||
import { syncKuaishouFeifeiTaskStatus } from '../fulfillment/kuaishou-feifei/index.js'
|
||||
import {
|
||||
assertBoundUidMatchesExpected,
|
||||
assertClaimExpectedUidReady,
|
||||
assertValidClaimUid,
|
||||
canUpdateClaimUid,
|
||||
getClaimIdentityFromTask,
|
||||
} from './claim-identity.js'
|
||||
import { buildClaimDetailPayload, getClaimContext } from './kuaishou-cloud-claim-context.js'
|
||||
import { syncKuaishouCloudRoleInfo } from './kuaishou-cloud-sync-service.js'
|
||||
import type { TaskRow } from '../../types/repository/rows.js'
|
||||
@@ -179,9 +185,11 @@ export async function getKuaishouCloudClaimDetail(token: unknown): Promise<Claim
|
||||
}
|
||||
}
|
||||
|
||||
// 已提交 UID 的 lewan 任务:详情查询时顺带刷新角色,便于前端轮询匹配状态
|
||||
if (
|
||||
executorKey === 'kuaishou_ct_assisted' &&
|
||||
!isKuaishouCloudMockTask(task)
|
||||
!isKuaishouCloudMockTask(task) &&
|
||||
getClaimIdentityFromTask(task).expectedUid
|
||||
) {
|
||||
task = (await syncKuaishouCloudRoleInfo(task)) || task
|
||||
}
|
||||
@@ -194,6 +202,81 @@ export async function getKuaishouCloudClaimDetail(token: unknown): Promise<Claim
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 统一领取 Step1:提交游戏 UID。
|
||||
*/
|
||||
export async function submitClaimUid(
|
||||
token: unknown,
|
||||
payload: { uid?: unknown } = {},
|
||||
): Promise<ClaimDetailPayload> {
|
||||
const context = await getClaimContext(token)
|
||||
const expectedUid = assertValidClaimUid(payload.uid)
|
||||
const now = nowIso()
|
||||
|
||||
if (!canUpdateClaimUid(context.task)) {
|
||||
throw createHttpError('当前任务状态不可修改 UID', {
|
||||
statusCode: 409,
|
||||
errorCode: 'claim_uid_locked',
|
||||
})
|
||||
}
|
||||
|
||||
const taskContext = parseTaskContext(context.task)
|
||||
const previous = getClaimIdentityFromTask(context.task)
|
||||
const nextContext = {
|
||||
...taskContext,
|
||||
claimIdentity: {
|
||||
expectedUid,
|
||||
submittedAt: previous.expectedUid === expectedUid && previous.submittedAt
|
||||
? previous.submittedAt
|
||||
: now,
|
||||
source: 'claim_page',
|
||||
},
|
||||
}
|
||||
|
||||
let updatedTask =
|
||||
(await updateTask(context.task.id, {
|
||||
context_json: JSON.stringify(nextContext),
|
||||
claimed_at: context.task.claimed_at || now,
|
||||
last_error: '',
|
||||
updated_at: now,
|
||||
})) || {
|
||||
...context.task,
|
||||
context_json: JSON.stringify(nextContext),
|
||||
claimed_at: context.task.claimed_at || now,
|
||||
updated_at: now,
|
||||
}
|
||||
|
||||
if (previous.expectedUid !== expectedUid) {
|
||||
await createTaskEvent(
|
||||
context.task.id,
|
||||
'claim_uid_submitted',
|
||||
{
|
||||
expectedUid,
|
||||
previousUid: previous.expectedUid || '',
|
||||
source: 'claim_page',
|
||||
},
|
||||
now,
|
||||
)
|
||||
}
|
||||
|
||||
const executorKey = String(updatedTask.executor_key || '').trim()
|
||||
if (executorKey === 'kuaishou_ct_assisted' && !isKuaishouCloudMockTask(updatedTask)) {
|
||||
try {
|
||||
const prepared = await prepareKuaishouCloudFulfillmentTask(updatedTask, {
|
||||
source: 'claim_page_submit_uid',
|
||||
actor: { source: 'claim_page' },
|
||||
})
|
||||
if (prepared?.task) {
|
||||
updatedTask = prepared.task
|
||||
}
|
||||
} catch {
|
||||
// 绑定资源准备失败时仍保留 UID,详情页可继续重试/轮询
|
||||
}
|
||||
}
|
||||
|
||||
return getKuaishouCloudClaimDetail(token)
|
||||
}
|
||||
|
||||
export async function rebindKuaishouCloudClaimRole(token: unknown) {
|
||||
const context = await getClaimContext(token)
|
||||
|
||||
@@ -213,20 +296,7 @@ export async function rebindKuaishouCloudClaimRole(token: unknown) {
|
||||
}
|
||||
|
||||
function parseTaskContext(task: Partial<TaskRow> | null | undefined): JsonObject {
|
||||
const rawValue = task?.context_json
|
||||
if (!rawValue) {
|
||||
return {}
|
||||
}
|
||||
|
||||
if (typeof rawValue === 'object') {
|
||||
return rawValue
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.parse(String(rawValue || '{}'))
|
||||
} catch {
|
||||
return {}
|
||||
}
|
||||
return parseTaskContextValue(task)
|
||||
}
|
||||
|
||||
function hasUsableIndustryVoucher(context: JsonObject = {}) {
|
||||
@@ -273,6 +343,7 @@ export async function confirmKuaishouCloudClaimRole(token: unknown) {
|
||||
return getKuaishouCloudClaimDetail(token)
|
||||
}
|
||||
|
||||
const expectedUid = assertClaimExpectedUidReady(context.task)
|
||||
const contextSource = parseTaskContext(context.task)
|
||||
const mockMode = isKuaishouCloudMockContext(contextSource)
|
||||
const refreshed = mockMode
|
||||
@@ -285,13 +356,6 @@ export async function confirmKuaishouCloudClaimRole(token: unknown) {
|
||||
})
|
||||
const flow = normalizeKuaishouCloudFlow(parseTaskContext(refreshed.task).kuaishouCloudFulfillment)
|
||||
|
||||
if (flow.ticket.status !== 'verified') {
|
||||
throw createHttpError('请先完成电子凭证确认', {
|
||||
statusCode: 409,
|
||||
errorCode: 'claim_kuaishou_cloud_ticket_not_verified',
|
||||
})
|
||||
}
|
||||
|
||||
if (!flow.binding.vnPhone || !flow.binding.roleName || !flow.binding.roleId) {
|
||||
throw createHttpError('角色信息还未刷新到系统,请完成绑定后稍等片刻再试', {
|
||||
statusCode: 409,
|
||||
@@ -299,16 +363,9 @@ export async function confirmKuaishouCloudClaimRole(token: unknown) {
|
||||
})
|
||||
}
|
||||
|
||||
const hasDefaultRoleSnapshot = hasKuaishouCloudDefaultRoleSnapshot(flow)
|
||||
if (!mockMode && !hasKuaishouCloudCustomerRole(flow)) {
|
||||
const message = hasDefaultRoleSnapshot
|
||||
? '当前仍是虚拟机默认角色,请先重新绑定自己的角色信息'
|
||||
: '系统还未获取到虚拟机默认角色信息,请稍后刷新角色信息后再试'
|
||||
throw createHttpError(message, {
|
||||
statusCode: 409,
|
||||
errorCode: hasDefaultRoleSnapshot
|
||||
? 'claim_kuaishou_cloud_default_role_not_rebound'
|
||||
: 'claim_kuaishou_cloud_default_role_not_ready',
|
||||
if (!mockMode) {
|
||||
assertBoundUidMatchesExpected(refreshed.task, flow, {
|
||||
errorCodePrefix: 'claim_kuaishou_cloud',
|
||||
})
|
||||
}
|
||||
|
||||
@@ -326,6 +383,7 @@ export async function confirmKuaishouCloudClaimRole(token: unknown) {
|
||||
context.task.id,
|
||||
'kuaishou_cloud_role_confirmed',
|
||||
{
|
||||
expectedUid,
|
||||
vnPhone: flow.binding.vnPhone,
|
||||
roleName: flow.binding.roleName,
|
||||
roleId: flow.binding.roleId,
|
||||
@@ -359,6 +417,15 @@ export async function redeemKuaishouCloudClaim(token: unknown) {
|
||||
})
|
||||
}
|
||||
|
||||
const flowBeforeRedeem = normalizeKuaishouCloudFlow(
|
||||
parseTaskContext(context.task).kuaishouCloudFulfillment,
|
||||
)
|
||||
if (!isKuaishouCloudMockTask(context.task)) {
|
||||
assertBoundUidMatchesExpected(context.task, flowBeforeRedeem, {
|
||||
errorCodePrefix: 'claim_kuaishou_cloud_redeem',
|
||||
})
|
||||
}
|
||||
|
||||
const lockedTask = await updateTaskStatusIfCurrent(context.task.id, TASK_STATUS.ROLE_CONFIRMED, {
|
||||
task_status: TASK_STATUS.REDEEMING,
|
||||
user_action_status: 'not_required',
|
||||
|
||||
@@ -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