半自动+人工”链路已经落下第一版

This commit is contained in:
yml
2026-04-12 19:07:51 +08:00
parent 4c98b78267
commit 75647e9eed
24 changed files with 711 additions and 93 deletions
@@ -5,10 +5,10 @@ import {
getTencentBrowserSessionScreenshotPath,
redeemTencentBrowserSession,
} from '../session/session.js'
import { findClaimTokenByToken, updateClaimToken } from '../../repositories/claim-token-repo.js'
import { findClaimTokenByToken, getClaimTokenById, updateClaimToken } from '../../repositories/claim-token-repo.js'
import { getOrderById } from '../../repositories/order-repo.js'
import { getOrderItemById } from '../../repositories/order-item-repo.js'
import { findTaskByClaimTokenId, updateTask } from '../../repositories/task-repo.js'
import { findTaskByClaimTokenId, getTaskById, updateTask } from '../../repositories/task-repo.js'
import {
getInventoryItemById,
markInventoryItemDelivered,
@@ -24,7 +24,7 @@ const CLAIM_TERMINAL_STATUSES = new Set(['expired', 'closed'])
export async function getClaimDetail(token, { includeQrImage = true } = {}) {
const context = await getClaimContext(token)
const session = await loadTaskSession(context.task, { includeQrImage })
const syncedTask = session ? syncTaskWithSession(context.task, session) : context.task
const syncedTask = session ? await syncTaskWithSession(context.task, session) : context.task
return buildClaimDetailPayload({
claimToken: context.claimToken,
@@ -97,7 +97,18 @@ export async function getClaimSessionSummary(token) {
export async function confirmClaimRole(token) {
const context = await getClaimContext(token)
assertPublicClaimActionAllowed(context.task, 'confirm')
return finalizeClaimRoleConfirmation(context)
}
export async function confirmClaimRoleForAdminTask(taskId) {
const context = await getClaimContextByTaskId(taskId)
return finalizeClaimRoleConfirmation(context)
}
async function finalizeClaimRoleConfirmation(context) {
if (!context.task.browser_session_id) {
throw createHttpError('当前任务还没有创建浏览器会话', {
statusCode: 409,
@@ -140,7 +151,19 @@ export async function confirmClaimRole(token) {
export async function redeemClaimTask(token) {
const context = await getClaimContext(token)
assertTaskCanProceed(context.task)
assertPublicClaimActionAllowed(context.task, 'redeem')
return finalizeClaimTaskRedeem(context)
}
export async function redeemClaimTaskForAdminTask(taskId) {
const context = await getClaimContextByTaskId(taskId)
assertTaskCanProceed(context.task)
return finalizeClaimTaskRedeem(context)
}
async function finalizeClaimTaskRedeem(context) {
if (!context.task.browser_session_id) {
throw createHttpError('当前任务还没有创建浏览器会话', {
statusCode: 409,
@@ -298,6 +321,54 @@ async function getClaimContext(token) {
}
}
async function getClaimContextByTaskId(taskId) {
const task = await getTaskById(Number(taskId))
if (!task) {
throw createHttpError('领取任务不存在', {
statusCode: 404,
errorCode: 'claim_task_not_found',
})
}
const claimTokenId = Number(task.primary_claim_token_id || 0)
if (!claimTokenId) {
throw createHttpError('当前任务还没有领取链接', {
statusCode: 409,
errorCode: 'claim_token_missing',
})
}
const claimToken = await getClaimTokenById(claimTokenId)
if (!claimToken) {
throw createHttpError('领取链接不存在', {
statusCode: 404,
errorCode: 'claim_token_not_found',
})
}
const [order, orderItem] = await Promise.all([
getOrderById(task.order_id),
getOrderItemById(task.order_item_id),
])
if (!order || !orderItem) {
throw createHttpError('领取任务关联订单不完整', {
statusCode: 500,
errorCode: 'claim_order_incomplete',
})
}
return {
claimToken,
task,
order,
orderItem,
}
}
function assertTaskCanProceed(task) {
if (CLAIM_TERMINAL_STATUSES.has(String(task.task_status || ''))) {
throw createHttpError('当前任务已经结束,不能继续操作', {
@@ -307,6 +378,21 @@ function assertTaskCanProceed(task) {
}
}
function assertPublicClaimActionAllowed(task, action) {
if (!isAssistedClaimTask(task)) {
return
}
const message = action === 'confirm'
? '当前商品需要客服复核角色,请登录后联系人工继续'
: '当前商品需要客服确认后再执行兑换,请联系人工继续'
throw createHttpError(message, {
statusCode: 409,
errorCode: 'claim_support_review_required',
})
}
async function expireClaimContext(claimToken, task) {
const now = nowIso()
const nextClaimToken = await updateClaimToken(claimToken.id, {
@@ -370,6 +456,16 @@ async function syncTaskWithSession(task, session) {
patch.claimed_at = task.claimed_at || nowIso()
}
if (session.review?.capturedAt) {
patch.state_json = JSON.stringify({
...parseTaskState(task),
reviewScreenshotReady: true,
reviewCapturedAt: String(session.review.capturedAt || ''),
reviewRoleId: String(session.review.roleId || ''),
reviewRoleName: String(session.review.roleName || ''),
})
}
if (session.status === 'redeemed' && session.artifacts?.hasScreenshot) {
patch.screenshot_path = task.screenshot_path || ''
}
@@ -389,6 +485,8 @@ function buildClaimDetailPayload({ claimToken, task, order, orderItem, session }
taskId: task.id,
taskNo: task.task_no,
status: task.task_status,
executorKey: task.executor_key || '',
requiresSupportReview: isAssistedClaimTask(task),
expiresAt: claimToken.expired_at,
claimedAt: task.claimed_at,
roleConfirmedAt: task.role_confirmed_at,
@@ -424,3 +522,25 @@ function buildClaimDetailPayload({ claimToken, task, order, orderItem, session }
: null,
}
}
function parseTaskState(task) {
const value = task?.state_json
if (!value) {
return {}
}
if (typeof value === 'object') {
return value
}
try {
return JSON.parse(String(value || '{}'))
} catch {
return {}
}
}
function isAssistedClaimTask(task) {
return String(task?.executor_key || '').trim() === 'tencent_claim_assisted'
}