增加人工渠道
This commit is contained in:
@@ -4,6 +4,7 @@ import authRouter from './admin/auth.js'
|
||||
import auditLogsRouter from './admin/audit-logs.js'
|
||||
import dashboardRouter from './admin/dashboard.js'
|
||||
import inventoryRouter from './admin/inventory.js'
|
||||
import manualRedeemRouter from './admin/manual-redeem.js'
|
||||
import messageDeliveriesRouter from './admin/message-deliveries.js'
|
||||
import ordersRouter from './admin/orders.js'
|
||||
import platformConfigRouter from './admin/platform-config.js'
|
||||
@@ -23,6 +24,7 @@ router.use(auditLogsRouter)
|
||||
router.use(platformConfigRouter)
|
||||
router.use(ordersRouter)
|
||||
router.use(tasksRouter)
|
||||
router.use(manualRedeemRouter)
|
||||
router.use(inventoryRouter)
|
||||
router.use(messageDeliveriesRouter)
|
||||
router.use(webhookEventsRouter)
|
||||
|
||||
@@ -0,0 +1,183 @@
|
||||
// @ts-check
|
||||
|
||||
import { Router } from 'express'
|
||||
|
||||
import {
|
||||
closeAdminManualRedeemSession,
|
||||
closeAdminManualRedeemTask,
|
||||
confirmAdminManualRedeemRole,
|
||||
createAdminManualRedeemSession,
|
||||
createAdminManualRedeemTask,
|
||||
getAdminManualRedeemDetail,
|
||||
getAdminManualRedeemSessionSummary,
|
||||
reloadAdminManualRedeemSession,
|
||||
redeemAdminManualRedeemTask,
|
||||
} from '../../services/admin/admin-manual-redeem-service.js'
|
||||
import { getAdminInventorySkuSuggestions } from '../../services/admin/admin-read-service.js'
|
||||
import { createJsonHandler, requireAdminRoles } from './shared.js'
|
||||
|
||||
/** @typedef {import('../../types/admin-route-inputs.js').AdminInventorySkuSuggestionRouteQuery} AdminInventorySkuSuggestionRouteQuery */
|
||||
/** @typedef {import('../../types/admin-route-inputs.js').AdminManualRedeemCreateRouteBody} AdminManualRedeemCreateRouteBody */
|
||||
/** @typedef {import('../../types/admin-route-inputs.js').AdminManualRedeemRouteParams} AdminManualRedeemRouteParams */
|
||||
|
||||
const router = Router()
|
||||
|
||||
router.use('/manual-redeem', requireAdminRoles(['admin', 'operator', 'support']))
|
||||
|
||||
router.get('/manual-redeem/sku-suggestions', createJsonHandler(
|
||||
(req) => getAdminInventorySkuSuggestions(/** @type {AdminInventorySkuSuggestionRouteQuery} */ (req.query)),
|
||||
{
|
||||
successMessage: 'ok',
|
||||
errorMessage: '读取人工兑换 SKU 建议失败',
|
||||
scope: '[admin/manual-redeem/sku-suggestions]',
|
||||
},
|
||||
))
|
||||
|
||||
router.post('/manual-redeem', createJsonHandler(
|
||||
(req) => createAdminManualRedeemTask(
|
||||
/** @type {AdminManualRedeemCreateRouteBody} */ (req.body),
|
||||
req.adminSession || null,
|
||||
),
|
||||
{
|
||||
successMessage: '人工兑换任务已创建',
|
||||
errorMessage: '创建人工兑换任务失败',
|
||||
scope: '[admin/manual-redeem]',
|
||||
audit: (req, data) => {
|
||||
const result = /** @type {{ task?: { taskId?: number, taskNo?: string }, manualRequest?: { proofValue?: string }, order?: { platformOrderId?: string }, orderItem?: { skuCode?: string } }} */ (data)
|
||||
return {
|
||||
action: 'manual_redeem_created',
|
||||
targetType: 'task',
|
||||
targetId: String(result.task?.taskId || ''),
|
||||
data: {
|
||||
taskId: result.task?.taskId,
|
||||
taskNo: result.task?.taskNo,
|
||||
proofValue: result.manualRequest?.proofValue || result.order?.platformOrderId || '',
|
||||
skuCode: result.orderItem?.skuCode || '',
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
))
|
||||
|
||||
router.get('/manual-redeem/:taskId', createJsonHandler(
|
||||
(req) => getAdminManualRedeemDetail((/** @type {AdminManualRedeemRouteParams} */ (req.params)).taskId),
|
||||
{
|
||||
successMessage: 'ok',
|
||||
errorMessage: '读取人工兑换详情失败',
|
||||
scope: '[admin/manual-redeem/:taskId]',
|
||||
},
|
||||
))
|
||||
|
||||
router.post('/manual-redeem/:taskId/session', createJsonHandler(
|
||||
(req) => createAdminManualRedeemSession(
|
||||
(/** @type {AdminManualRedeemRouteParams} */ (req.params)).taskId,
|
||||
/** @type {{ loginType?: string, forceRecreate?: boolean }} */ (req.body),
|
||||
),
|
||||
{
|
||||
successMessage: '人工兑换登录会话已创建',
|
||||
errorMessage: '创建人工兑换登录会话失败',
|
||||
scope: '[admin/manual-redeem/:taskId/session]',
|
||||
},
|
||||
))
|
||||
|
||||
router.get('/manual-redeem/:taskId/session/summary', createJsonHandler(
|
||||
(req) => getAdminManualRedeemSessionSummary((/** @type {AdminManualRedeemRouteParams} */ (req.params)).taskId),
|
||||
{
|
||||
successMessage: 'ok',
|
||||
errorMessage: '读取人工兑换会话摘要失败',
|
||||
scope: '[admin/manual-redeem/:taskId/session/summary]',
|
||||
},
|
||||
))
|
||||
|
||||
router.post('/manual-redeem/:taskId/session/refresh', createJsonHandler(
|
||||
(req) => reloadAdminManualRedeemSession((/** @type {AdminManualRedeemRouteParams} */ (req.params)).taskId),
|
||||
{
|
||||
successMessage: '人工兑换会话已刷新',
|
||||
errorMessage: '刷新人工兑换会话失败',
|
||||
scope: '[admin/manual-redeem/:taskId/session/refresh]',
|
||||
},
|
||||
))
|
||||
|
||||
router.delete('/manual-redeem/:taskId/session', createJsonHandler(
|
||||
(req) => closeAdminManualRedeemSession((/** @type {AdminManualRedeemRouteParams} */ (req.params)).taskId),
|
||||
{
|
||||
successMessage: '人工兑换会话已关闭',
|
||||
errorMessage: '关闭人工兑换会话失败',
|
||||
scope: '[admin/manual-redeem/:taskId/session]',
|
||||
},
|
||||
))
|
||||
|
||||
router.post('/manual-redeem/:taskId/confirm-role', createJsonHandler(
|
||||
(req) => confirmAdminManualRedeemRole(
|
||||
(/** @type {AdminManualRedeemRouteParams} */ (req.params)).taskId,
|
||||
req.adminSession || null,
|
||||
),
|
||||
{
|
||||
successMessage: '角色已确认',
|
||||
errorMessage: '确认人工兑换角色失败',
|
||||
scope: '[admin/manual-redeem/:taskId/confirm-role]',
|
||||
audit: (req, data) => {
|
||||
const result = /** @type {{ task?: { taskId?: number, taskNo?: string, status?: string } }} */ (data)
|
||||
return {
|
||||
action: 'manual_redeem_role_confirmed',
|
||||
targetType: 'task',
|
||||
targetId: String(result.task?.taskId || ''),
|
||||
data: {
|
||||
taskId: result.task?.taskId,
|
||||
taskNo: result.task?.taskNo,
|
||||
status: result.task?.status,
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
))
|
||||
|
||||
router.post('/manual-redeem/:taskId/redeem', createJsonHandler(
|
||||
(req) => redeemAdminManualRedeemTask(
|
||||
(/** @type {AdminManualRedeemRouteParams} */ (req.params)).taskId,
|
||||
req.adminSession || null,
|
||||
),
|
||||
{
|
||||
successMessage: '人工兑换任务已启动',
|
||||
errorMessage: '执行人工兑换失败',
|
||||
scope: '[admin/manual-redeem/:taskId/redeem]',
|
||||
audit: (req, data) => {
|
||||
const result = /** @type {{ task?: { taskId?: number, taskNo?: string, status?: string, deliveryStatus?: string } }} */ (data)
|
||||
return {
|
||||
action: 'manual_redeem_started',
|
||||
targetType: 'task',
|
||||
targetId: String(result.task?.taskId || ''),
|
||||
data: {
|
||||
taskId: result.task?.taskId,
|
||||
taskNo: result.task?.taskNo,
|
||||
status: result.task?.status,
|
||||
deliveryStatus: result.task?.deliveryStatus,
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
))
|
||||
|
||||
router.post('/manual-redeem/:taskId/close', createJsonHandler(
|
||||
(req) => closeAdminManualRedeemTask((/** @type {AdminManualRedeemRouteParams} */ (req.params)).taskId),
|
||||
{
|
||||
successMessage: '人工兑换任务已关闭',
|
||||
errorMessage: '关闭人工兑换任务失败',
|
||||
scope: '[admin/manual-redeem/:taskId/close]',
|
||||
audit: (req, data) => {
|
||||
const result = /** @type {{ task?: { taskId?: number, taskNo?: string, status?: string } }} */ (data)
|
||||
return {
|
||||
action: 'manual_redeem_closed',
|
||||
targetType: 'task',
|
||||
targetId: String(result.task?.taskId || ''),
|
||||
data: {
|
||||
taskId: result.task?.taskId,
|
||||
taskNo: result.task?.taskNo,
|
||||
status: result.task?.status,
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
))
|
||||
|
||||
export default router
|
||||
@@ -0,0 +1,376 @@
|
||||
// @ts-check
|
||||
|
||||
import { createTaskClaimToken } from '../claim/claim-service.js'
|
||||
import {
|
||||
closeClaimSessionForAdminTask,
|
||||
createClaimSessionForAdminTask,
|
||||
getClaimDetailForAdminTask,
|
||||
getClaimSessionSummaryForAdminTask,
|
||||
reloadClaimSessionForAdminTask,
|
||||
} from '../claim/claim-session-service.js'
|
||||
import { reserveInventoryForTask } from '../order/inventory-service.js'
|
||||
import { createHttpError } from '../../utils/http.js'
|
||||
import { replaceOrderItems } from '../../repositories/order-item-repo.js'
|
||||
import { createOrder, findOrderByPlatformOrderId } from '../../repositories/order-repo.js'
|
||||
import { getFulfillmentProfileByKey, listFulfillmentProfileRequirements } from '../../repositories/fulfillment-profile-repo.js'
|
||||
import { findFirstAvailableInventoryItemBySkuCode } from '../../repositories/inventory-repo.js'
|
||||
import { createTask, getTaskById, updateTask } from '../../repositories/task-repo.js'
|
||||
import { createTaskEvent } from '../../repositories/task-event-repo.js'
|
||||
import { nowIso } from '../../utils/time.js'
|
||||
import { randomId } from '../../utils/random.js'
|
||||
import { closeAdminTask, confirmAdminTaskAssistedRole, redeemAdminTaskAssisted } from './admin-write-service.js'
|
||||
import { isAssistedClaimTask, parseTaskContext } from './admin-read-shared-helpers.js'
|
||||
|
||||
const MANUAL_PROVIDER = 'manual'
|
||||
const MANUAL_PLATFORM = 'manual_redeem'
|
||||
const MANUAL_SOURCE_TYPE = 'admin_manual_redeem'
|
||||
const ASSISTED_PROFILE_KEY = 'tencent_claim_assisted'
|
||||
|
||||
/** @typedef {import('../../types/admin-read-inputs.js').AdminEntityIdInput} AdminEntityIdInput */
|
||||
/** @typedef {import('../../types/admin-read-inputs.js').AdminViewerSessionInput} AdminViewerSessionInput */
|
||||
/** @typedef {import('../../types/admin-write-inputs.js').AdminManualRedeemCreateInput} AdminManualRedeemCreateInput */
|
||||
|
||||
/** @param {AdminManualRedeemCreateInput} [payload] */
|
||||
/** @param {AdminViewerSessionInput | null} [session] */
|
||||
export async function createAdminManualRedeemTask(
|
||||
payload = /** @type {AdminManualRedeemCreateInput} */ ({}),
|
||||
session = null,
|
||||
) {
|
||||
const proofValue = normalizeManualProofValue(payload.proofValue)
|
||||
const skuCode = String(payload.skuCode || '').trim()
|
||||
const skuName = String(payload.skuName || '').trim() || skuCode
|
||||
const remark = String(payload.remark || '').trim()
|
||||
|
||||
if (!proofValue) {
|
||||
throw createHttpError('请先输入唯一凭据', {
|
||||
statusCode: 400,
|
||||
errorCode: 'admin_manual_redeem_missing_proof',
|
||||
})
|
||||
}
|
||||
|
||||
if (!skuCode) {
|
||||
throw createHttpError('请先选择内部履约 SKU', {
|
||||
statusCode: 400,
|
||||
errorCode: 'admin_manual_redeem_missing_sku',
|
||||
})
|
||||
}
|
||||
|
||||
const existingOrder = await findOrderByPlatformOrderId({
|
||||
provider: MANUAL_PROVIDER,
|
||||
platform: MANUAL_PLATFORM,
|
||||
shopId: '',
|
||||
platformOrderId: proofValue,
|
||||
})
|
||||
|
||||
if (existingOrder) {
|
||||
throw createHttpError('该唯一凭据已经创建过人工兑换任务,请勿重复提交', {
|
||||
statusCode: 409,
|
||||
errorCode: 'admin_manual_redeem_duplicate_proof',
|
||||
})
|
||||
}
|
||||
|
||||
const profile = await getFulfillmentProfileByKey(ASSISTED_PROFILE_KEY)
|
||||
if (!profile) {
|
||||
throw createHttpError('人工兑换所需的履约档案不存在,请先检查系统初始化', {
|
||||
statusCode: 500,
|
||||
errorCode: 'admin_manual_redeem_profile_missing',
|
||||
})
|
||||
}
|
||||
|
||||
const requirements = await listFulfillmentProfileRequirements(profile.id)
|
||||
const primaryRequirement = requirements.find((item) => item.is_required !== false) || requirements[0] || null
|
||||
const credentialType = String(primaryRequirement?.credential_type || primaryRequirement?.credentialType || 'tencent_code').trim() || 'tencent_code'
|
||||
const roleKey = String(primaryRequirement?.role_key || primaryRequirement?.roleKey || 'primary_code').trim() || 'primary_code'
|
||||
const availableInventory = await findFirstAvailableInventoryItemBySkuCode(skuCode, credentialType)
|
||||
|
||||
if (!availableInventory) {
|
||||
throw createHttpError('当前 SKU 没有可用库存,无法创建人工兑换任务', {
|
||||
statusCode: 409,
|
||||
errorCode: 'admin_manual_redeem_inventory_unavailable',
|
||||
})
|
||||
}
|
||||
|
||||
const now = nowIso()
|
||||
const manualContext = {
|
||||
sourceType: MANUAL_SOURCE_TYPE,
|
||||
proofValue,
|
||||
remark,
|
||||
createdAt: now,
|
||||
createdBy: session
|
||||
? {
|
||||
userId: Number(session.userId || 0) || 0,
|
||||
username: String(session.username || '').trim(),
|
||||
role: String(session.role || '').trim(),
|
||||
}
|
||||
: null,
|
||||
}
|
||||
|
||||
let order
|
||||
try {
|
||||
order = await createOrder({
|
||||
provider: MANUAL_PROVIDER,
|
||||
platform: MANUAL_PLATFORM,
|
||||
shopId: '',
|
||||
shopName: '人工兑换',
|
||||
platformOrderId: proofValue,
|
||||
orderStatus: 'manual_created',
|
||||
payStatus: 'paid',
|
||||
buyerId: '',
|
||||
buyerName: '',
|
||||
receiverContact: '',
|
||||
totalAmount: 0,
|
||||
currency: 'CNY',
|
||||
rawPayloadJson: JSON.stringify({
|
||||
manualRedeem: manualContext,
|
||||
}),
|
||||
paidAt: now,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
})
|
||||
} catch (error) {
|
||||
if (String(error?.code || '') === '23505') {
|
||||
throw createHttpError('该唯一凭据已经创建过人工兑换任务,请勿重复提交', {
|
||||
statusCode: 409,
|
||||
errorCode: 'admin_manual_redeem_duplicate_proof',
|
||||
})
|
||||
}
|
||||
|
||||
throw error
|
||||
}
|
||||
|
||||
if (!order) {
|
||||
throw createHttpError('人工兑换订单创建失败', {
|
||||
statusCode: 500,
|
||||
errorCode: 'admin_manual_redeem_order_create_failed',
|
||||
})
|
||||
}
|
||||
|
||||
const orderItems = await replaceOrderItems(order.id, [
|
||||
{
|
||||
skuCode,
|
||||
skuName,
|
||||
quantity: 1,
|
||||
specJson: JSON.stringify({
|
||||
title: skuName,
|
||||
sourceType: MANUAL_SOURCE_TYPE,
|
||||
}),
|
||||
itemSnapshotJson: JSON.stringify({
|
||||
manualRedeem: manualContext,
|
||||
skuCode,
|
||||
skuName,
|
||||
}),
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
},
|
||||
])
|
||||
|
||||
const orderItem = orderItems[0]
|
||||
if (!orderItem) {
|
||||
throw createHttpError('人工兑换订单商品创建失败', {
|
||||
statusCode: 500,
|
||||
errorCode: 'admin_manual_redeem_order_item_create_failed',
|
||||
})
|
||||
}
|
||||
|
||||
const createdTask = await createTask({
|
||||
orderId: order.id,
|
||||
orderItemId: orderItem.id,
|
||||
unitIndex: 1,
|
||||
provider: MANUAL_PROVIDER,
|
||||
platform: MANUAL_PLATFORM,
|
||||
shopId: '',
|
||||
shopName: '人工兑换',
|
||||
platformOrderId: proofValue,
|
||||
taskNo: randomId('MR'),
|
||||
profileId: Number(profile.id),
|
||||
executorKey: String(profile.executor_key || ASSISTED_PROFILE_KEY),
|
||||
taskStatus: 'paid',
|
||||
inventoryStatus: 'pending',
|
||||
deliveryStatus: 'pending',
|
||||
resultCode: '',
|
||||
resultMessage: '',
|
||||
claimToken: '',
|
||||
claimExpiresAt: null,
|
||||
automationMode: 'manual',
|
||||
requiresClaim: true,
|
||||
userActionStatus: 'pending_claim',
|
||||
attemptCount: 0,
|
||||
lastError: '',
|
||||
contextJson: JSON.stringify({
|
||||
profileKey: String(profile.profile_key || ASSISTED_PROFILE_KEY),
|
||||
profileName: String(profile.name || ''),
|
||||
skuCode,
|
||||
skuName,
|
||||
inventorySkuCode: skuCode,
|
||||
primaryRequirement: primaryRequirement
|
||||
? {
|
||||
roleKey,
|
||||
credentialType,
|
||||
}
|
||||
: null,
|
||||
manualRedeem: manualContext,
|
||||
}),
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
})
|
||||
|
||||
if (!createdTask) {
|
||||
throw createHttpError('人工兑换任务创建失败', {
|
||||
statusCode: 500,
|
||||
errorCode: 'admin_manual_redeem_task_create_failed',
|
||||
})
|
||||
}
|
||||
|
||||
const reservedInventory = await reserveInventoryForTask({
|
||||
skuCode,
|
||||
taskId: createdTask.id,
|
||||
credentialType,
|
||||
roleKey,
|
||||
})
|
||||
|
||||
if (!reservedInventory) {
|
||||
await updateTask(createdTask.id, {
|
||||
task_status: 'waiting_inventory',
|
||||
inventory_status: 'pending',
|
||||
last_error: '库存不足,无法为人工兑换任务预占库存',
|
||||
updated_at: nowIso(),
|
||||
})
|
||||
|
||||
throw createHttpError('库存刚刚被其他任务占用,请重新选择或稍后再试', {
|
||||
statusCode: 409,
|
||||
errorCode: 'admin_manual_redeem_inventory_race_lost',
|
||||
})
|
||||
}
|
||||
|
||||
const claimToken = await createTaskClaimToken(createdTask.id)
|
||||
await updateTask(createdTask.id, {
|
||||
task_status: 'link_generated',
|
||||
inventory_status: 'reserved',
|
||||
claim_token: claimToken.token,
|
||||
claim_expires_at: claimToken.expired_at,
|
||||
user_action_status: 'pending_claim',
|
||||
last_error: '',
|
||||
updated_at: nowIso(),
|
||||
})
|
||||
|
||||
await createTaskEvent(createdTask.id, 'manual_redeem_created', {
|
||||
sourceType: MANUAL_SOURCE_TYPE,
|
||||
proofValue,
|
||||
skuCode,
|
||||
skuName,
|
||||
inventoryItemId: Number(reservedInventory.id || 0) || null,
|
||||
createdBy: manualContext.createdBy,
|
||||
remark,
|
||||
}, nowIso())
|
||||
|
||||
return getAdminManualRedeemDetail(createdTask.id)
|
||||
}
|
||||
|
||||
/** @param {AdminEntityIdInput} taskId */
|
||||
export async function getAdminManualRedeemDetail(taskId) {
|
||||
const task = await getRequiredManualRedeemTask(taskId)
|
||||
const detail = await getClaimDetailForAdminTask(task.id)
|
||||
return decorateManualRedeemDetail(task, detail)
|
||||
}
|
||||
|
||||
/** @param {AdminEntityIdInput} taskId */
|
||||
/** @param {{ loginType?: string, forceRecreate?: boolean }} [payload] */
|
||||
export async function createAdminManualRedeemSession(taskId, payload = {}) {
|
||||
const task = await getRequiredManualRedeemTask(taskId)
|
||||
const detail = await createClaimSessionForAdminTask(task.id, payload)
|
||||
return decorateManualRedeemDetail(task, detail)
|
||||
}
|
||||
|
||||
/** @param {AdminEntityIdInput} taskId */
|
||||
export async function getAdminManualRedeemSessionSummary(taskId) {
|
||||
const task = await getRequiredManualRedeemTask(taskId)
|
||||
const detail = await getClaimSessionSummaryForAdminTask(task.id)
|
||||
return decorateManualRedeemDetail(task, detail)
|
||||
}
|
||||
|
||||
/** @param {AdminEntityIdInput} taskId */
|
||||
export async function reloadAdminManualRedeemSession(taskId) {
|
||||
const task = await getRequiredManualRedeemTask(taskId)
|
||||
const detail = await reloadClaimSessionForAdminTask(task.id)
|
||||
return decorateManualRedeemDetail(task, detail)
|
||||
}
|
||||
|
||||
/** @param {AdminEntityIdInput} taskId */
|
||||
export async function closeAdminManualRedeemSession(taskId) {
|
||||
const task = await getRequiredManualRedeemTask(taskId)
|
||||
const detail = await closeClaimSessionForAdminTask(task.id)
|
||||
return decorateManualRedeemDetail(task, detail)
|
||||
}
|
||||
|
||||
/** @param {AdminEntityIdInput} taskId */
|
||||
export async function closeAdminManualRedeemTask(taskId) {
|
||||
const task = await getRequiredManualRedeemTask(taskId)
|
||||
await closeAdminTask(task.id)
|
||||
return getAdminManualRedeemDetail(task.id)
|
||||
}
|
||||
|
||||
/** @param {AdminEntityIdInput} taskId */
|
||||
/** @param {AdminViewerSessionInput | null} [session] */
|
||||
export async function confirmAdminManualRedeemRole(taskId, session = null) {
|
||||
const task = await getRequiredManualRedeemTask(taskId)
|
||||
await confirmAdminTaskAssistedRole(task.id, session)
|
||||
return getAdminManualRedeemDetail(task.id)
|
||||
}
|
||||
|
||||
/** @param {AdminEntityIdInput} taskId */
|
||||
/** @param {AdminViewerSessionInput | null} [session] */
|
||||
export async function redeemAdminManualRedeemTask(taskId, session = null) {
|
||||
const task = await getRequiredManualRedeemTask(taskId)
|
||||
await redeemAdminTaskAssisted(task.id, session)
|
||||
return getAdminManualRedeemDetail(task.id)
|
||||
}
|
||||
|
||||
async function getRequiredManualRedeemTask(taskId) {
|
||||
const task = await getTaskById(Number(taskId))
|
||||
|
||||
if (!task) {
|
||||
throw createHttpError('人工兑换任务不存在', {
|
||||
statusCode: 404,
|
||||
errorCode: 'admin_manual_redeem_task_not_found',
|
||||
})
|
||||
}
|
||||
|
||||
const taskContext = parseTaskContext(task)
|
||||
const sourceType = String(taskContext?.manualRedeem?.sourceType || '').trim()
|
||||
|
||||
if (!isAssistedClaimTask(task) || sourceType !== MANUAL_SOURCE_TYPE) {
|
||||
throw createHttpError('当前任务不是人工兑换任务', {
|
||||
statusCode: 409,
|
||||
errorCode: 'admin_manual_redeem_task_invalid',
|
||||
})
|
||||
}
|
||||
|
||||
return task
|
||||
}
|
||||
|
||||
function decorateManualRedeemDetail(task, detail) {
|
||||
const taskContext = parseTaskContext(task)
|
||||
const manualRedeem = taskContext.manualRedeem && typeof taskContext.manualRedeem === 'object'
|
||||
? taskContext.manualRedeem
|
||||
: {}
|
||||
|
||||
return {
|
||||
...detail,
|
||||
claimUrl: '',
|
||||
manualRequest: {
|
||||
sourceType: String(manualRedeem.sourceType || MANUAL_SOURCE_TYPE).trim() || MANUAL_SOURCE_TYPE,
|
||||
proofValue: String(manualRedeem.proofValue || detail?.order?.platformOrderId || '').trim(),
|
||||
remark: String(manualRedeem.remark || '').trim(),
|
||||
},
|
||||
result: detail.result
|
||||
? {
|
||||
...detail.result,
|
||||
screenshotUrl: detail.result.screenshotReady ? `/api/v1/admin/tasks/${detail.task.taskId}/screenshot` : '',
|
||||
}
|
||||
: null,
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeManualProofValue(value) {
|
||||
return String(value || '').replace(/\s+/g, ' ').trim()
|
||||
}
|
||||
@@ -44,6 +44,20 @@ export async function getClaimDetail(token, { includeQrImage = true } = {}) {
|
||||
})
|
||||
}
|
||||
|
||||
export async function getClaimDetailForAdminTask(taskId, { includeQrImage = true } = {}) {
|
||||
const context = await getClaimContextByTaskId(taskId)
|
||||
const { task, session } = await loadTaskSession(context.task, { includeQrImage })
|
||||
const syncedTask = session ? await syncTaskWithSession(task, session) : 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)
|
||||
@@ -106,6 +120,68 @@ export async function createClaimSession(token, payload = {}) {
|
||||
})
|
||||
}
|
||||
|
||||
export async function createClaimSessionForAdminTask(taskId, payload = {}) {
|
||||
const context = await getClaimContextByTaskId(taskId)
|
||||
assertTaskCanProceed(context.task)
|
||||
const requestedLoginType = normalizeClaimLoginType(payload.loginType)
|
||||
const forceRecreate = Boolean(payload.forceRecreate)
|
||||
let task = context.task
|
||||
|
||||
if (task.browser_session_id) {
|
||||
const existing = await loadTaskSession(task, { includeQrImage: true })
|
||||
task = existing.task
|
||||
|
||||
if (existing.session) {
|
||||
const existingLoginType = normalizeClaimLoginType(existing.session.loginType || task.login_type)
|
||||
|
||||
if (!forceRecreate && existingLoginType === requestedLoginType) {
|
||||
const syncedTask = await syncTaskWithSession(task, existing.session)
|
||||
|
||||
return buildClaimDetailPayload({
|
||||
claimToken: context.claimToken,
|
||||
task: syncedTask,
|
||||
order: context.order,
|
||||
orderItem: context.orderItem,
|
||||
session: existing.session,
|
||||
})
|
||||
}
|
||||
|
||||
try {
|
||||
await closeTencentBrowserSession(existing.session.sessionId)
|
||||
} catch (error) {
|
||||
if (!isRecoverableSessionError(error)) {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
task = await clearTaskSession(task, {
|
||||
lastError: '',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const session = await createTencentBrowserSession({
|
||||
loginType: requestedLoginType,
|
||||
})
|
||||
const updatedTask = await updateTask(task.id, {
|
||||
task_status: 'claimed',
|
||||
browser_session_id: session.sessionId,
|
||||
login_type: session.loginType,
|
||||
user_action_status: 'claimed',
|
||||
claimed_at: task.claimed_at || nowIso(),
|
||||
updated_at: nowIso(),
|
||||
last_error: '',
|
||||
})
|
||||
|
||||
return buildClaimDetailPayload({
|
||||
claimToken: context.claimToken,
|
||||
task: updatedTask,
|
||||
order: context.order,
|
||||
orderItem: context.orderItem,
|
||||
session,
|
||||
})
|
||||
}
|
||||
|
||||
export async function getClaimSessionSummary(token) {
|
||||
const context = await getClaimContext(token)
|
||||
const { task, session } = await loadTaskSession(context.task, {
|
||||
@@ -133,6 +209,33 @@ export async function getClaimSessionSummary(token) {
|
||||
})
|
||||
}
|
||||
|
||||
export async function getClaimSessionSummaryForAdminTask(taskId) {
|
||||
const context = await getClaimContextByTaskId(taskId)
|
||||
const { task, session } = await loadTaskSession(context.task, {
|
||||
includeQrImage: false,
|
||||
})
|
||||
|
||||
if (!session) {
|
||||
return buildClaimDetailPayload({
|
||||
claimToken: context.claimToken,
|
||||
task,
|
||||
order: context.order,
|
||||
orderItem: context.orderItem,
|
||||
session: null,
|
||||
})
|
||||
}
|
||||
|
||||
const syncedTask = await syncTaskWithSession(task, session)
|
||||
|
||||
return buildClaimDetailPayload({
|
||||
claimToken: context.claimToken,
|
||||
task: syncedTask,
|
||||
order: context.order,
|
||||
orderItem: context.orderItem,
|
||||
session,
|
||||
})
|
||||
}
|
||||
|
||||
export async function reloadClaimSession(token) {
|
||||
const context = await getClaimContext(token)
|
||||
assertTaskCanProceed(context.task)
|
||||
@@ -163,6 +266,36 @@ export async function reloadClaimSession(token) {
|
||||
})
|
||||
}
|
||||
|
||||
export async function reloadClaimSessionForAdminTask(taskId) {
|
||||
const context = await getClaimContextByTaskId(taskId)
|
||||
assertTaskCanProceed(context.task)
|
||||
|
||||
const active = await loadTaskSession(context.task, {
|
||||
includeQrImage: true,
|
||||
})
|
||||
|
||||
if (!active.session) {
|
||||
return buildClaimDetailPayload({
|
||||
claimToken: context.claimToken,
|
||||
task: active.task,
|
||||
order: context.order,
|
||||
orderItem: context.orderItem,
|
||||
session: null,
|
||||
})
|
||||
}
|
||||
|
||||
const session = await reloadTencentBrowserSession(active.session.sessionId)
|
||||
const syncedTask = await syncTaskWithSession(active.task, session)
|
||||
|
||||
return buildClaimDetailPayload({
|
||||
claimToken: context.claimToken,
|
||||
task: syncedTask,
|
||||
order: context.order,
|
||||
orderItem: context.orderItem,
|
||||
session,
|
||||
})
|
||||
}
|
||||
|
||||
export async function closeClaimSession(token) {
|
||||
const context = await getClaimContext(token)
|
||||
assertTaskCanProceed(context.task)
|
||||
@@ -199,6 +332,42 @@ export async function closeClaimSession(token) {
|
||||
})
|
||||
}
|
||||
|
||||
export async function closeClaimSessionForAdminTask(taskId) {
|
||||
const context = await getClaimContextByTaskId(taskId)
|
||||
assertTaskCanProceed(context.task)
|
||||
let task = context.task
|
||||
|
||||
if (!task.browser_session_id) {
|
||||
return buildClaimDetailPayload({
|
||||
claimToken: context.claimToken,
|
||||
task,
|
||||
order: context.order,
|
||||
orderItem: context.orderItem,
|
||||
session: null,
|
||||
})
|
||||
}
|
||||
|
||||
try {
|
||||
await closeTencentBrowserSession(task.browser_session_id)
|
||||
} catch (error) {
|
||||
if (!isRecoverableSessionError(error)) {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
task = await clearTaskSession(task, {
|
||||
lastError: '',
|
||||
})
|
||||
|
||||
return buildClaimDetailPayload({
|
||||
claimToken: context.claimToken,
|
||||
task,
|
||||
order: context.order,
|
||||
orderItem: context.orderItem,
|
||||
session: null,
|
||||
})
|
||||
}
|
||||
|
||||
export async function confirmClaimRole(token) {
|
||||
const context = await getClaimContext(token)
|
||||
assertPublicClaimActionAllowed(context.task, 'confirm')
|
||||
|
||||
@@ -58,6 +58,10 @@ export {}
|
||||
* @typedef {import('./admin-write-inputs.js').AdminTaskManualDispatchInput} AdminTaskManualDispatchRouteBody
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {import('./admin-write-inputs.js').AdminManualRedeemCreateInput} AdminManualRedeemCreateRouteBody
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {{ inventoryItemId?: string }} AdminInventoryRouteParams
|
||||
*/
|
||||
@@ -70,6 +74,10 @@ export {}
|
||||
* @typedef {{ taskId?: string, bindingId?: string }} AdminTaskRouteParams
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {{ taskId?: string }} AdminManualRedeemRouteParams
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {{ eventId?: string }} AdminWebhookEventRouteParams
|
||||
*/
|
||||
|
||||
@@ -107,3 +107,12 @@ export {}
|
||||
* deliveredCredential?: string
|
||||
* }} AdminTaskManualDispatchInput
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {{
|
||||
* proofValue?: string
|
||||
* skuCode?: string
|
||||
* skuName?: string
|
||||
* remark?: string
|
||||
* }} AdminManualRedeemCreateInput
|
||||
*/
|
||||
|
||||
@@ -63,8 +63,10 @@ export {}
|
||||
* role_name: string
|
||||
* role_id: string
|
||||
* claim_token: string
|
||||
* claim_expires_at?: string | null
|
||||
* primary_claim_token: string
|
||||
* primary_claim_token_id: number | null
|
||||
* primary_claim_expires_at?: string | null
|
||||
* primary_inventory_item_id: number | null
|
||||
* inventory_display_value: string
|
||||
* primary_inventory_display_value: string
|
||||
|
||||
Reference in New Issue
Block a user