优化了一些文件 增加多店铺

This commit is contained in:
yml
2026-04-09 01:31:47 +08:00
parent c2ec3aa6d2
commit e4ab1f559e
43 changed files with 1839 additions and 91 deletions
@@ -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,
}
}