前面的ok了, 绑定与角色
This commit is contained in:
@@ -29,6 +29,7 @@ import { nowIso } from '../../utils/time.js'
|
||||
|
||||
const CLAIM_TERMINAL_STATUSES = new Set(['expired', 'closed'])
|
||||
const REDEEM_REPLACEMENT_LIMIT = 10
|
||||
const KUAISHOU_CLOUD_CLAIM_GUIDE_BASE_PATH = '/kuaishou-cloud-guide'
|
||||
|
||||
export async function getClaimDetail(token, { includeQrImage = true } = {}) {
|
||||
const context = await getClaimContext(token)
|
||||
@@ -574,7 +575,7 @@ export async function getClaimScreenshotPath(token) {
|
||||
return getTencentBrowserSessionScreenshotPath(context.task.browser_session_id)
|
||||
}
|
||||
|
||||
async function getClaimContext(token) {
|
||||
export async function getClaimContext(token) {
|
||||
const normalized = String(token || '').trim()
|
||||
|
||||
if (!normalized) {
|
||||
@@ -818,10 +819,12 @@ function buildClaimDetailPayload({ claimToken, task, order, orderItem, session }
|
||||
const screenshotReady = Boolean(task.screenshot_path) || Boolean(session?.artifacts?.hasScreenshot)
|
||||
const screenshotUrl = screenshotReady ? `/api/v1/claim/${claimToken.token}/screenshot` : ''
|
||||
const finalRedeem = session?.redeem?.final?.redeem || null
|
||||
const kuaishouCloudFulfillment = mapClaimKuaishouCloudFulfillment(task, order)
|
||||
|
||||
return {
|
||||
tokenStatus: claimToken.status,
|
||||
claimUrl: buildClaimUrl(claimToken.token),
|
||||
flowType: kuaishouCloudFulfillment ? 'kuaishou_cloud' : 'tencent_claim',
|
||||
task: {
|
||||
taskId: task.id,
|
||||
taskNo: task.task_no,
|
||||
@@ -853,6 +856,7 @@ function buildClaimDetailPayload({ claimToken, task, order, orderItem, session }
|
||||
quantity: orderItem.quantity,
|
||||
},
|
||||
session,
|
||||
kuaishouCloudFulfillment,
|
||||
result: task.redeemed_at || session?.status === 'redeemed'
|
||||
? {
|
||||
resultCode: String(task.result_code || finalRedeem?.iRet || finalRedeem?.ret || ''),
|
||||
@@ -864,6 +868,62 @@ function buildClaimDetailPayload({ claimToken, task, order, orderItem, session }
|
||||
}
|
||||
}
|
||||
|
||||
function mapClaimKuaishouCloudFulfillment(task, order) {
|
||||
if (String(task?.executor_key || '').trim() !== 'kuaishou_ct_assisted') {
|
||||
return null
|
||||
}
|
||||
|
||||
const source = parseTaskContext(task).kuaishouCloudFulfillment
|
||||
if (!source || typeof source !== 'object') {
|
||||
return null
|
||||
}
|
||||
|
||||
const binding = source.binding && typeof source.binding === 'object' ? source.binding : {}
|
||||
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 : {}
|
||||
const consume = source.consume && typeof source.consume === 'object' ? source.consume : {}
|
||||
|
||||
return {
|
||||
flowType: 'kuaishou_cloud',
|
||||
shopId: String(order?.shop_id || '').trim(),
|
||||
shopName: String(order?.shop_name || '').trim(),
|
||||
guideImages: [
|
||||
`${KUAISHOU_CLOUD_CLAIM_GUIDE_BASE_PATH}/1.png`,
|
||||
`${KUAISHOU_CLOUD_CLAIM_GUIDE_BASE_PATH}/2.png`,
|
||||
`${KUAISHOU_CLOUD_CLAIM_GUIDE_BASE_PATH}/3.png`,
|
||||
],
|
||||
ticket: {
|
||||
code: String(ticket.code || '').trim(),
|
||||
status: String(ticket.status || 'pending').trim() || 'pending',
|
||||
capturedAt: ticket.capturedAt || null,
|
||||
verifiedAt: ticket.verifiedAt || null,
|
||||
goodsTitle: String(ticket.goodsTitle || '').trim(),
|
||||
leftCount: Number(ticket.leftCount || 0) || 0,
|
||||
},
|
||||
binding: {
|
||||
prepareStatus: String(binding.prepareStatus || 'pending').trim() || 'pending',
|
||||
bindUrl: String(binding.bindUrl || '').trim(),
|
||||
bindPreparedAt: binding.bindPreparedAt || null,
|
||||
vnPhone: String(binding.vnPhone || '').trim(),
|
||||
},
|
||||
dispatch: {
|
||||
status: String(dispatch.status || 'pending').trim() || 'pending',
|
||||
dispatchAt: dispatch.dispatchAt || null,
|
||||
note: String(dispatch.note || '').trim(),
|
||||
},
|
||||
returnNumber: {
|
||||
status: String(returnNumber.status || 'pending').trim() || 'pending',
|
||||
returnedAt: returnNumber.returnedAt || null,
|
||||
},
|
||||
consume: {
|
||||
status: String(consume.status || 'pending').trim() || 'pending',
|
||||
consumedAt: consume.consumedAt || null,
|
||||
errorMessage: String(consume.errorMessage || '').trim(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
async function redeemClaimTaskWithInventoryFallback(context, initialInventoryItem) {
|
||||
return redeemClaimTaskWithInventoryFallbackWithDeps(context, initialInventoryItem)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,192 @@
|
||||
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 { updateTask } from '../../repositories/task-repo.js'
|
||||
import { createHttpError } from '../../utils/http.js'
|
||||
import { nowIso } from '../../utils/time.js'
|
||||
import {
|
||||
getKuaishouEticketSourceConfig,
|
||||
resolveKuaishouEticketShopConfig,
|
||||
} from '../platforms/kuaishou-eticket/source-config-service.js'
|
||||
import { queryKuaishouEticketConsumeDetail } from '../platforms/kuaishou-eticket/consume-service.js'
|
||||
import { getClaimContext, getClaimDetail } from './claim-session-service.js'
|
||||
|
||||
const KUAISHOU_CLOUD_GUIDE_DIR = path.resolve(PROJECT_ROOT, '../../tems/imgs')
|
||||
const ALLOWED_GUIDE_FILES = new Set(['1.png', '2.png', '3.png'])
|
||||
|
||||
export async function verifyKuaishouCloudClaimTicket(token, payload = {}) {
|
||||
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 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) {
|
||||
throw createHttpError('请先粘贴快手小店核销码', {
|
||||
statusCode: 400,
|
||||
errorCode: 'claim_kuaishou_cloud_missing_ticket_code',
|
||||
})
|
||||
}
|
||||
|
||||
const shopId = String(flow.consume.shopId || context.order.shop_id || '').trim()
|
||||
const shopConfig = resolveKuaishouEticketShopConfig({
|
||||
shopId,
|
||||
shopName: String(context.order.shop_name || '').trim(),
|
||||
})
|
||||
if (!shopConfig || shopConfig.enabled === false || !String(shopConfig.cookie || '').trim()) {
|
||||
throw createHttpError('这笔订单对应的快手小店还没有配置可用 Cookie,请联系客服处理', {
|
||||
statusCode: 409,
|
||||
errorCode: 'claim_kuaishou_cloud_shop_cookie_missing',
|
||||
})
|
||||
}
|
||||
|
||||
const eticketSource = getKuaishouEticketSourceConfig()
|
||||
const detailResult = await queryKuaishouEticketConsumeDetail({
|
||||
baseUrl: eticketSource.baseUrl,
|
||||
cookie: shopConfig.cookie,
|
||||
eTicketId: ticketCode,
|
||||
})
|
||||
|
||||
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 nextContext = {
|
||||
...parseTaskContext(context.task),
|
||||
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,
|
||||
shopId,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
await updateTask(context.task.id, {
|
||||
claim_token: context.claimToken.token,
|
||||
claim_expires_at: context.claimToken.expired_at,
|
||||
claimed_at: context.task.claimed_at || now,
|
||||
context_json: JSON.stringify(nextContext),
|
||||
last_error: '',
|
||||
updated_at: now,
|
||||
})
|
||||
|
||||
await createTaskEvent(context.task.id, 'kuaishou_cloud_ticket_verified', {
|
||||
ticketCodeMasked: maskCode(detailResult.eTicketId || ticketCode),
|
||||
shopId,
|
||||
shopName: String(context.order.shop_name || '').trim(),
|
||||
goodsTitle: String(detailResult.goods?.itemTitle || '').trim(),
|
||||
}, now)
|
||||
|
||||
return getClaimDetail(token, { includeQrImage: false })
|
||||
}
|
||||
|
||||
export async function getKuaishouCloudClaimGuideAssetPath(filename) {
|
||||
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
|
||||
}
|
||||
|
||||
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',
|
||||
bindUrl: String(binding.bindUrl || '').trim(),
|
||||
},
|
||||
consume: {
|
||||
status: String(consume.status || 'pending').trim() || 'pending',
|
||||
shopId: String(consume.shopId || '').trim(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
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 {}
|
||||
}
|
||||
}
|
||||
|
||||
function maskCode(value) {
|
||||
const normalized = String(value || '').trim()
|
||||
if (normalized.length <= 6) {
|
||||
return normalized
|
||||
}
|
||||
|
||||
return `${normalized.slice(0, 3)}***${normalized.slice(-3)}`
|
||||
}
|
||||
Reference in New Issue
Block a user