245 lines
8.0 KiB
JavaScript
245 lines
8.0 KiB
JavaScript
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 {
|
|
dispatchKuaishouCloudFulfillmentTask,
|
|
maskCode,
|
|
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')
|
|
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)
|
|
|
|
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,
|
|
},
|
|
},
|
|
}
|
|
|
|
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,
|
|
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)
|
|
|
|
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 })
|
|
}
|
|
|
|
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 parseTaskContext(task) {
|
|
const rawValue = task?.context_json
|
|
if (!rawValue) {
|
|
return {}
|
|
}
|
|
|
|
if (typeof rawValue === 'object') {
|
|
return rawValue
|
|
}
|
|
|
|
try {
|
|
return JSON.parse(String(rawValue || '{}'))
|
|
} catch {
|
|
return {}
|
|
}
|
|
}
|
|
|
|
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',
|
|
})
|
|
}
|
|
|
|
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 })
|
|
}
|