diff --git a/apps/backend/data/khhao-sync-state.json b/apps/backend/data/khhao-sync-state.json index 8bffd08d..4faeb6d1 100644 --- a/apps/backend/data/khhao-sync-state.json +++ b/apps/backend/data/khhao-sync-state.json @@ -1,13 +1,13 @@ { "syncFromCreatedAt": "2026-05-02T10:53:28.550Z", - "lastRunStartedAt": "2026-05-03T15:58:17.921Z", - "lastRunFinishedAt": "2026-05-03T15:58:18.685Z", + "lastRunStartedAt": "2026-05-03T17:50:39.717Z", + "lastRunFinishedAt": "2026-05-03T17:50:40.412Z", "lastRunStatus": "success", "lastErrorMessage": "", "fetchedCount": 40, - "syncedCount": 1, - "ignoredCount": 39, - "lastOrderCreatedAt": "2026-05-03T15:57:42.000Z", + "syncedCount": 0, + "ignoredCount": 40, + "lastOrderCreatedAt": "2026-05-03T17:47:36.000Z", "watchMode": "normal", "watchOrders": [] } diff --git a/apps/backend/src/routes/claims.js b/apps/backend/src/routes/claims.js index 0b83dc6d..6d31a941 100644 --- a/apps/backend/src/routes/claims.js +++ b/apps/backend/src/routes/claims.js @@ -11,7 +11,9 @@ import { redeemClaimTask, } from '../services/claim/claim-session-service.js' import { + confirmKuaishouCloudClaimRole, getKuaishouCloudClaimGuideAssetPath, + redeemKuaishouCloudClaim, verifyKuaishouCloudClaimTicket, } from '../services/claim/kuaishou-cloud-claim-service.js' import { buildNotFoundPayload, buildSuccessPayload, sendRouteError } from '../utils/http.js' @@ -90,6 +92,24 @@ router.post('/:token/kuaishou-cloud/verify-ticket', async (req, res) => { } }) +router.post('/:token/kuaishou-cloud/confirm-role', async (req, res) => { + try { + const data = await confirmKuaishouCloudClaimRole(req.params.token) + res.json(buildSuccessPayload(data, '角色已确认')) + } catch (error) { + sendRouteError(res, error, '确认角色失败', '[claims/:token/kuaishou-cloud/confirm-role]') + } +}) + +router.post('/:token/kuaishou-cloud/redeem', async (req, res) => { + try { + const data = await redeemKuaishouCloudClaim(req.params.token) + res.json(buildSuccessPayload(data, '兑换请求已提交')) + } catch (error) { + sendRouteError(res, error, '兑换失败', '[claims/:token/kuaishou-cloud/redeem]') + } +}) + router.get('/:token/screenshot', async (req, res) => { try { const screenshotPath = await getClaimScreenshotPath(req.params.token) diff --git a/apps/backend/src/services/admin/admin-write-service.js b/apps/backend/src/services/admin/admin-write-service.js index 592a74da..019cc744 100644 --- a/apps/backend/src/services/admin/admin-write-service.js +++ b/apps/backend/src/services/admin/admin-write-service.js @@ -824,6 +824,16 @@ export async function prepareAdminTaskKuaishouCloudFulfillment(taskId, session = vnPhone, bindUrl: bindUrlResult.bindUrl, bindPreparedAt: now, + roleName: '', + roleId: '', + }, + role: { + status: 'pending', + name: '', + rid: '', + refreshedAt: null, + errorMessage: '', + rawInfo: null, }, purchase: { ...flowWithResolvedBinding.purchase, @@ -1017,6 +1027,11 @@ export async function refreshAdminTaskKuaishouCloudRoleInfo(taskId, session = nu ...taskContext, kuaishouCloudFulfillment: { ...flow, + binding: { + ...flow.binding, + roleName: bindInfo.name, + roleId: bindInfo.rid, + }, role: { status: bindInfo.name || bindInfo.rid ? 'ready' : 'pending', name: bindInfo.name, @@ -1029,8 +1044,8 @@ export async function refreshAdminTaskKuaishouCloudRoleInfo(taskId, session = nu } const updatedTask = await updateTask(task.id, { - role_id: bindInfo.rid || task.role_id || '', - role_name: bindInfo.name || task.role_name || '', + role_id: bindInfo.rid || '', + role_name: bindInfo.name || '', context_json: JSON.stringify(nextContext), updated_at: now, }) diff --git a/apps/backend/src/services/claim/claim-session-service.js b/apps/backend/src/services/claim/claim-session-service.js index e2f7a185..ee72d806 100644 --- a/apps/backend/src/services/claim/claim-session-service.js +++ b/apps/backend/src/services/claim/claim-session-service.js @@ -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', diff --git a/apps/backend/src/services/claim/kuaishou-cloud-claim-service.js b/apps/backend/src/services/claim/kuaishou-cloud-claim-service.js index 7f86c262..613a2102 100644 --- a/apps/backend/src/services/claim/kuaishou-cloud-claim-service.js +++ b/apps/backend/src/services/claim/kuaishou-cloud-claim-service.js @@ -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 }) } diff --git a/apps/backend/src/services/claim/kuaishou-cloud-sync-service.js b/apps/backend/src/services/claim/kuaishou-cloud-sync-service.js new file mode 100644 index 00000000..b02715d1 --- /dev/null +++ b/apps/backend/src/services/claim/kuaishou-cloud-sync-service.js @@ -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 {} + } +} diff --git a/apps/backend/src/services/fulfillment/kuaishou-cloud-task-service.js b/apps/backend/src/services/fulfillment/kuaishou-cloud-task-service.js new file mode 100644 index 00000000..fbaf7776 --- /dev/null +++ b/apps/backend/src/services/fulfillment/kuaishou-cloud-task-service.js @@ -0,0 +1,945 @@ +// @ts-check + +import { createTaskEvent } from '../../repositories/task-event-repo.js' +import { getOrderById } from '../../repositories/order-repo.js' +import { updateTask } from '../../repositories/task-repo.js' +import { buildClaimUrl, createTaskClaimToken } from '../claim/claim-service.js' +import { normalizeProductName } from '../order/product-match-service.js' +import { getCloudtentaclesSourceConfig } from '../platforms/cloudtentacles/source-config-service.js' +import { getCloudtentaclesSessionState } from '../platforms/cloudtentacles/session-state-service.js' +import { + buyCloudtentaclesSku, + getCloudtentaclesAsset, + listCloudtentaclesSku, + useCloudtentaclesSku, +} from '../platforms/cloudtentacles/catalog-service.js' +import { getCloudtentaclesKnapsack } from '../platforms/cloudtentacles/knapsack-service.js' +import { + appointCloudtentaclesVirtualNumber, + backCloudtentaclesVirtualNumber, + fetchCloudtentaclesVirtualNumberCode, + generateCloudtentaclesLoginCode, + getCloudtentaclesBindInfo, + getCloudtentaclesBindUrl, + verifyCloudtentaclesLoginCode, +} from '../platforms/cloudtentacles/virtual-number-service.js' +import { consumeKuaishouEticket } from '../platforms/kuaishou-eticket/consume-service.js' +import { + getKuaishouEticketSourceConfig, + resolveKuaishouEticketShopConfig, +} from '../platforms/kuaishou-eticket/source-config-service.js' +import { createHttpError } from '../../utils/http.js' +import { nowIso } from '../../utils/time.js' + +const KUAISHOU_CLOUD_FIXED_VN_KEY = '1' + +export function isKuaishouCloudTask(task) { + return String(task?.executor_key || '').trim() === 'kuaishou_ct_assisted' +} + +export function normalizeKuaishouCloudFlow(value) { + const source = value && typeof value === 'object' ? value : {} + 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 dispatch = source.dispatch && typeof source.dispatch === 'object' ? source.dispatch : {} + const returnNumber = source.returnNumber && typeof source.returnNumber === 'object' ? source.returnNumber : {} + const consume = source.consume && typeof source.consume === 'object' ? source.consume : {} + const ticket = source.ticket && typeof source.ticket === 'object' ? source.ticket : {} + + const roleName = String(role.name || binding.roleName || '').trim() + const roleId = String(role.rid || binding.roleId || '').trim() + + return { + ...source, + configId: String(source.configId || '').trim(), + internalSkuCode: String(source.internalSkuCode || '').trim(), + internalSkuName: String(source.internalSkuName || '').trim(), + 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 || 'default').trim() || 'default', + skuId: Number(binding.skuId || 0) || 0, + skuName: String(binding.skuName || '').trim(), + vnKey: String(binding.vnKey || KUAISHOU_CLOUD_FIXED_VN_KEY).trim() || KUAISHOU_CLOUD_FIXED_VN_KEY, + vnId: Number(binding.vnId || 0) || 0, + vnPhone: String(binding.vnPhone || '').trim(), + bindUrl: String(binding.bindUrl || '').trim(), + bindPreparedAt: binding.bindPreparedAt || null, + roleName, + roleId, + }, + role: { + status: String(role.status || (roleName || roleId ? 'ready' : 'pending')).trim() || 'pending', + name: roleName, + rid: roleId, + refreshedAt: role.refreshedAt || null, + errorMessage: String(role.errorMessage || '').trim(), + rawInfo: role.rawInfo && typeof role.rawInfo === 'object' ? role.rawInfo : null, + }, + 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', + dispatchAt: dispatch.dispatchAt || null, + dispatchBy: dispatch.dispatchBy || null, + sendType: Number(dispatch.sendType || 0) || 0, + note: String(dispatch.note || '').trim(), + }, + returnNumber: { + status: String(returnNumber.status || 'pending').trim() || 'pending', + returnedAt: returnNumber.returnedAt || null, + returnedBy: returnNumber.returnedBy || null, + autoReturnEnabled: returnNumber.autoReturnEnabled === true, + }, + 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(), + }, + notes: String(source.notes || '').trim(), + } +} + +export function normalizeKuaishouCloudRoleInfo(value) { + const rawInfo = value && typeof value === 'object' ? value : null + + return { + name: String(rawInfo?.name || rawInfo?.roleName || rawInfo?.nickname || '').trim(), + rid: String(rawInfo?.rid || rawInfo?.roleId || rawInfo?.uid || '').trim(), + rawInfo, + } +} + +export function resolvePersistedCloudtentaclesContext() { + const source = getCloudtentaclesSourceConfig() + const session = getCloudtentaclesSessionState() + const token = String(session.token || '').trim() + + if (!token) { + throw createHttpError('当前 cloudtentacles 没有可用 token,请先到平台配置完成登录校验', { + statusCode: 409, + errorCode: 'kuaishou_cloud_missing_cloud_token', + }) + } + + return { + baseUrl: String(session.baseUrl || source.baseUrl || '').trim() || 'https://123.207.217.176', + token, + deviceId: String(session.deviceId || source.deviceId || '-').trim() || '-', + deviceType: Number(session.deviceType ?? source.deviceType ?? 0), + } +} + +export async function ensureTaskClaimLink(task) { + const tokenStatus = String(task?.primary_claim_token_status || '').trim() + const token = String(task?.primary_claim_token || task?.claim_token || '').trim() + const expiredAt = getTaskClaimExpiresAt(task) + + if (tokenStatus === 'active' && token && !isClaimExpired(expiredAt)) { + return { + token, + expiredAt, + claimUrl: buildClaimUrl(token), + } + } + + const claimToken = await createTaskClaimToken(task.id) + return { + token: claimToken.token, + expiredAt: claimToken.expired_at, + claimUrl: claimToken.claimUrl, + } +} + +export async function prepareKuaishouCloudFulfillmentTask(task, options = {}) { + if (!isKuaishouCloudTask(task)) { + throw createHttpError('当前任务不是快手 Cloud 履约任务', { + 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) { + let readyTask = task + + if (String(task.task_status || '').trim() !== 'waiting_binding') { + readyTask = await updateTask(task.id, { + task_status: 'waiting_binding', + claim_token: claimLinkState.token || task.claim_token || '', + claim_expires_at: claimLinkState.expiredAt || getTaskClaimExpiresAt(task), + updated_at: now, + }) + } + + return { + task: readyTask, + claimUrl: claimLinkState.claimUrl, + token: claimLinkState.token, + flow, + } + } + + const cloudContext = resolvePersistedCloudtentaclesContext() + 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 knapsackItem = resolvedBinding.knapsackItem + let usedKnapsack = Number(knapsackItem?.count || 0) > 0 + let purchaseTriggered = false + let assetBefore = 0 + let assetAfter = 0 + + if (!usedKnapsack) { + if (!flowWithResolvedBinding.purchase.autoBuyEnabled) { + throw createHttpError('背包中没有现成库存,且当前配置未开启自动购买', { + statusCode: 409, + errorCode: 'kuaishou_cloud_auto_buy_disabled', + }) + } + + const asset = await getCloudtentaclesAsset(cloudContext) + const targetSku = resolvedBinding.skuItem + + if (!targetSku) { + throw createHttpError(`cloudtentacles 未找到 SKU ${flowWithResolvedBinding.binding.skuId}`, { + statusCode: 404, + errorCode: 'kuaishou_cloud_sku_not_found', + }) + } + + assetBefore = Number(asset.asset || 0) || 0 + const targetPrice = Number(targetSku.price || 0) || 0 + const requiredAsset = targetPrice + flowWithResolvedBinding.purchase.minAssetReserve + if (assetBefore < requiredAsset) { + throw createHttpError(`余额不足,当前 ${assetBefore},至少需要 ${requiredAsset}`, { + statusCode: 409, + errorCode: 'kuaishou_cloud_asset_not_enough', + }) + } + + await buyCloudtentaclesSku({ + ...cloudContext, + id: flowWithResolvedBinding.binding.skuId, + count: 1, + }) + purchaseTriggered = true + + const assetResult = await getCloudtentaclesAsset(cloudContext) + assetAfter = Number(assetResult.asset || 0) || 0 + } + + const preparedBinding = await prepareKuaishouCloudBindResourceWithFallback({ + cloudContext, + vnKeyCandidates, + }) + + const nextContext = { + ...taskContext, + kuaishouCloudFulfillment: { + ...flowWithResolvedBinding, + binding: { + ...flowWithResolvedBinding.binding, + vnKey: preparedBinding.vnKey, + prepareStatus: 'ready', + vnId: preparedBinding.vnId, + vnPhone: preparedBinding.vnPhone, + bindUrl: preparedBinding.bindUrl, + bindPreparedAt: now, + roleName: '', + roleId: '', + }, + role: { + status: 'pending', + name: '', + rid: '', + refreshedAt: null, + errorMessage: '', + rawInfo: null, + }, + purchase: { + ...flowWithResolvedBinding.purchase, + usedKnapsack, + purchaseTriggered, + assetBefore, + assetAfter, + purchaseAt: purchaseTriggered ? now : flowWithResolvedBinding.purchase.purchaseAt, + }, + }, + } + + const updatedTask = await updateTask(task.id, { + task_status: 'waiting_binding', + inventory_status: 'not_required', + 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, + resolvedByName: resolvedBinding.resolvedByName, + actor, + }, now) + + return { + task: updatedTask, + claimUrl: claimLinkState.claimUrl, + token: claimLinkState.token, + flow: normalizeKuaishouCloudFlow(nextContext.kuaishouCloudFulfillment), + } +} + +export async function refreshKuaishouCloudTaskRoleInfo(task, options = {}) { + if (!isKuaishouCloudTask(task)) { + throw createHttpError('当前任务不是快手 Cloud 履约任务', { + 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_info_context', + }) + } + + const cloudContext = resolvePersistedCloudtentaclesContext() + const bindInfoResult = await getCloudtentaclesBindInfo({ + ...cloudContext, + key: flow.binding.vnKey, + id: flow.binding.vnId, + }) + + const bindInfo = normalizeKuaishouCloudRoleInfo(bindInfoResult.bindInfo) + const hasRoleInfo = Boolean(bindInfo.name || bindInfo.rid) + const nextContext = { + ...taskContext, + kuaishouCloudFulfillment: { + ...flow, + binding: { + ...flow.binding, + roleName: hasRoleInfo ? bindInfo.name : '', + roleId: hasRoleInfo ? bindInfo.rid : '', + }, + role: { + status: hasRoleInfo ? 'ready' : 'pending', + name: hasRoleInfo ? bindInfo.name : '', + rid: hasRoleInfo ? bindInfo.rid : '', + refreshedAt: now, + errorMessage: hasRoleInfo ? '' : '当前还没有查询到角色信息,请完成绑定后稍等片刻再试', + rawInfo: bindInfo.rawInfo, + }, + }, + } + + const updatedTask = await updateTask(task.id, { + role_id: hasRoleInfo ? bindInfo.rid : '', + role_name: hasRoleInfo ? bindInfo.name : '', + context_json: JSON.stringify(nextContext), + updated_at: now, + }) + + if (options.recordEvent !== false) { + await createTaskEvent(task.id, 'kuaishou_cloud_role_info_refreshed', { + source: String(options.source || 'system').trim() || 'system', + roleName: bindInfo.name, + roleId: bindInfo.rid, + vnId: flow.binding.vnId, + actor, + }, now) + } + + return { + task: updatedTask, + roleInfo: bindInfo, + flow: normalizeKuaishouCloudFlow(nextContext.kuaishouCloudFulfillment), + } +} + +export async function dispatchKuaishouCloudFulfillmentTask(task, options = {}) { + if (!isKuaishouCloudTask(task)) { + throw createHttpError('当前任务不是快手 Cloud 履约任务', { + statusCode: 409, + errorCode: 'kuaishou_cloud_task_invalid', + }) + } + + const actor = normalizeActor(options.actor) + const now = nowIso() + const taskContext = parseTaskContext(task) + const flow = normalizeKuaishouCloudFlow(taskContext.kuaishouCloudFulfillment) + const cloudContext = resolvePersistedCloudtentaclesContext() + + if (!flow.binding.skuId || !flow.binding.vnId || !flow.binding.vnPhone) { + throw createHttpError('当前任务还没有准备好绑定资源,请先完成绑定资源准备', { + statusCode: 409, + errorCode: 'kuaishou_cloud_not_prepared', + }) + } + + const ticketCode = String(options.ticketCode || '').trim() + const persistedTicketCode = String(flow.ticket.code || '').trim() + if (!persistedTicketCode && !ticketCode) { + throw createHttpError('客户还没有提交有效核销码,暂时不能继续兑换', { + statusCode: 409, + errorCode: 'kuaishou_cloud_missing_ticket_code', + }) + } + + if (flow.dispatch.status === 'success' && String(task.task_status || '').trim() === 'dispatched_pending_return') { + return { task, flow } + } + + const dispatchResult = await useCloudtentaclesSku({ + ...cloudContext, + id: flow.binding.skuId, + virtualNumberId: flow.binding.vnId, + phone: flow.binding.vnPhone, + }) + + const nextContext = { + ...taskContext, + kuaishouCloudFulfillment: { + ...flow, + ticket: { + ...flow.ticket, + code: ticketCode || persistedTicketCode, + capturedAt: ticketCode ? now : flow.ticket.capturedAt, + capturedBy: ticketCode && actor ? actor : flow.ticket.capturedBy, + }, + dispatch: { + ...flow.dispatch, + status: 'success', + dispatchAt: now, + dispatchBy: actor, + sendType: Number(dispatchResult.sendType || 0) || 0, + note: String(dispatchResult.note || dispatchResult.responseMessage || 'cloudtentacles 发货成功').trim(), + }, + }, + } + + let updatedTask = await updateTask(task.id, { + task_status: 'dispatched_pending_return', + delivery_status: 'delivered', + result_code: 'kuaishou_cloud_dispatched', + result_message: String(dispatchResult.responseMessage || dispatchResult.note || 'cloudtentacles 发货成功').trim(), + user_action_status: 'not_required', + last_error: '', + context_json: JSON.stringify(nextContext), + updated_at: now, + }) + + await createTaskEvent(task.id, 'kuaishou_cloud_dispatched', { + source: String(options.source || 'system').trim() || 'system', + ticketCodeMasked: maskCode(ticketCode || persistedTicketCode), + skuId: flow.binding.skuId, + vnId: flow.binding.vnId, + vnPhoneMasked: maskPhone(flow.binding.vnPhone), + sendType: dispatchResult.sendType, + note: dispatchResult.note, + actor, + }, now) + + const shouldAutoFinalize = options.autoFinalize === true + && normalizeKuaishouCloudFlow(nextContext.kuaishouCloudFulfillment).returnNumber.autoReturnEnabled === true + + if (shouldAutoFinalize) { + const finalizeResult = await returnKuaishouCloudFulfillmentTask(updatedTask, { + actor, + source: options.source || 'system_auto_finalize', + }) + updatedTask = finalizeResult.task + } + + return { + task: updatedTask, + flow: normalizeKuaishouCloudFlow(parseTaskContext(updatedTask).kuaishouCloudFulfillment), + } +} + +export async function returnKuaishouCloudFulfillmentTask(task, options = {}) { + if (!isKuaishouCloudTask(task)) { + throw createHttpError('当前任务不是快手 Cloud 履约任务', { + statusCode: 409, + errorCode: 'kuaishou_cloud_task_invalid', + }) + } + + const actor = normalizeActor(options.actor) + const now = nowIso() + const taskContext = parseTaskContext(task) + const flow = normalizeKuaishouCloudFlow(taskContext.kuaishouCloudFulfillment) + const cloudContext = resolvePersistedCloudtentaclesContext() + + if (!flow.binding.vnId || !flow.binding.vnKey) { + throw createHttpError('当前任务缺少可退还的虚拟号信息', { + statusCode: 409, + errorCode: 'kuaishou_cloud_missing_return_context', + }) + } + + if (flow.returnNumber.status === 'success' && ['completed', 'manual_review'].includes(String(task.task_status || '').trim())) { + return { task, flow } + } + + await backCloudtentaclesVirtualNumber({ + ...cloudContext, + key: flow.binding.vnKey, + id: flow.binding.vnId, + }) + + const order = await getOrderById(task.order_id) + const ticketCode = String(flow.ticket.code || '').trim() + const shopId = String(order?.shop_id || '').trim() + const eticketSource = getKuaishouEticketSourceConfig() + const shopConfig = resolveKuaishouEticketShopConfig({ + shopId, + shopName: String(order?.shop_name || '').trim(), + }) + + let consumeStatus = 'pending' + let consumeErrorMessage = '' + let consumedAt = null + let nextTaskStatus = 'completed' + let nextResultCode = 'kuaishou_cloud_completed' + let nextResultMessage = 'cloudtentacles 发货、退号并完成快手核销' + + if (!order) { + consumeStatus = 'failed' + consumeErrorMessage = '任务关联订单不存在,无法执行快手核销' + } else if (!ticketCode) { + consumeStatus = 'failed' + consumeErrorMessage = '客户未提交有效核销码,无法执行快手核销' + } else if (!shopConfig || shopConfig.enabled === false || !String(shopConfig.cookie || '').trim()) { + consumeStatus = 'failed' + consumeErrorMessage = '订单对应快手小店缺少可用 Cookie,无法执行快手核销' + } else { + try { + const consumeResult = await consumeKuaishouEticket({ + baseUrl: eticketSource.baseUrl, + cookie: shopConfig.cookie, + eTicketId: ticketCode, + oid: String(flow.ticket.oid || '').trim(), + formToken: String(flow.ticket.formToken || '').trim(), + }) + + if (consumeResult.consumed) { + consumeStatus = 'success' + consumedAt = now + } else { + consumeStatus = 'failed' + consumeErrorMessage = String(consumeResult.errorMessage || '快手核销失败').trim() + } + } catch (error) { + consumeStatus = 'failed' + consumeErrorMessage = error instanceof Error ? error.message : '快手核销失败' + } + } + + if (consumeStatus !== 'success') { + nextTaskStatus = 'manual_review' + nextResultCode = 'kuaishou_cloud_consume_failed' + nextResultMessage = consumeErrorMessage || '号码已退还,但快手核销未完成,请人工处理' + } + + const nextContext = { + ...taskContext, + kuaishouCloudFulfillment: { + ...flow, + returnNumber: { + ...flow.returnNumber, + status: 'success', + returnedAt: now, + returnedBy: actor, + }, + consume: { + ...flow.consume, + status: consumeStatus, + shopId: shopId || flow.consume.shopId, + shopName: String(flow.consume.shopName || order?.shop_name || '').trim(), + autoConsumeEnabled: flow.consume.autoConsumeEnabled === true, + consumedAt, + errorMessage: consumeErrorMessage, + }, + }, + } + + const updatedTask = await updateTask(task.id, { + task_status: nextTaskStatus, + delivery_status: 'delivered', + result_code: nextResultCode, + result_message: nextResultMessage, + redeemed_at: consumeStatus === 'success' ? now : task.redeemed_at, + last_error: consumeErrorMessage, + context_json: JSON.stringify(nextContext), + updated_at: now, + }) + + await createTaskEvent(task.id, 'kuaishou_cloud_number_returned', { + source: String(options.source || 'system').trim() || 'system', + vnId: flow.binding.vnId, + vnPhoneMasked: maskPhone(flow.binding.vnPhone), + actor, + }, now) + + await createTaskEvent( + task.id, + consumeStatus === 'success' ? 'kuaishou_cloud_consumed' : 'kuaishou_cloud_consume_failed', + { + source: String(options.source || 'system').trim() || 'system', + ticketCodeMasked: maskCode(ticketCode), + shopId, + consumeStatus, + errorMessage: consumeErrorMessage, + actor, + }, + now, + ) + + return { + task: updatedTask, + flow: normalizeKuaishouCloudFlow(parseTaskContext(updatedTask).kuaishouCloudFulfillment), + } +} + +export function maskPhone(value) { + const text = String(value || '').trim() + if (!text) { + return '' + } + + if (text.length <= 7) { + return `${text.slice(0, 2)}***${text.slice(-2)}` + } + + return `${text.slice(0, 3)}****${text.slice(-4)}` +} + +export function maskCode(value) { + const text = String(value || '').trim() + if (!text) { + return '' + } + + if (text.length <= 8) { + return `${text.slice(0, 2)}***${text.slice(-2)}` + } + + return `${text.slice(0, 4)}****${text.slice(-4)}` +} + +function resolveKuaishouCloudBindingResources(flow, { skuItems = [], knapsackItems = [] } = {}) { + const normalizedSkuItems = Array.isArray(skuItems) ? skuItems.filter(isCloudSkuLikeItem) : [] + const normalizedKnapsackItems = Array.isArray(knapsackItems) ? knapsackItems.filter(isCloudSkuLikeItem) : [] + const currentSkuId = Number(flow?.binding?.skuId || 0) || 0 + const currentSkuName = String(flow?.binding?.skuName || '').trim() + const nameCandidates = collectKuaishouCloudNameCandidates(flow) + + const skuItemById = currentSkuId > 0 + ? normalizedSkuItems.find((item) => Number(item.id || 0) === currentSkuId) || null + : null + const knapsackItemById = currentSkuId > 0 + ? normalizedKnapsackItems.find((item) => Number(item.id || 0) === currentSkuId) || null + : null + + if (skuItemById || knapsackItemById) { + const matchedItem = skuItemById || knapsackItemById + return { + skuId: Number(matchedItem?.id || 0) || 0, + skuName: String(currentSkuName || matchedItem?.name || '').trim(), + vnKey: KUAISHOU_CLOUD_FIXED_VN_KEY, + skuItem: skuItemById, + knapsackItem: knapsackItemById, + resolvedByName: false, + } + } + + const matchedSkuItem = findCloudItemByNames(normalizedSkuItems, nameCandidates) + const matchedKnapsackItem = findCloudItemByNames( + normalizedKnapsackItems, + nameCandidates, + matchedSkuItem ? Number(matchedSkuItem.id || 0) : 0, + ) + const matchedItem = matchedSkuItem || matchedKnapsackItem + + return { + skuId: Number(matchedItem?.id || 0) || 0, + skuName: String(currentSkuName || matchedItem?.name || '').trim(), + vnKey: KUAISHOU_CLOUD_FIXED_VN_KEY, + skuItem: matchedSkuItem, + knapsackItem: matchedKnapsackItem, + resolvedByName: Boolean(matchedItem), + } +} + +function resolveKuaishouCloudVnKeyCandidates() { + return [KUAISHOU_CLOUD_FIXED_VN_KEY] +} + +async function prepareKuaishouCloudBindResourceWithFallback(input = {}) { + const { cloudContext = {}, vnKeyCandidates = [] } = input + const candidates = Array.isArray(vnKeyCandidates) ? vnKeyCandidates : [] + let lastError = null + + for (const vnKey of candidates) { + let vnId = 0 + let vnPhone = '' + + try { + const appointed = await appointCloudtentaclesVirtualNumber({ + ...cloudContext, + key: vnKey, + }) + vnId = Number(appointed.item?.id || 0) + vnPhone = String(appointed.item?.phone || '').trim() + + if (!vnId || !vnPhone) { + throw createHttpError('申请虚拟号成功但返回数据不完整', { + statusCode: 502, + errorCode: 'kuaishou_cloud_invalid_vn', + }) + } + + await generateCloudtentaclesLoginCode({ + ...cloudContext, + key: vnKey, + id: vnId, + }) + + const fetchedCode = await fetchCloudtentaclesVirtualNumberCode({ + ...cloudContext, + key: vnKey, + phone: vnPhone, + }) + + await verifyCloudtentaclesLoginCode({ + ...cloudContext, + key: vnKey, + id: vnId, + code: fetchedCode.code, + }) + + const bindUrlResult = await getCloudtentaclesBindUrl({ + ...cloudContext, + key: vnKey, + id: vnId, + }) + + return { + vnKey, + vnId, + vnPhone, + bindUrl: String(bindUrlResult.bindUrl || '').trim(), + } + } catch (error) { + lastError = error + + if (vnId > 0) { + try { + await backCloudtentaclesVirtualNumber({ + ...cloudContext, + key: vnKey, + id: vnId, + }) + } catch { + // 退号失败保留主错误 + } + } + + if (!isRecoverableKuaishouCloudVnKeyError(error)) { + throw error + } + } + } + + throw lastError || createHttpError('没有找到可用的 VN Key', { + statusCode: 409, + errorCode: 'kuaishou_cloud_missing_binding_config', + }) +} + +function collectKuaishouCloudNameCandidates(flow) { + return Array.from(new Set([ + String(flow?.binding?.skuName || '').trim(), + String(flow?.internalSkuName || '').trim(), + String(flow?.internalSkuCode || '').trim(), + ].filter(Boolean))) +} + +function findCloudItemByNames(items, nameCandidates, preferredId = 0) { + const normalizedItems = Array.isArray(items) ? items : [] + const normalizedNames = nameCandidates + .map((item) => ({ + raw: String(item || '').trim(), + normalized: normalizeProductName(item), + })) + .filter((item) => item.raw && item.normalized) + + if (normalizedNames.length === 0 || normalizedItems.length === 0) { + return preferredId > 0 + ? normalizedItems.find((item) => Number(item.id || 0) === preferredId) || null + : null + } + + if (preferredId > 0) { + const preferred = normalizedItems.find((item) => Number(item.id || 0) === preferredId) || null + if (preferred) { + return preferred + } + } + + const exactMatches = normalizedItems.filter((item) => { + const itemName = normalizeProductName(item.name) + return normalizedNames.some((candidate) => candidate.normalized === itemName) + }) + if (exactMatches.length > 0) { + return exactMatches[0] + } + + const partialMatches = normalizedItems.filter((item) => { + const itemName = normalizeProductName(item.name) + return normalizedNames.some((candidate) => itemName.includes(candidate.normalized) || candidate.normalized.includes(itemName)) + }) + if (partialMatches.length > 0) { + return partialMatches.sort((left, right) => String(left.name || '').length - String(right.name || '').length)[0] + } + + return null +} + +function isCloudSkuLikeItem(item) { + return Boolean(item) && typeof item === 'object' && Number(item.id || 0) > 0 +} + +function isRecoverableKuaishouCloudVnKeyError(error) { + const errorCode = String(error?.errorCode || error?.code || '').trim() + const errorMessage = String(error?.message || '').trim() + return errorCode === 'cloudtentacles_vn_bind_url_failed' + && errorMessage.includes('不支持的游戏类型') +} + +function normalizeActor(actor) { + if (!actor || typeof actor !== 'object') { + return null + } + + const source = String(actor.source || '').trim() + const userId = Number(actor.userId || 0) || 0 + const username = String(actor.username || '').trim() + const role = String(actor.role || '').trim() + + if (!source && !userId && !username && !role) { + return null + } + + return { + source, + userId, + username, + role, + } +} + +function parseTaskContext(task) { + const value = task?.context_json + + if (!value) { + return {} + } + + if (typeof value === 'object') { + return value + } + + try { + return JSON.parse(String(value || '{}')) + } catch { + return {} + } +} + +function getTaskClaimExpiresAt(task) { + return task?.claim_expires_at || task?.primary_claim_expires_at || null +} + +function isClaimExpired(expiredAt) { + if (!expiredAt) { + return false + } + + const timestamp = new Date(expiredAt).getTime() + return Number.isFinite(timestamp) && timestamp <= Date.now() +} diff --git a/apps/backend/src/services/order/delivery-task-service.js b/apps/backend/src/services/order/delivery-task-service.js index 99a6d550..1183ba52 100644 --- a/apps/backend/src/services/order/delivery-task-service.js +++ b/apps/backend/src/services/order/delivery-task-service.js @@ -196,14 +196,28 @@ async function preparePaidTask(task) { 'manual_review', 'failed', ].includes(String(task.task_status || '').trim())) { + if (!task.primary_claim_token_id && !String(task.claim_token || '').trim()) { + const claimToken = await createTaskClaimToken(task.id) + return updateTask(task.id, { + claim_token: claimToken.token, + claim_expires_at: claimToken.expired_at, + user_action_status: 'pending_claim', + updated_at: now, + }) + } + return task } + const claimToken = await createTaskClaimToken(task.id) + return updateTask(task.id, { task_status: 'pending_binding_prepare', inventory_status: 'not_required', - user_action_status: 'pending', - last_error: task.last_error || '等待准备绑定资源', + claim_token: claimToken.token, + claim_expires_at: claimToken.expired_at, + user_action_status: 'pending_claim', + last_error: task.last_error || '领取链接已生成,等待客户提交核销码', updated_at: now, }) } diff --git a/apps/backend/src/services/order/order-service.js b/apps/backend/src/services/order/order-service.js index 8cc18ade..cebfe428 100644 --- a/apps/backend/src/services/order/order-service.js +++ b/apps/backend/src/services/order/order-service.js @@ -132,10 +132,16 @@ export async function upsertOrderFromSource(event, { sourceLabel = 'source' } = }) for (const task of tasks) { + const isClaimLinkTask = String(task.task_status || '') === 'link_generated' + || ( + String(task.executor_key || '').trim() === 'kuaishou_ct_assisted' + && ['pending_binding_prepare', 'waiting_binding', 'role_confirmed', 'dispatched_pending_return'].includes(String(task.task_status || '').trim()) + ) + if ( event.provider !== 'agiso' || event.platform !== 'xianyu' - || String(task.task_status || '') !== 'link_generated' + || !isClaimLinkTask || !task.primary_claim_token_id ) { continue diff --git a/apps/frontend/package-lock.json b/apps/frontend/package-lock.json index 325e9b11..b7aa86fc 100644 --- a/apps/frontend/package-lock.json +++ b/apps/frontend/package-lock.json @@ -8,8 +8,10 @@ "name": "order-site-rewrite", "version": "0.0.0", "dependencies": { + "@types/qrcode": "^1.5.6", "axios": "^1.9.0", "element-plus": "^2.10.2", + "qrcode": "^1.5.4", "vue": "^3.5.30", "vue-router": "^4.5.1" }, @@ -574,12 +576,20 @@ "version": "24.12.2", "resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.2.tgz", "integrity": "sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g==", - "dev": true, "license": "MIT", "dependencies": { "undici-types": "~7.16.0" } }, + "node_modules/@types/qrcode": { + "version": "1.5.6", + "resolved": "https://registry.npmjs.org/@types/qrcode/-/qrcode-1.5.6.tgz", + "integrity": "sha512-te7NQcV2BOvdj2b1hCAHzAoMNuj65kNBMz0KBaxM6c3VGBOhU0dURQKOtH8CFNI/dsKkwlv32p26qYQTWoB5bw==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/web-bluetooth": { "version": "0.0.20", "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.20.tgz", @@ -829,6 +839,30 @@ "dev": true, "license": "MIT" }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/async-validator": { "version": "4.2.5", "resolved": "https://registry.npmjs.org/async-validator/-/async-validator-4.2.5.tgz", @@ -865,6 +899,15 @@ "node": ">= 0.4" } }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/chokidar": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", @@ -881,6 +924,35 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -912,6 +984,15 @@ "integrity": "sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==", "license": "MIT" }, + "node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -931,6 +1012,12 @@ "node": ">=8" } }, + "node_modules/dijkstrajs": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz", + "integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==", + "license": "MIT" + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -971,6 +1058,12 @@ "vue": "^3.3.0" } }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, "node_modules/entities": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", @@ -1072,6 +1165,19 @@ } } }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/follow-redirects": { "version": "1.15.11", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", @@ -1132,6 +1238,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, "node_modules/get-intrinsic": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", @@ -1220,6 +1335,15 @@ "node": ">= 0.4" } }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/js-tokens": { "version": "9.0.1", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", @@ -1506,6 +1630,18 @@ "url": "https://github.com/sponsors/antfu" } }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/lodash": { "version": "4.18.1", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", @@ -1648,6 +1784,42 @@ ], "license": "MIT" }, + "node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/path-browserify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", @@ -1655,6 +1827,15 @@ "dev": true, "license": "MIT" }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/pathe": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", @@ -1693,6 +1874,15 @@ "pathe": "^2.0.3" } }, + "node_modules/pngjs": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz", + "integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==", + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/postcss": { "version": "8.5.8", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", @@ -1730,6 +1920,23 @@ "node": ">=10" } }, + "node_modules/qrcode": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz", + "integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==", + "license": "MIT", + "dependencies": { + "dijkstrajs": "^1.0.1", + "pngjs": "^5.0.0", + "yargs": "^15.3.1" + }, + "bin": { + "qrcode": "bin/qrcode" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/quansync": { "version": "0.2.11", "resolved": "https://registry.npmjs.org/quansync/-/quansync-0.2.11.tgz", @@ -1761,6 +1968,21 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "license": "ISC" + }, "node_modules/rolldown": { "version": "1.0.0-rc.13", "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.13.tgz", @@ -1809,6 +2031,12 @@ "dev": true, "license": "MIT" }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "license": "ISC" + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -1818,6 +2046,32 @@ "node": ">=0.10.0" } }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-literal": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz", @@ -1881,7 +2135,6 @@ "version": "7.16.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", - "dev": true, "license": "MIT" }, "node_modules/unimport": { @@ -2184,6 +2437,67 @@ "integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==", "dev": true, "license": "MIT" + }, + "node_modules/which-module": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", + "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==", + "license": "ISC" + }, + "node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/y18n": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", + "license": "ISC" + }, + "node_modules/yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "license": "MIT", + "dependencies": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "license": "ISC", + "dependencies": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + }, + "engines": { + "node": ">=6" + } } } } diff --git a/apps/frontend/package.json b/apps/frontend/package.json index bc013ab3..d2e490d1 100644 --- a/apps/frontend/package.json +++ b/apps/frontend/package.json @@ -10,8 +10,10 @@ "typecheck": "vue-tsc --noEmit -p tsconfig.app.json" }, "dependencies": { + "@types/qrcode": "^1.5.6", "axios": "^1.9.0", "element-plus": "^2.10.2", + "qrcode": "^1.5.4", "vue": "^3.5.30", "vue-router": "^4.5.1" }, diff --git a/apps/frontend/src/components.d.ts b/apps/frontend/src/components.d.ts index 9f1844ad..106924a6 100644 --- a/apps/frontend/src/components.d.ts +++ b/apps/frontend/src/components.d.ts @@ -20,6 +20,7 @@ declare module 'vue' { ElDialog: typeof import('element-plus/es')['ElDialog'] ElForm: typeof import('element-plus/es')['ElForm'] ElFormItem: typeof import('element-plus/es')['ElFormItem'] + ElIcon: typeof import('element-plus/es')['ElIcon'] ElInput: typeof import('element-plus/es')['ElInput'] ElInputNumber: typeof import('element-plus/es')['ElInputNumber'] ElOption: typeof import('element-plus/es')['ElOption'] diff --git a/apps/frontend/src/services/claim.ts b/apps/frontend/src/services/claim.ts index 08e6b2d1..ddeb6d58 100644 --- a/apps/frontend/src/services/claim.ts +++ b/apps/frontend/src/services/claim.ts @@ -30,6 +30,14 @@ export function verifyKuaishouCloudClaimTicket( return apiPost(`/api/v1/claim/${token}/kuaishou-cloud/verify-ticket`, payload) } +export function confirmKuaishouCloudClaimRole(token: string) { + return apiPost(`/api/v1/claim/${token}/kuaishou-cloud/confirm-role`, {}) +} + +export function redeemKuaishouCloudClaim(token: string) { + return apiPost(`/api/v1/claim/${token}/kuaishou-cloud/redeem`, {}) +} + export function refreshClaimSession(token: string) { return apiPost(`/api/v1/claim/${token}/session/refresh`, {}) } diff --git a/apps/frontend/src/types/claim.ts b/apps/frontend/src/types/claim.ts index 68ffe1e6..8a9f7473 100644 --- a/apps/frontend/src/types/claim.ts +++ b/apps/frontend/src/types/claim.ts @@ -78,9 +78,32 @@ export interface ClaimKuaishouCloudFlowInfo { } binding: { prepareStatus: string + cloudSourceKey: string + skuId: number + skuName: string + vnKey: string + vnId: number bindUrl: string bindPreparedAt: string | null vnPhone: string + roleName: string + roleId: string + } + role: { + status: string + name: string + rid: string + refreshedAt: string | null + errorMessage: string + } + purchase: { + autoBuyEnabled: boolean + minAssetReserve: number + usedKnapsack: boolean + purchaseTriggered: boolean + assetBefore: number + assetAfter: number + purchaseAt: string | null } dispatch: { status: string diff --git a/apps/frontend/src/views/claim/KuaishouCloudClaimView.vue b/apps/frontend/src/views/claim/KuaishouCloudClaimView.vue index 6c4b2763..6dc71b42 100644 --- a/apps/frontend/src/views/claim/KuaishouCloudClaimView.vue +++ b/apps/frontend/src/views/claim/KuaishouCloudClaimView.vue @@ -1,8 +1,16 @@