统一领取流:Step1 填 UID,按平台分叉履约
领取页改为统一 UID 入口;lewan 强制角色 ID 匹配后才发货核销,feifei 将 uid 拼入 H5 并去掉 claim 短链,对外仍返回本站 claimUrl。
This commit is contained in:
@@ -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]'
|
||||
}
|
||||
Reference in New Issue
Block a user