客户领取页 ui与逻辑优化

This commit is contained in:
yml
2026-05-04 01:53:35 +08:00
parent dd842c165a
commit a914b7b828
15 changed files with 2124 additions and 349 deletions
@@ -23,6 +23,7 @@ import {
import { reserveInventoryForTask } from '../order/inventory-service.js'
import { buildClaimUrl } from './claim-service.js'
import { ensureAgisoXianyuAutoDeliveryForDeliveredTask } from '../platforms/agiso/xianyu/auto-delivery-service.js'
import { syncKuaishouCloudRoleInfo } from './kuaishou-cloud-sync-service.js'
import { createHttpError } from '../../utils/http.js'
import { formatFenToAmount, normalizeFen } from '../../utils/money.js'
import { nowIso } from '../../utils/time.js'
@@ -33,7 +34,13 @@ const KUAISHOU_CLOUD_CLAIM_GUIDE_BASE_PATH = '/kuaishou-cloud-guide'
export async function getClaimDetail(token, { includeQrImage = true } = {}) {
const context = await getClaimContext(token)
const { task, session } = await loadTaskSession(context.task, { includeQrImage })
let { task, session } = await loadTaskSession(context.task, { includeQrImage })
// 如果是快手 Cloud 流程,尝试同步角色信息
if (String(task.executor_key || '').trim() === 'kuaishou_ct_assisted') {
task = await syncKuaishouCloudRoleInfo(task)
}
const syncedTask = session ? await syncTaskWithSession(task, session) : task
return buildClaimDetailPayload({
@@ -879,6 +886,8 @@ function mapClaimKuaishouCloudFulfillment(task, order) {
}
const binding = source.binding && typeof source.binding === 'object' ? source.binding : {}
const role = source.role && typeof source.role === 'object' ? source.role : {}
const purchase = source.purchase && typeof source.purchase === 'object' ? source.purchase : {}
const ticket = source.ticket && typeof source.ticket === 'object' ? source.ticket : {}
const dispatch = source.dispatch && typeof source.dispatch === 'object' ? source.dispatch : {}
const returnNumber = source.returnNumber && typeof source.returnNumber === 'object' ? source.returnNumber : {}
@@ -903,9 +912,32 @@ function mapClaimKuaishouCloudFulfillment(task, order) {
},
binding: {
prepareStatus: String(binding.prepareStatus || 'pending').trim() || 'pending',
cloudSourceKey: String(binding.cloudSourceKey || '').trim(),
skuId: Number(binding.skuId || 0) || 0,
skuName: String(binding.skuName || '').trim(),
vnKey: String(binding.vnKey || '').trim(),
vnId: Number(binding.vnId || 0) || 0,
bindUrl: String(binding.bindUrl || '').trim(),
bindPreparedAt: binding.bindPreparedAt || null,
vnPhone: String(binding.vnPhone || '').trim(),
roleName: String(binding.roleName || '').trim(),
roleId: String(binding.roleId || '').trim(),
},
role: {
status: String(role.status || 'pending').trim() || 'pending',
name: String(role.name || binding.roleName || '').trim(),
rid: String(role.rid || binding.roleId || '').trim(),
refreshedAt: role.refreshedAt || null,
errorMessage: String(role.errorMessage || '').trim(),
},
purchase: {
autoBuyEnabled: purchase.autoBuyEnabled !== false,
minAssetReserve: Number(purchase.minAssetReserve || 0) || 0,
usedKnapsack: purchase.usedKnapsack === true,
purchaseTriggered: purchase.purchaseTriggered === true,
assetBefore: Number(purchase.assetBefore || 0) || 0,
assetAfter: Number(purchase.assetAfter || 0) || 0,
purchaseAt: purchase.purchaseAt || null,
},
dispatch: {
status: String(dispatch.status || 'pending').trim() || 'pending',
@@ -11,6 +11,12 @@ import {
resolveKuaishouEticketShopConfig,
} from '../platforms/kuaishou-eticket/source-config-service.js'
import { queryKuaishouEticketConsumeDetail } from '../platforms/kuaishou-eticket/consume-service.js'
import {
dispatchKuaishouCloudFulfillmentTask,
normalizeKuaishouCloudFlow,
prepareKuaishouCloudFulfillmentTask,
refreshKuaishouCloudTaskRoleInfo,
} from '../fulfillment/kuaishou-cloud-task-service.js'
import { getClaimContext, getClaimDetail } from './claim-session-service.js'
const KUAISHOU_CLOUD_GUIDE_DIR = path.resolve(PROJECT_ROOT, '../../tems/imgs')
@@ -28,12 +34,6 @@ export async function verifyKuaishouCloudClaimTicket(token, payload = {}) {
}
const flow = normalizeKuaishouCloudFlow(parseTaskContext(context.task).kuaishouCloudFulfillment)
if (!flow.binding.bindUrl || flow.binding.prepareStatus !== 'ready') {
throw createHttpError('当前任务还没有准备好绑定资源,请联系客服稍后重试', {
statusCode: 409,
errorCode: 'claim_kuaishou_cloud_binding_not_ready',
})
}
const ticketCode = String(payload.ticketCode || payload.eTicketId || '').trim()
if (!ticketCode) {
@@ -96,7 +96,7 @@ export async function verifyKuaishouCloudClaimTicket(token, payload = {}) {
},
}
await updateTask(context.task.id, {
const updatedTask = await updateTask(context.task.id, {
claim_token: context.claimToken.token,
claim_expires_at: context.claimToken.expired_at,
claimed_at: context.task.claimed_at || now,
@@ -112,6 +112,14 @@ export async function verifyKuaishouCloudClaimTicket(token, payload = {}) {
goodsTitle: String(detailResult.goods?.itemTitle || '').trim(),
}, now)
const preparedFlow = normalizeKuaishouCloudFlow(nextContext.kuaishouCloudFulfillment)
if (preparedFlow.binding.prepareStatus !== 'ready' || !preparedFlow.binding.bindUrl) {
await prepareKuaishouCloudFulfillmentTask(updatedTask, {
source: 'claim_page_ticket_verified',
actor: { source: 'claim_page' },
})
}
return getClaimDetail(token, { includeQrImage: false })
}
@@ -135,47 +143,6 @@ export async function getKuaishouCloudClaimGuideAssetPath(filename) {
return filePath
}
function normalizeKuaishouCloudFlow(value) {
const source = value && typeof value === 'object' ? value : {}
const binding = source.binding && typeof source.binding === 'object' ? source.binding : {}
const consume = source.consume && typeof source.consume === 'object' ? source.consume : {}
const ticket = source.ticket && typeof source.ticket === 'object' ? source.ticket : {}
return {
...source,
ticket: {
code: String(ticket.code || '').trim(),
status: String(ticket.status || 'pending').trim() || 'pending',
capturedAt: ticket.capturedAt || null,
capturedBy: ticket.capturedBy || null,
verifiedAt: ticket.verifiedAt || null,
oid: String(ticket.oid || '').trim(),
formToken: String(ticket.formToken || '').trim(),
leftCount: Number(ticket.leftCount || 0) || 0,
goodsTitle: String(ticket.goodsTitle || '').trim(),
},
binding: {
prepareStatus: String(binding.prepareStatus || 'pending').trim() || 'pending',
cloudSourceKey: String(binding.cloudSourceKey || '').trim(),
skuId: Number(binding.skuId || 0) || 0,
skuName: String(binding.skuName || '').trim(),
vnKey: String(binding.vnKey || '').trim(),
vnId: Number(binding.vnId || 0) || 0,
vnPhone: String(binding.vnPhone || '').trim(),
bindUrl: String(binding.bindUrl || '').trim(),
bindPreparedAt: binding.bindPreparedAt || null,
},
consume: {
status: String(consume.status || 'pending').trim() || 'pending',
shopId: String(consume.shopId || '').trim(),
shopName: String(consume.shopName || '').trim(),
autoConsumeEnabled: consume.autoConsumeEnabled === true,
consumedAt: consume.consumedAt || null,
errorMessage: String(consume.errorMessage || '').trim(),
},
}
}
function parseTaskContext(task) {
const rawValue = task?.context_json
if (!rawValue) {
@@ -193,11 +160,84 @@ function parseTaskContext(task) {
}
}
function maskCode(value) {
const normalized = String(value || '').trim()
if (normalized.length <= 6) {
return normalized
export async function confirmKuaishouCloudClaimRole(token) {
const context = await getClaimContext(token)
const now = nowIso()
if (String(context.task.executor_key || '').trim() !== 'kuaishou_ct_assisted') {
throw createHttpError('当前领取链接不是快手 Cloud 客户领取流程', {
statusCode: 409,
errorCode: 'claim_not_kuaishou_cloud',
})
}
return `${normalized.slice(0, 3)}***${normalized.slice(-3)}`
if (['role_confirmed', 'dispatched_pending_return', 'completed'].includes(String(context.task.task_status || '').trim())) {
return getClaimDetail(token, { includeQrImage: false })
}
const refreshed = await refreshKuaishouCloudTaskRoleInfo(context.task, {
source: 'claim_page_role_confirm',
actor: { source: 'claim_page' },
recordEvent: false,
})
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,
errorCode: 'claim_kuaishou_cloud_role_not_ready',
})
}
await updateTask(context.task.id, {
task_status: 'role_confirmed',
role_id: flow.binding.roleId,
role_name: flow.binding.roleName,
user_action_status: 'role_confirmed',
role_confirmed_at: now,
last_error: '',
updated_at: now,
})
await createTaskEvent(context.task.id, 'kuaishou_cloud_role_confirmed', {
vnPhone: flow.binding.vnPhone,
roleName: flow.binding.roleName,
roleId: flow.binding.roleId,
}, now)
return getClaimDetail(token, { includeQrImage: false })
}
export async function redeemKuaishouCloudClaim(token) {
const context = await getClaimContext(token)
const now = nowIso()
if (String(context.task.executor_key || '').trim() !== 'kuaishou_ct_assisted') {
throw createHttpError('当前领取链接不是快手 Cloud 客户领取流程', {
statusCode: 409,
errorCode: 'claim_not_kuaishou_cloud',
})
}
if (context.task.task_status !== 'role_confirmed') {
throw createHttpError('请先确认角色信息', {
statusCode: 409,
errorCode: 'claim_kuaishou_cloud_role_not_confirmed',
})
}
await dispatchKuaishouCloudFulfillmentTask(context.task, {
source: 'claim_page_redeem',
actor: { source: 'claim_page' },
autoFinalize: true,
})
return getClaimDetail(token, { includeQrImage: false })
}
@@ -0,0 +1,45 @@
import {
normalizeKuaishouCloudFlow,
refreshKuaishouCloudTaskRoleInfo,
} from '../fulfillment/kuaishou-cloud-task-service.js'
export async function syncKuaishouCloudRoleInfo(task) {
const flow = normalizeKuaishouCloudFlow(parseTaskContext(task).kuaishouCloudFulfillment)
if (!flow.binding.vnId || !flow.binding.vnKey || !flow.binding.vnPhone) {
return task
}
if (flow.binding.roleName && flow.binding.roleId) {
return task
}
try {
const result = await refreshKuaishouCloudTaskRoleInfo(task, {
source: 'claim_page_polling',
actor: { source: 'system' },
recordEvent: false,
})
return result.task
} catch {
return task
}
}
function parseTaskContext(task) {
const rawValue = task?.context_json
if (!rawValue) {
return {}
}
if (typeof rawValue === 'object') {
return rawValue
}
try {
return JSON.parse(String(rawValue || '{}'))
} catch {
return {}
}
}