911 lines
28 KiB
TypeScript
911 lines
28 KiB
TypeScript
import fs from 'node:fs'
|
|
import path from 'node:path'
|
|
|
|
import { PROJECT_ROOT } from '../../config/runtime.js'
|
|
import { createTaskEvent } from '../../repositories/task-event-repo.js'
|
|
import { getTaskById, updateTask, updateTaskStatusIfCurrent } from '../../repositories/task-repo.js'
|
|
import { createHttpError } from '../../utils/http.js'
|
|
import { addHours, nowIso } from '../../utils/time.js'
|
|
import {
|
|
TASK_STATUS,
|
|
canRedeemKuaishouCloudClaimStatus,
|
|
isKuaishouCloudRedeemSettledStatus,
|
|
isKuaishouCloudRoleConfirmSettledStatus,
|
|
normalizeTaskStatus,
|
|
} from '../../domain/task-status.js'
|
|
import {
|
|
getKuaishouEticketSourceConfig,
|
|
listKuaishouEticketShopConfigs,
|
|
resolveKuaishouEticketShopConfig,
|
|
type KuaishouEticketShopConfig,
|
|
} from '../platforms/kuaishou-eticket/source-config-service.js'
|
|
import {
|
|
consumeKuaishouEticket,
|
|
queryKuaishouEticketConsumeDetail,
|
|
} from '../platforms/kuaishou-eticket/consume-service.js'
|
|
import { isKuaishouEticketMockTicketCode } from '../platforms/kuaishou-eticket/mock-ticket-service.js'
|
|
import {
|
|
dispatchKuaishouCloudFulfillmentTask,
|
|
hasKuaishouCloudCustomerRole,
|
|
hasKuaishouCloudDefaultRoleSnapshot,
|
|
maskCode,
|
|
normalizeKuaishouCloudFlow,
|
|
prepareKuaishouCloudFulfillmentTask,
|
|
rebindKuaishouCloudTaskRole,
|
|
refreshKuaishouCloudTaskRoleInfo,
|
|
} from '../fulfillment/kuaishou-cloud/index.js'
|
|
import { syncKuaishouFeifeiTaskStatus } from '../fulfillment/kuaishou-feifei/index.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'
|
|
|
|
const KUAISHOU_CLOUD_GUIDE_DIR = path.resolve(PROJECT_ROOT, '../../tems/imgs')
|
|
const ALLOWED_GUIDE_FILES = new Set(['1.png', '2.png', '3.png'])
|
|
|
|
type JsonObject = Record<string, any>
|
|
type ClaimDetailPayload = ReturnType<typeof buildClaimDetailPayload>
|
|
|
|
export async function verifyKuaishouCloudClaimTicket(token: unknown, payload: JsonObject = {}) {
|
|
const context = await getClaimContext(token)
|
|
const now = nowIso()
|
|
|
|
const executorKey = String(context.task.executor_key || '').trim()
|
|
|
|
if (executorKey === 'kuaishou-industry') {
|
|
return verifyIndustryVoucherTicket(context, now)
|
|
}
|
|
|
|
if (executorKey !== 'kuaishou_ct_assisted') {
|
|
throw createHttpError('当前领取链接不是快手 Cloud 客户领取流程', {
|
|
statusCode: 409,
|
|
errorCode: 'claim_not_kuaishou_cloud',
|
|
})
|
|
}
|
|
|
|
const flow = normalizeKuaishouCloudFlow(parseTaskContext(context.task).kuaishouCloudFulfillment)
|
|
if (hasUsableIndustryVoucher(parseTaskContext(context.task))) {
|
|
return verifyIndustryVoucherTicket(context, now)
|
|
}
|
|
|
|
const ticketCode = String(payload.ticketCode || payload.eTicketId || '').trim()
|
|
if (!ticketCode) {
|
|
throw createHttpError('请先粘贴快手小店核销码', {
|
|
statusCode: 400,
|
|
errorCode: 'claim_kuaishou_cloud_missing_ticket_code',
|
|
})
|
|
}
|
|
|
|
const persistedTicketCode = String(flow.ticket.code || '').trim()
|
|
if (flow.consume.status === 'success') {
|
|
if (!persistedTicketCode || persistedTicketCode === ticketCode) {
|
|
return getKuaishouCloudClaimDetail(token)
|
|
}
|
|
|
|
throw createHttpError('当前领取链接已完成核销,不能更换核销码', {
|
|
statusCode: 409,
|
|
errorCode: 'claim_kuaishou_cloud_ticket_already_consumed',
|
|
})
|
|
}
|
|
|
|
const mockTicketCode = isKuaishouEticketMockTicketCode(ticketCode)
|
|
let shopId = String(
|
|
flow.consume.shopId || context.order.shop_id || (mockTicketCode ? 'mock' : ''),
|
|
).trim()
|
|
let shopName = String(flow.consume.shopName || context.order.shop_name || '').trim()
|
|
let detailResult
|
|
let shopCookie = ''
|
|
let consumeBaseUrl = ''
|
|
|
|
if (mockTicketCode) {
|
|
detailResult = await queryKuaishouEticketConsumeDetail({
|
|
eTicketId: ticketCode,
|
|
goodsTitle: context.orderItem.sku_name || context.orderItem.sku_code,
|
|
})
|
|
} else {
|
|
const matched = await queryKuaishouEticketConsumeDetailWithShopFallback({
|
|
ticketCode,
|
|
shopId,
|
|
shopName,
|
|
fallbackShopName: String(context.order.shop_name || '').trim(),
|
|
})
|
|
|
|
detailResult = matched.detailResult
|
|
shopId = matched.shopConfig.shopId || shopId
|
|
shopName = matched.shopConfig.kshopName || shopName
|
|
shopCookie = matched.shopConfig.cookie
|
|
consumeBaseUrl = getKuaishouEticketSourceConfig().baseUrl
|
|
}
|
|
|
|
if (!detailResult.ok || detailResult.alreadyConsumed || !detailResult.detail) {
|
|
throw createHttpError(detailResult.errorMessage || '核销码校验失败,请确认是否复制完整', {
|
|
statusCode: 409,
|
|
errorCode: detailResult.alreadyConsumed
|
|
? 'claim_kuaishou_cloud_ticket_already_consumed'
|
|
: 'claim_kuaishou_cloud_ticket_invalid',
|
|
})
|
|
}
|
|
|
|
const consumePayload = {
|
|
baseUrl: consumeBaseUrl,
|
|
cookie: shopCookie,
|
|
eTicketId: detailResult.eTicketId || ticketCode,
|
|
oid: String(detailResult.detail.oid || '').trim(),
|
|
formToken: String(detailResult.detail.formToken || '').trim(),
|
|
num: Number(detailResult.detail.leftCount || 0) || 1,
|
|
}
|
|
const consumeResult = await consumeKuaishouEticket(consumePayload)
|
|
if (!consumeResult.consumed) {
|
|
throw createHttpError(consumeResult.errorMessage || '核销失败,请确认核销码状态后重试', {
|
|
statusCode: 409,
|
|
errorCode: 'claim_kuaishou_cloud_consume_failed',
|
|
})
|
|
}
|
|
|
|
const taskContext = parseTaskContext(context.task)
|
|
const nextContext = {
|
|
...taskContext,
|
|
kuaishouCloudFulfillment: {
|
|
...flow,
|
|
ticket: {
|
|
...flow.ticket,
|
|
code: detailResult.eTicketId || ticketCode,
|
|
status: 'verified',
|
|
capturedAt: flow.ticket.capturedAt || now,
|
|
capturedBy: flow.ticket.capturedBy || {
|
|
source: 'claim_page',
|
|
},
|
|
verifiedAt: now,
|
|
oid: String(detailResult.detail.oid || '').trim(),
|
|
formToken: String(detailResult.detail.formToken || '').trim(),
|
|
leftCount: Number(detailResult.detail.leftCount || 0) || 0,
|
|
goodsTitle: String(detailResult.goods?.itemTitle || '').trim(),
|
|
},
|
|
consume: {
|
|
...flow.consume,
|
|
status: 'success',
|
|
shopId,
|
|
shopName,
|
|
consumedAt: now,
|
|
errorMessage: '',
|
|
},
|
|
},
|
|
}
|
|
|
|
if (isKuaishouCloudMockContext(nextContext)) {
|
|
const mockFlow = buildMockVerifiedKuaishouCloudFlow(nextContext.kuaishouCloudFulfillment, now)
|
|
await updateTask(context.task.id, {
|
|
claim_token: context.claimToken.token,
|
|
claim_expires_at: context.claimToken.expired_at,
|
|
context_json: JSON.stringify({
|
|
...nextContext,
|
|
kuaishouCloudFulfillment: mockFlow,
|
|
}),
|
|
claimed_at: context.task.claimed_at || now,
|
|
task_status: TASK_STATUS.WAITING_BINDING,
|
|
user_action_status: 'pending_claim',
|
|
last_error: '',
|
|
updated_at: now,
|
|
})
|
|
} else {
|
|
const preparedFlow = normalizeKuaishouCloudFlow(nextContext.kuaishouCloudFulfillment)
|
|
const taskWithConsumedTicket = (await updateTask(context.task.id, {
|
|
claim_token: context.claimToken.token,
|
|
claim_expires_at: context.claimToken.expired_at,
|
|
context_json: JSON.stringify(nextContext),
|
|
claimed_at: context.task.claimed_at || now,
|
|
task_status: TASK_STATUS.WAITING_BINDING,
|
|
user_action_status: 'pending_claim',
|
|
last_error: '',
|
|
updated_at: now,
|
|
})) || {
|
|
...context.task,
|
|
context_json: JSON.stringify(nextContext),
|
|
claimed_at: context.task.claimed_at || now,
|
|
task_status: TASK_STATUS.WAITING_BINDING,
|
|
user_action_status: 'pending_claim',
|
|
last_error: '',
|
|
updated_at: now,
|
|
}
|
|
|
|
if (preparedFlow.binding.prepareStatus !== 'ready' || !preparedFlow.binding.bindUrl) {
|
|
await prepareKuaishouCloudFulfillmentTask(taskWithConsumedTicket, {
|
|
source: 'claim_page_ticket_verified',
|
|
actor: { source: 'claim_page' },
|
|
})
|
|
}
|
|
}
|
|
|
|
await createTaskEvent(
|
|
context.task.id,
|
|
'kuaishou_cloud_ticket_verified',
|
|
{
|
|
ticketCodeMasked: maskCode(detailResult.eTicketId || ticketCode),
|
|
shopId,
|
|
shopName,
|
|
goodsTitle: String(detailResult.goods?.itemTitle || '').trim(),
|
|
mock: mockTicketCode,
|
|
consumedAt: now,
|
|
consumeAlreadyCompleted: consumeResult.alreadyConsumed === true,
|
|
},
|
|
now,
|
|
)
|
|
|
|
await createTaskEvent(
|
|
context.task.id,
|
|
'kuaishou_cloud_consumed',
|
|
{
|
|
source: 'claim_page_ticket_submit',
|
|
ticketCodeMasked: maskCode(detailResult.eTicketId || ticketCode),
|
|
shopId,
|
|
shopName,
|
|
consumeStatus: 'success',
|
|
consumedAt: now,
|
|
mock: mockTicketCode,
|
|
},
|
|
now,
|
|
)
|
|
|
|
return getKuaishouCloudClaimDetail(token)
|
|
}
|
|
|
|
async function verifyIndustryVoucherTicket(
|
|
context: Awaited<ReturnType<typeof getClaimContext>>,
|
|
now: string,
|
|
): Promise<ClaimDetailPayload> {
|
|
const taskContext = parseTaskContext(context.task)
|
|
const flow = normalizeKuaishouCloudFlow(taskContext.kuaishouCloudFulfillment)
|
|
|
|
if (flow.consume.status === 'success') {
|
|
return getKuaishouCloudClaimDetail(context.claimToken.token)
|
|
}
|
|
|
|
const industryContext = typeof taskContext === 'object' ? taskContext : {}
|
|
const voucherContext = normalizeIndustryVoucherContext(industryContext.kuaishouIndustryVoucher)
|
|
if (voucherContext.status === 'DESTROYED') {
|
|
throw createHttpError('当前电子凭证已销毁,无法继续领取', {
|
|
statusCode: 409,
|
|
errorCode: 'claim_kuaishou_industry_voucher_destroyed',
|
|
})
|
|
}
|
|
|
|
const token = String(voucherContext.token || industryContext.token || '').trim()
|
|
const voucherCode = String(voucherContext.voucherCode || voucherContext.eticketId || '').trim()
|
|
const certExpireType = Number(industryContext.certExpireType || 0)
|
|
const certActualStartTime = Number(
|
|
voucherContext.validStartTime || industryContext.certActualStartTime || 0,
|
|
)
|
|
const certActualEndTime = Number(
|
|
voucherContext.validEndTime || industryContext.certActualEndTime || 0,
|
|
)
|
|
|
|
const nextContext = {
|
|
...taskContext,
|
|
kuaishouIndustryVoucher: {
|
|
...voucherContext,
|
|
oid: voucherContext.oid || context.order.platform_order_id,
|
|
token,
|
|
eticketId: voucherCode,
|
|
voucherCode,
|
|
status: voucherContext.status || 'UNUSED',
|
|
verifiedAt: voucherContext.verifiedAt || now,
|
|
},
|
|
kuaishouCloudFulfillment: {
|
|
...flow,
|
|
ticket: {
|
|
...flow.ticket,
|
|
code: voucherCode,
|
|
status: 'verified',
|
|
capturedAt: flow.ticket.capturedAt || now,
|
|
capturedBy: flow.ticket.capturedBy || { source: 'send_code_callback' },
|
|
verifiedAt: now,
|
|
oid: voucherContext.oid || context.order.platform_order_id,
|
|
formToken: token,
|
|
leftCount: voucherContext.status === 'CONSUMED' ? 0 : 1,
|
|
goodsTitle: context.orderItem.sku_name || context.orderItem.sku_code || '',
|
|
},
|
|
consume: {
|
|
...flow.consume,
|
|
status: voucherContext.status === 'CONSUMED' ? 'success' : 'pending',
|
|
shopId: context.order.shop_id,
|
|
shopName: context.order.shop_name,
|
|
autoConsumeEnabled: true,
|
|
consumedAt: voucherContext.status === 'CONSUMED'
|
|
? voucherContext.consumedAt || flow.consume.consumedAt || now
|
|
: flow.consume.consumedAt,
|
|
},
|
|
certInfo: {
|
|
certExpireType,
|
|
certActualStartTime,
|
|
certActualEndTime,
|
|
},
|
|
},
|
|
}
|
|
|
|
const taskWithTicket = (await updateTask(context.task.id, {
|
|
claim_token: context.claimToken.token,
|
|
claim_expires_at: context.claimToken.expired_at,
|
|
context_json: JSON.stringify(nextContext),
|
|
claimed_at: context.task.claimed_at || now,
|
|
task_status: TASK_STATUS.WAITING_BINDING,
|
|
user_action_status: 'pending_claim',
|
|
last_error: '',
|
|
updated_at: now,
|
|
})) || {
|
|
...context.task,
|
|
context_json: JSON.stringify(nextContext),
|
|
claimed_at: context.task.claimed_at || now,
|
|
task_status: TASK_STATUS.WAITING_BINDING,
|
|
user_action_status: 'pending_claim',
|
|
last_error: '',
|
|
updated_at: now,
|
|
}
|
|
|
|
const preparedFlow = normalizeKuaishouCloudFlow(nextContext.kuaishouCloudFulfillment)
|
|
if (preparedFlow.binding.prepareStatus !== 'ready' || !preparedFlow.binding.bindUrl) {
|
|
await prepareKuaishouCloudFulfillmentTask(taskWithTicket, {
|
|
source: 'send_code_callback',
|
|
actor: { source: 'send_code_callback' },
|
|
})
|
|
}
|
|
|
|
await createTaskEvent(
|
|
context.task.id,
|
|
'kuaishou_cloud_ticket_verified',
|
|
{
|
|
ticketCodeMasked: '',
|
|
source: 'send_code_callback',
|
|
industryVoucher: true,
|
|
verifiedAt: now,
|
|
},
|
|
now,
|
|
)
|
|
|
|
return getKuaishouCloudClaimDetail(context.claimToken.token)
|
|
}
|
|
|
|
export async function getKuaishouCloudClaimGuideAssetPath(filename: unknown) {
|
|
const normalized = String(filename || '').trim()
|
|
if (!ALLOWED_GUIDE_FILES.has(normalized)) {
|
|
throw createHttpError('指引图片不存在', {
|
|
statusCode: 404,
|
|
errorCode: 'claim_kuaishou_cloud_asset_not_found',
|
|
})
|
|
}
|
|
|
|
const filePath = path.resolve(KUAISHOU_CLOUD_GUIDE_DIR, normalized)
|
|
if (!filePath.startsWith(KUAISHOU_CLOUD_GUIDE_DIR) || !fs.existsSync(filePath)) {
|
|
throw createHttpError('指引图片不存在', {
|
|
statusCode: 404,
|
|
errorCode: 'claim_kuaishou_cloud_asset_not_found',
|
|
})
|
|
}
|
|
|
|
return filePath
|
|
}
|
|
|
|
export async function getKuaishouCloudClaimDetail(token: unknown): Promise<ClaimDetailPayload> {
|
|
const context = await getClaimContext(token)
|
|
let task = context.task
|
|
|
|
const executorKey = String(task.executor_key || '').trim()
|
|
|
|
if (executorKey === 'kuaishou_feifei') {
|
|
task = (await syncKuaishouFeifeiTaskStatus(task)) || task
|
|
}
|
|
|
|
if (executorKey === 'kuaishou-industry') {
|
|
const flow = normalizeKuaishouCloudFlow(parseTaskContext(task).kuaishouCloudFulfillment)
|
|
if (flow.consume.status !== 'success') {
|
|
const now = nowIso()
|
|
await verifyIndustryVoucherTicket(context, now).catch(() => null)
|
|
}
|
|
}
|
|
|
|
if (executorKey === 'kuaishou_ct_assisted') {
|
|
const taskContext = parseTaskContext(task)
|
|
const flow = normalizeKuaishouCloudFlow(taskContext.kuaishouCloudFulfillment)
|
|
const needsIndustryVoucherPrepare =
|
|
hasUsableIndustryVoucher(taskContext) &&
|
|
(
|
|
flow.ticket.status !== 'verified' ||
|
|
(flow.binding.prepareStatus !== 'ready' && !flow.binding.bindUrl)
|
|
)
|
|
|
|
if (needsIndustryVoucherPrepare) {
|
|
const now = nowIso()
|
|
const prepared: ClaimDetailPayload | null = await verifyIndustryVoucherTicket(context, now)
|
|
.catch(() => null)
|
|
if (prepared) {
|
|
return prepared
|
|
}
|
|
}
|
|
}
|
|
|
|
if (
|
|
executorKey === 'kuaishou_ct_assisted' &&
|
|
!isKuaishouCloudMockTask(task)
|
|
) {
|
|
task = (await syncKuaishouCloudRoleInfo(task)) || task
|
|
}
|
|
|
|
return buildClaimDetailPayload({
|
|
claimToken: context.claimToken,
|
|
task,
|
|
order: context.order,
|
|
orderItem: context.orderItem,
|
|
})
|
|
}
|
|
|
|
export async function rebindKuaishouCloudClaimRole(token: unknown) {
|
|
const context = await getClaimContext(token)
|
|
|
|
if (String(context.task.executor_key || '').trim() !== 'kuaishou_ct_assisted') {
|
|
throw createHttpError('当前领取链接不是快手 Cloud 客户领取流程', {
|
|
statusCode: 409,
|
|
errorCode: 'claim_not_kuaishou_cloud',
|
|
})
|
|
}
|
|
|
|
await rebindKuaishouCloudTaskRole(context.task, {
|
|
source: 'claim_page_rebind_role',
|
|
actor: { source: 'claim_page' },
|
|
})
|
|
|
|
return getKuaishouCloudClaimDetail(token)
|
|
}
|
|
|
|
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 {}
|
|
}
|
|
}
|
|
|
|
function hasUsableIndustryVoucher(context: JsonObject = {}) {
|
|
const voucher = normalizeIndustryVoucherContext(context.kuaishouIndustryVoucher)
|
|
if (!voucher.voucherCode && !voucher.eticketId) {
|
|
return false
|
|
}
|
|
|
|
return voucher.status !== 'DESTROYED'
|
|
}
|
|
|
|
function normalizeIndustryVoucherContext(value: unknown): JsonObject {
|
|
const source = value && typeof value === 'object' && !Array.isArray(value)
|
|
? value as JsonObject
|
|
: {}
|
|
const status = String(source.status || 'UNUSED').trim().toUpperCase()
|
|
|
|
return {
|
|
...source,
|
|
oid: String(source.oid || '').trim(),
|
|
token: String(source.token || '').trim(),
|
|
eticketId: String(source.eticketId || source.voucherCode || '').trim(),
|
|
voucherCode: String(source.voucherCode || source.eticketId || '').trim(),
|
|
status: status === 'CONSUMED' || status === 'DESTROYED' ? status : 'UNUSED',
|
|
validStartTime: Number(source.validStartTime || 0) || 0,
|
|
validEndTime: Number(source.validEndTime || 0) || 0,
|
|
verifiedAt: source.verifiedAt || null,
|
|
consumedAt: source.consumedAt || null,
|
|
}
|
|
}
|
|
|
|
async function queryKuaishouEticketConsumeDetailWithShopFallback({
|
|
ticketCode,
|
|
shopId,
|
|
shopName,
|
|
fallbackShopName = '',
|
|
}: {
|
|
ticketCode: string
|
|
shopId: string
|
|
shopName: string
|
|
fallbackShopName?: string
|
|
}) {
|
|
const eticketSource = getKuaishouEticketSourceConfig()
|
|
const shopConfigs = resolveKuaishouEticketDetailCandidateShops({
|
|
shopId,
|
|
shopName,
|
|
fallbackShopName,
|
|
})
|
|
|
|
if (shopConfigs.length === 0) {
|
|
throw createHttpError('还没有配置可用的快手小店 Cookie,请联系客服处理', {
|
|
statusCode: 409,
|
|
errorCode: 'claim_kuaishou_cloud_shop_cookie_missing',
|
|
})
|
|
}
|
|
|
|
let lastResult: JsonObject | null = null
|
|
let lastError: unknown = null
|
|
|
|
for (const shopConfig of shopConfigs) {
|
|
try {
|
|
const detailResult = await queryKuaishouEticketConsumeDetail({
|
|
baseUrl: eticketSource.baseUrl,
|
|
cookie: shopConfig.cookie,
|
|
eTicketId: ticketCode,
|
|
})
|
|
|
|
lastResult = detailResult
|
|
if (detailResult.ok || detailResult.alreadyConsumed) {
|
|
return {
|
|
shopConfig,
|
|
detailResult,
|
|
}
|
|
}
|
|
} catch (error) {
|
|
lastError = error
|
|
}
|
|
}
|
|
|
|
if (lastResult) {
|
|
const fallbackShopConfig = shopConfigs[0]
|
|
if (!fallbackShopConfig) {
|
|
throw createHttpError('还没有配置可用的快手小店 Cookie,请联系客服处理', {
|
|
statusCode: 409,
|
|
errorCode: 'claim_kuaishou_cloud_shop_cookie_missing',
|
|
})
|
|
}
|
|
|
|
return {
|
|
shopConfig: fallbackShopConfig,
|
|
detailResult: lastResult,
|
|
}
|
|
}
|
|
|
|
throw createHttpError(
|
|
lastError instanceof Error ? lastError.message : '核销码校验失败,请确认是否复制完整',
|
|
{
|
|
statusCode: 409,
|
|
errorCode: 'claim_kuaishou_cloud_ticket_invalid',
|
|
},
|
|
)
|
|
}
|
|
|
|
function resolveKuaishouEticketDetailCandidateShops({
|
|
shopId,
|
|
shopName,
|
|
fallbackShopName = '',
|
|
}: {
|
|
shopId: string
|
|
shopName: string
|
|
fallbackShopName?: string
|
|
}) {
|
|
const candidates: KuaishouEticketShopConfig[] = []
|
|
const configuredShop = resolveKuaishouEticketShopConfig({
|
|
shopId,
|
|
shopName: shopName || fallbackShopName,
|
|
})
|
|
|
|
if (isUsableKuaishouEticketShopConfig(configuredShop)) {
|
|
candidates.push(configuredShop)
|
|
}
|
|
|
|
for (const shopConfig of listKuaishouEticketShopConfigs()) {
|
|
if (!isUsableKuaishouEticketShopConfig(shopConfig)) {
|
|
continue
|
|
}
|
|
|
|
if (candidates.some((item) => isSameKuaishouEticketShopConfig(item, shopConfig))) {
|
|
continue
|
|
}
|
|
|
|
candidates.push(shopConfig)
|
|
}
|
|
|
|
return candidates
|
|
}
|
|
|
|
function isUsableKuaishouEticketShopConfig(
|
|
shopConfig: KuaishouEticketShopConfig | null | undefined,
|
|
): shopConfig is KuaishouEticketShopConfig {
|
|
return Boolean(
|
|
shopConfig && shopConfig.enabled !== false && String(shopConfig.cookie || '').trim(),
|
|
)
|
|
}
|
|
|
|
function isSameKuaishouEticketShopConfig(
|
|
left: KuaishouEticketShopConfig,
|
|
right: KuaishouEticketShopConfig,
|
|
) {
|
|
const leftShopId = String(left.shopId || '').trim()
|
|
const rightShopId = String(right.shopId || '').trim()
|
|
|
|
if (leftShopId && rightShopId) {
|
|
return leftShopId === rightShopId
|
|
}
|
|
|
|
return String(left.kshopName || '').trim() === String(right.kshopName || '').trim()
|
|
}
|
|
|
|
export async function confirmKuaishouCloudClaimRole(token: unknown) {
|
|
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 (isKuaishouCloudRoleConfirmSettledStatus(context.task.task_status)) {
|
|
return getKuaishouCloudClaimDetail(token)
|
|
}
|
|
|
|
const contextSource = parseTaskContext(context.task)
|
|
const mockMode = isKuaishouCloudMockContext(contextSource)
|
|
const refreshed = mockMode
|
|
? { task: context.task }
|
|
: await refreshKuaishouCloudTaskRoleInfo(context.task, {
|
|
source: 'claim_page_role_confirm',
|
|
actor: { source: 'claim_page' },
|
|
recordEvent: false,
|
|
forceProbe: true,
|
|
})
|
|
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',
|
|
})
|
|
}
|
|
|
|
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',
|
|
})
|
|
}
|
|
|
|
await updateTask(context.task.id, {
|
|
task_status: TASK_STATUS.ROLE_CONFIRMED,
|
|
role_id: flow.binding.roleId,
|
|
role_name: flow.binding.roleName,
|
|
user_action_status: TASK_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 getKuaishouCloudClaimDetail(token)
|
|
}
|
|
|
|
export async function redeemKuaishouCloudClaim(token: unknown) {
|
|
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',
|
|
})
|
|
}
|
|
|
|
const currentStatus = normalizeTaskStatus(context.task.task_status)
|
|
if (isKuaishouCloudRedeemSettledStatus(currentStatus)) {
|
|
return getKuaishouCloudClaimDetail(token)
|
|
}
|
|
|
|
if (!canRedeemKuaishouCloudClaimStatus(currentStatus)) {
|
|
throw createHttpError('请先确认角色信息', {
|
|
statusCode: 409,
|
|
errorCode: 'claim_kuaishou_cloud_role_not_confirmed',
|
|
})
|
|
}
|
|
|
|
const lockedTask = await updateTaskStatusIfCurrent(context.task.id, TASK_STATUS.ROLE_CONFIRMED, {
|
|
task_status: TASK_STATUS.REDEEMING,
|
|
user_action_status: 'not_required',
|
|
last_error: '',
|
|
updated_at: now,
|
|
})
|
|
|
|
if (!lockedTask) {
|
|
return getKuaishouCloudClaimDetail(token)
|
|
}
|
|
|
|
if (isKuaishouCloudMockTask(lockedTask)) {
|
|
await completeMockKuaishouCloudClaimTask(lockedTask, now)
|
|
return getKuaishouCloudClaimDetail(token)
|
|
}
|
|
|
|
try {
|
|
await dispatchKuaishouCloudFulfillmentTask(lockedTask, {
|
|
source: 'claim_page_redeem',
|
|
actor: { source: 'claim_page' },
|
|
autoFinalize: true,
|
|
})
|
|
} catch (error) {
|
|
const latestTask = await getTaskById(lockedTask.id)
|
|
const latestStatus = latestTask ? normalizeTaskStatus(latestTask.task_status) : ''
|
|
if (latestTask && latestStatus !== TASK_STATUS.REDEEMING) {
|
|
if (
|
|
latestStatus === TASK_STATUS.DISPATCHED_PENDING_RETURN ||
|
|
latestStatus === TASK_STATUS.COMPLETED ||
|
|
latestStatus === TASK_STATUS.REDEEMED
|
|
) {
|
|
return getKuaishouCloudClaimDetail(token)
|
|
}
|
|
|
|
throw error
|
|
}
|
|
|
|
const message = error instanceof Error ? error.message : '兑换请求提交失败,请联系客服处理'
|
|
await updateTask(lockedTask.id, {
|
|
task_status: TASK_STATUS.MANUAL_REVIEW,
|
|
user_action_status: 'not_required',
|
|
last_error: message,
|
|
result_code: 'kuaishou_cloud_redeem_failed',
|
|
result_message: message,
|
|
updated_at: nowIso(),
|
|
})
|
|
|
|
await createTaskEvent(
|
|
lockedTask.id,
|
|
'kuaishou_cloud_redeem_failed',
|
|
{
|
|
source: 'claim_page_redeem',
|
|
errorMessage: message,
|
|
},
|
|
nowIso(),
|
|
)
|
|
|
|
throw error
|
|
}
|
|
|
|
return getKuaishouCloudClaimDetail(token)
|
|
}
|
|
|
|
function isKuaishouCloudMockTask(task: Partial<TaskRow> | null | undefined) {
|
|
return isKuaishouCloudMockContext(parseTaskContext(task))
|
|
}
|
|
|
|
function isKuaishouCloudMockContext(context: JsonObject = {}) {
|
|
const flow =
|
|
context.kuaishouCloudFulfillment && typeof context.kuaishouCloudFulfillment === 'object'
|
|
? context.kuaishouCloudFulfillment
|
|
: {}
|
|
const mock = flow.mock && typeof flow.mock === 'object' ? flow.mock : context.mock
|
|
|
|
return Boolean(mock && typeof mock === 'object' && mock.enabled === true)
|
|
}
|
|
|
|
function buildMockVerifiedKuaishouCloudFlow(value: unknown, timestamp: string) {
|
|
const flow = normalizeKuaishouCloudFlow(value)
|
|
const source = flow as JsonObject
|
|
const bindUrl =
|
|
flow.binding.bindUrl ||
|
|
`https://example.com/mock-kuaishou-cloud-bind?task=mock&ts=${encodeURIComponent(timestamp)}`
|
|
const vnPhone = flow.binding.vnPhone || '13800000000'
|
|
const roleName = flow.binding.roleName || flow.role.name || '测试角色'
|
|
const roleId = flow.binding.roleId || flow.role.rid || '10001'
|
|
|
|
return {
|
|
...flow,
|
|
mock: {
|
|
...(source.mock && typeof source.mock === 'object' ? source.mock : {}),
|
|
enabled: true,
|
|
},
|
|
binding: {
|
|
...flow.binding,
|
|
prepareStatus: 'ready',
|
|
vnId: flow.binding.vnId || 900001,
|
|
vnPhone,
|
|
bindUrl,
|
|
bindPreparedAt: flow.binding.bindPreparedAt || timestamp,
|
|
bindExpiresAt: flow.binding.bindExpiresAt || addHours(timestamp, 24),
|
|
roleName,
|
|
roleId,
|
|
},
|
|
role: {
|
|
...flow.role,
|
|
status: 'ready',
|
|
name: roleName,
|
|
rid: roleId,
|
|
refreshedAt: flow.role.refreshedAt || timestamp,
|
|
errorMessage: '',
|
|
rawInfo: flow.role.rawInfo || {
|
|
mock: true,
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
async function completeMockKuaishouCloudClaimTask(task: TaskRow, timestamp: string) {
|
|
const taskContext = parseTaskContext(task)
|
|
const flow = normalizeKuaishouCloudFlow(taskContext.kuaishouCloudFulfillment)
|
|
const source = flow as JsonObject
|
|
const nextFlow = {
|
|
...flow,
|
|
mock: {
|
|
...(source.mock && typeof source.mock === 'object' ? source.mock : {}),
|
|
enabled: true,
|
|
},
|
|
dispatch: {
|
|
...flow.dispatch,
|
|
status: 'success',
|
|
dispatchAt: timestamp,
|
|
dispatchBy: {
|
|
source: 'claim_page_mock',
|
|
},
|
|
note: '开发 mock 已模拟发货成功',
|
|
items: flow.deliveryItems,
|
|
},
|
|
returnNumber: {
|
|
...flow.returnNumber,
|
|
status: 'success',
|
|
returnedAt: timestamp,
|
|
returnedBy: {
|
|
source: 'claim_page_mock',
|
|
},
|
|
},
|
|
consume: {
|
|
...flow.consume,
|
|
status: 'success',
|
|
consumedAt: timestamp,
|
|
errorMessage: '',
|
|
},
|
|
}
|
|
|
|
await updateTask(task.id, {
|
|
task_status: TASK_STATUS.COMPLETED,
|
|
delivery_status: 'success',
|
|
result_code: 'mock_success',
|
|
result_message: '开发 mock 已模拟兑换成功',
|
|
user_action_status: 'not_required',
|
|
last_error: '',
|
|
context_json: JSON.stringify({
|
|
...taskContext,
|
|
kuaishouCloudFulfillment: nextFlow,
|
|
}),
|
|
redeemed_at: timestamp,
|
|
updated_at: timestamp,
|
|
})
|
|
|
|
await createTaskEvent(
|
|
task.id,
|
|
'kuaishou_cloud_mock_redeemed',
|
|
{
|
|
source: 'claim_page_mock',
|
|
deliveryItems: flow.deliveryItems,
|
|
},
|
|
timestamp,
|
|
)
|
|
}
|