优化了一些文件 增加多店铺
This commit is contained in:
@@ -0,0 +1,30 @@
|
||||
import { runtimeConfig } from '../../config/runtime.js'
|
||||
import { createClaimToken } from '../../repositories/claim-token-repo.js'
|
||||
import { addHours, nowIso } from '../../utils/time.js'
|
||||
import { randomToken } from '../../utils/random.js'
|
||||
|
||||
export function createTaskClaimToken(taskId) {
|
||||
const createdAt = nowIso()
|
||||
const expiredAt = addHours(createdAt, Number(runtimeConfig.orders.tokenTtlHours || 24))
|
||||
const token = createClaimToken({
|
||||
taskId,
|
||||
token: randomToken(24),
|
||||
status: 'active',
|
||||
expiredAt,
|
||||
usedAt: null,
|
||||
maxUseCount: 1,
|
||||
usedCount: 0,
|
||||
createdAt,
|
||||
updatedAt: createdAt,
|
||||
})
|
||||
|
||||
return {
|
||||
...token,
|
||||
claimUrl: buildClaimUrl(token.token),
|
||||
}
|
||||
}
|
||||
|
||||
export function buildClaimUrl(token) {
|
||||
const baseUrl = String(runtimeConfig.orders.claimBaseUrl || '').trim()
|
||||
return baseUrl ? `${baseUrl.replace(/\/+$/, '')}/${token}` : token
|
||||
}
|
||||
@@ -0,0 +1,409 @@
|
||||
import {
|
||||
createTencentBrowserSession,
|
||||
getTencentBrowserSession,
|
||||
getTencentBrowserSessionScreenshotPath,
|
||||
redeemTencentBrowserSession,
|
||||
} from '../session/session.js'
|
||||
import { findClaimTokenByToken, 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 { getCdkById, markCdkDelivered, releaseReservedCdk } from '../../repositories/cdk-repo.js'
|
||||
import { buildClaimUrl } from './claim-service.js'
|
||||
import { createHttpError } from '../../utils/http.js'
|
||||
import { formatFenToAmount, normalizeFen } from '../../utils/money.js'
|
||||
import { nowIso } from '../../utils/time.js'
|
||||
|
||||
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
|
||||
|
||||
return buildClaimDetailPayload({
|
||||
claimToken: context.claimToken,
|
||||
task: syncedTask,
|
||||
order: context.order,
|
||||
orderItem: context.orderItem,
|
||||
session,
|
||||
})
|
||||
}
|
||||
|
||||
export async function createClaimSession(token, payload = {}) {
|
||||
const context = await getClaimContext(token)
|
||||
assertTaskCanProceed(context.task)
|
||||
|
||||
if (context.task.browser_session_id) {
|
||||
const existingSession = await getTencentBrowserSession(context.task.browser_session_id)
|
||||
const syncedTask = syncTaskWithSession(context.task, existingSession)
|
||||
|
||||
return buildClaimDetailPayload({
|
||||
claimToken: context.claimToken,
|
||||
task: syncedTask,
|
||||
order: context.order,
|
||||
orderItem: context.orderItem,
|
||||
session: existingSession,
|
||||
})
|
||||
}
|
||||
|
||||
const session = await createTencentBrowserSession({
|
||||
loginType: payload.loginType,
|
||||
})
|
||||
const updatedTask = updateTask(context.task.id, {
|
||||
task_status: 'claimed',
|
||||
browser_session_id: session.sessionId,
|
||||
login_type: session.loginType,
|
||||
claimed_at: context.task.claimed_at || nowIso(),
|
||||
updated_at: nowIso(),
|
||||
})
|
||||
|
||||
return buildClaimDetailPayload({
|
||||
claimToken: context.claimToken,
|
||||
task: updatedTask,
|
||||
order: context.order,
|
||||
orderItem: context.orderItem,
|
||||
session,
|
||||
})
|
||||
}
|
||||
|
||||
export async function getClaimSessionSummary(token) {
|
||||
const context = await getClaimContext(token)
|
||||
|
||||
if (!context.task.browser_session_id) {
|
||||
throw createHttpError('当前任务还没有创建浏览器会话', {
|
||||
statusCode: 409,
|
||||
errorCode: 'claim_session_not_created',
|
||||
})
|
||||
}
|
||||
|
||||
const session = await getTencentBrowserSession(context.task.browser_session_id)
|
||||
const syncedTask = syncTaskWithSession(context.task, session)
|
||||
|
||||
return buildClaimDetailPayload({
|
||||
claimToken: context.claimToken,
|
||||
task: syncedTask,
|
||||
order: context.order,
|
||||
orderItem: context.orderItem,
|
||||
session,
|
||||
})
|
||||
}
|
||||
|
||||
export async function confirmClaimRole(token) {
|
||||
const context = await getClaimContext(token)
|
||||
|
||||
if (!context.task.browser_session_id) {
|
||||
throw createHttpError('当前任务还没有创建浏览器会话', {
|
||||
statusCode: 409,
|
||||
errorCode: 'claim_session_not_created',
|
||||
})
|
||||
}
|
||||
|
||||
const session = await getTencentBrowserSession(context.task.browser_session_id)
|
||||
const activityInfo = session.activityInfo || null
|
||||
|
||||
if (!activityInfo?.role?.ready) {
|
||||
throw createHttpError('当前角色信息还没有准备好', {
|
||||
statusCode: 409,
|
||||
errorCode: 'claim_role_not_ready',
|
||||
})
|
||||
}
|
||||
|
||||
const updatedTask = updateTask(context.task.id, {
|
||||
task_status: 'role_confirmed',
|
||||
nickname: String(activityInfo.nickname || ''),
|
||||
role_id: String(activityInfo.role.roleId || ''),
|
||||
role_name: String(activityInfo.role.roleName || ''),
|
||||
area: String(activityInfo.role.area || ''),
|
||||
partition_name: String(activityInfo.role.partition || ''),
|
||||
role_confirmed_at: nowIso(),
|
||||
updated_at: nowIso(),
|
||||
last_error: '',
|
||||
})
|
||||
|
||||
return buildClaimDetailPayload({
|
||||
claimToken: context.claimToken,
|
||||
task: updatedTask,
|
||||
order: context.order,
|
||||
orderItem: context.orderItem,
|
||||
session,
|
||||
})
|
||||
}
|
||||
|
||||
export async function redeemClaimTask(token) {
|
||||
const context = await getClaimContext(token)
|
||||
assertTaskCanProceed(context.task)
|
||||
|
||||
if (!context.task.browser_session_id) {
|
||||
throw createHttpError('当前任务还没有创建浏览器会话', {
|
||||
statusCode: 409,
|
||||
errorCode: 'claim_session_not_created',
|
||||
})
|
||||
}
|
||||
|
||||
if (context.task.task_status !== 'role_confirmed' && context.task.task_status !== 'redeeming') {
|
||||
throw createHttpError('当前任务还未确认角色,不能开始兑换', {
|
||||
statusCode: 409,
|
||||
errorCode: 'claim_role_not_confirmed',
|
||||
})
|
||||
}
|
||||
|
||||
const cdk = context.task.reserved_cdk_id ? getCdkById(context.task.reserved_cdk_id) : null
|
||||
|
||||
if (!cdk || !String(cdk.cdk_code || '').trim()) {
|
||||
throw createHttpError('当前任务没有可用的预占 CDK', {
|
||||
statusCode: 409,
|
||||
errorCode: 'claim_cdk_not_reserved',
|
||||
})
|
||||
}
|
||||
|
||||
updateTask(context.task.id, {
|
||||
task_status: 'redeeming',
|
||||
updated_at: nowIso(),
|
||||
last_error: '',
|
||||
})
|
||||
|
||||
try {
|
||||
const session = await redeemTencentBrowserSession(context.task.browser_session_id, {
|
||||
code: cdk.cdk_code,
|
||||
})
|
||||
const finalRedeem = session.redeem?.final?.redeem || null
|
||||
const updatedTask = updateTask(context.task.id, {
|
||||
task_status: 'redeemed',
|
||||
result_code: String(finalRedeem?.iRet || finalRedeem?.ret || '0'),
|
||||
result_message: String(finalRedeem?.sMsg || finalRedeem?.msg || session.notice || '兑换完成'),
|
||||
screenshot_path: session.artifacts?.hasScreenshot ? await getTencentBrowserSessionScreenshotPath(session.sessionId) : '',
|
||||
artifacts_json: JSON.stringify(session.artifacts || {}),
|
||||
redeemed_at: nowIso(),
|
||||
updated_at: nowIso(),
|
||||
last_error: '',
|
||||
})
|
||||
|
||||
markCdkDelivered(cdk.id, nowIso())
|
||||
|
||||
return buildClaimDetailPayload({
|
||||
claimToken: context.claimToken,
|
||||
task: updatedTask,
|
||||
order: context.order,
|
||||
orderItem: context.orderItem,
|
||||
session,
|
||||
})
|
||||
} catch (error) {
|
||||
const nextRetryCount = Number(context.task.retry_count || 0) + 1
|
||||
const failedTask = updateTask(context.task.id, {
|
||||
task_status: 'retry_pending',
|
||||
retry_count: nextRetryCount,
|
||||
last_error: error instanceof Error ? error.message : String(error || ''),
|
||||
updated_at: nowIso(),
|
||||
})
|
||||
|
||||
throw Object.assign(error instanceof Error ? error : new Error(String(error || '兑换失败')), {
|
||||
task: failedTask,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export async function getClaimScreenshotPath(token) {
|
||||
const context = await getClaimContext(token)
|
||||
|
||||
if (context.task.screenshot_path) {
|
||||
return context.task.screenshot_path
|
||||
}
|
||||
|
||||
if (!context.task.browser_session_id) {
|
||||
throw createHttpError('当前任务还没有兑换截图', {
|
||||
statusCode: 404,
|
||||
errorCode: 'claim_screenshot_not_ready',
|
||||
})
|
||||
}
|
||||
|
||||
return getTencentBrowserSessionScreenshotPath(context.task.browser_session_id)
|
||||
}
|
||||
|
||||
async function getClaimContext(token) {
|
||||
const normalized = String(token || '').trim()
|
||||
|
||||
if (!normalized) {
|
||||
throw createHttpError('缺少领取 token', {
|
||||
statusCode: 400,
|
||||
errorCode: 'missing_claim_token',
|
||||
})
|
||||
}
|
||||
|
||||
const claimToken = findClaimTokenByToken(normalized)
|
||||
|
||||
if (!claimToken) {
|
||||
throw createHttpError('领取链接无效或不存在', {
|
||||
statusCode: 404,
|
||||
errorCode: 'claim_token_not_found',
|
||||
})
|
||||
}
|
||||
|
||||
const task = findTaskByClaimTokenId(claimToken.id)
|
||||
|
||||
if (!task) {
|
||||
throw createHttpError('领取任务不存在', {
|
||||
statusCode: 404,
|
||||
errorCode: 'claim_task_not_found',
|
||||
})
|
||||
}
|
||||
|
||||
if (claimToken.status !== 'active') {
|
||||
throw createHttpError('领取链接当前不可用', {
|
||||
statusCode: 410,
|
||||
errorCode: 'claim_token_inactive',
|
||||
})
|
||||
}
|
||||
|
||||
if (claimToken.expired_at && new Date(claimToken.expired_at).getTime() <= Date.now()) {
|
||||
const expiredContext = expireClaimContext(claimToken, task)
|
||||
|
||||
throw createHttpError('领取链接已过期', {
|
||||
statusCode: 410,
|
||||
errorCode: 'claim_token_expired',
|
||||
context: expiredContext,
|
||||
})
|
||||
}
|
||||
|
||||
const order = getOrderById(task.order_id)
|
||||
const orderItem = 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('当前任务已经结束,不能继续操作', {
|
||||
statusCode: 410,
|
||||
errorCode: 'claim_task_closed',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function expireClaimContext(claimToken, task) {
|
||||
const now = nowIso()
|
||||
const nextClaimToken = updateClaimToken(claimToken.id, {
|
||||
status: 'expired',
|
||||
updated_at: now,
|
||||
})
|
||||
|
||||
let nextTask = task
|
||||
|
||||
if (!CLAIM_TERMINAL_STATUSES.has(String(task.task_status || '')) && task.task_status !== 'redeemed') {
|
||||
if (task.reserved_cdk_id) {
|
||||
releaseReservedCdk(task.reserved_cdk_id, now)
|
||||
}
|
||||
|
||||
nextTask = updateTask(task.id, {
|
||||
task_status: 'expired',
|
||||
reserved_cdk_id: null,
|
||||
last_error: '领取链接已过期,预占 CDK 已释放',
|
||||
updated_at: now,
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
claimToken: nextClaimToken,
|
||||
task: nextTask,
|
||||
}
|
||||
}
|
||||
|
||||
async function loadTaskSession(task, { includeQrImage = false } = {}) {
|
||||
if (!task.browser_session_id) {
|
||||
return null
|
||||
}
|
||||
|
||||
return includeQrImage
|
||||
? getTencentBrowserSession(task.browser_session_id)
|
||||
: getTencentBrowserSessionSummary(task.browser_session_id)
|
||||
}
|
||||
|
||||
function syncTaskWithSession(task, session) {
|
||||
const activityInfo = session.activityInfo || null
|
||||
const patch = {
|
||||
login_type: String(session.loginType || task.login_type || ''),
|
||||
updated_at: nowIso(),
|
||||
}
|
||||
|
||||
if (activityInfo?.nickname) {
|
||||
patch.nickname = String(activityInfo.nickname)
|
||||
}
|
||||
|
||||
if (activityInfo?.role?.ready) {
|
||||
patch.role_id = String(activityInfo.role.roleId || '')
|
||||
patch.role_name = String(activityInfo.role.roleName || '')
|
||||
patch.area = String(activityInfo.role.area || '')
|
||||
patch.partition_name = String(activityInfo.role.partition || '')
|
||||
}
|
||||
|
||||
if (task.task_status === 'link_generated') {
|
||||
patch.task_status = 'claimed'
|
||||
patch.claimed_at = task.claimed_at || nowIso()
|
||||
}
|
||||
|
||||
if (session.status === 'redeemed' && session.artifacts?.hasScreenshot) {
|
||||
patch.screenshot_path = task.screenshot_path || ''
|
||||
}
|
||||
|
||||
return updateTask(task.id, patch)
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
return {
|
||||
tokenStatus: claimToken.status,
|
||||
claimUrl: buildClaimUrl(claimToken.token),
|
||||
task: {
|
||||
taskId: task.id,
|
||||
taskNo: task.task_no,
|
||||
status: task.task_status,
|
||||
expiresAt: task.expires_at,
|
||||
claimedAt: task.claimed_at,
|
||||
roleConfirmedAt: task.role_confirmed_at,
|
||||
redeemedAt: task.redeemed_at,
|
||||
loginType: task.login_type,
|
||||
lastError: task.last_error,
|
||||
browserSessionId: task.browser_session_id,
|
||||
},
|
||||
order: {
|
||||
orderId: order.id,
|
||||
platform: order.platform,
|
||||
platformOrderId: order.platform_order_id,
|
||||
payStatus: order.pay_status,
|
||||
orderStatus: order.order_status,
|
||||
totalAmount: formatFenToAmount(order.total_amount),
|
||||
totalAmountFen: normalizeFen(order.total_amount),
|
||||
currency: order.currency,
|
||||
},
|
||||
orderItem: {
|
||||
orderItemId: orderItem.id,
|
||||
skuCode: orderItem.sku_code,
|
||||
skuName: orderItem.sku_name,
|
||||
quantity: orderItem.quantity,
|
||||
},
|
||||
session,
|
||||
result: task.redeemed_at || session?.status === 'redeemed'
|
||||
? {
|
||||
resultCode: String(task.result_code || finalRedeem?.iRet || finalRedeem?.ret || ''),
|
||||
resultMessage: String(task.result_message || finalRedeem?.sMsg || finalRedeem?.msg || session?.notice || ''),
|
||||
screenshotReady,
|
||||
screenshotUrl,
|
||||
}
|
||||
: null,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user