449 lines
16 KiB
JavaScript
449 lines
16 KiB
JavaScript
// @ts-check
|
|
|
|
import { getTaskById } from '../../repositories/task-repo.js'
|
|
import { listTaskInventoryBindingSummariesByTaskIds } from '../../repositories/task-inventory-binding-repo.js'
|
|
import { createHttpError } from '../../utils/http.js'
|
|
import { safeParseJson } from './admin-query-utils.js'
|
|
import {
|
|
createAdminViewerContext,
|
|
getTaskPrimaryClaimTokenId,
|
|
getTaskPrimaryInventoryItemId,
|
|
parseTaskContext,
|
|
} from './admin-read-shared-helpers.js'
|
|
|
|
/** @typedef {import('../../types/admin-read-models.js').AdminTaskBindingSummary} AdminTaskBindingSummary */
|
|
/** @typedef {import('../../types/admin-read-models.js').AdminTaskListItem} AdminTaskListItem */
|
|
/** @typedef {import('../../types/repository-rows.js').TaskEventRow} TaskEventRow */
|
|
/** @typedef {import('../../types/repository-rows.js').TaskInventoryBindingRow} TaskInventoryBindingRow */
|
|
/** @typedef {import('../../types/repository-rows.js').TaskRow} TaskRow */
|
|
|
|
export function mapAdminTaskSummary(task, bindingSummary = createEmptyTaskBindingSummary()) {
|
|
const binding = buildTaskBindingState(task)
|
|
const taskContext = parseTaskContext(task)
|
|
|
|
return {
|
|
taskId: task.id,
|
|
taskNo: task.task_no,
|
|
deliveryStatus: task.delivery_status || '',
|
|
status: task.task_status,
|
|
systemBindingStatus: binding.systemBindingStatus,
|
|
userBindingStatus: binding.userBindingStatus,
|
|
loginType: task.login_type,
|
|
browserSessionId: task.browser_session_id,
|
|
claimedAt: task.claimed_at,
|
|
roleConfirmedAt: task.role_confirmed_at,
|
|
redeemedAt: task.redeemed_at,
|
|
lastError: task.last_error,
|
|
retryCount: getTaskRetryCount(task),
|
|
bindingSummary,
|
|
agisoAutoDelivery: mapAgisoAutoDeliveryContext(taskContext.agisoAutoDelivery),
|
|
createdAt: task.created_at,
|
|
updatedAt: task.updated_at,
|
|
}
|
|
}
|
|
|
|
export function mapTaskActionPayload(task) {
|
|
const inventoryItemId = getTaskPrimaryInventoryItemId(task)
|
|
const primaryClaimTokenId = getTaskPrimaryClaimTokenId(task)
|
|
|
|
return {
|
|
taskId: task.id,
|
|
taskNo: task.task_no,
|
|
status: task.task_status,
|
|
deliveryStatus: task.delivery_status,
|
|
resultCode: task.result_code,
|
|
resultMessage: task.result_message,
|
|
inventoryItemId,
|
|
primaryClaimTokenId,
|
|
lastError: task.last_error,
|
|
updatedAt: task.updated_at,
|
|
}
|
|
}
|
|
|
|
export async function getRequiredTask(taskId) {
|
|
const task = await getTaskById(Number(taskId))
|
|
|
|
if (!task) {
|
|
throw createHttpError('任务不存在', {
|
|
statusCode: 404,
|
|
errorCode: 'admin_task_not_found',
|
|
})
|
|
}
|
|
|
|
return task
|
|
}
|
|
|
|
/**
|
|
* @param {TaskRow} task
|
|
* @returns {AdminTaskListItem}
|
|
*/
|
|
export function mapAdminTaskListItem(task, bindingSummary = createEmptyTaskBindingSummary(), viewerContext = createAdminViewerContext()) {
|
|
const binding = buildTaskBindingState(task)
|
|
const taskContext = parseTaskContext(task)
|
|
|
|
return {
|
|
taskId: task.id,
|
|
taskNo: task.task_no,
|
|
platformOrderId: task.platform_order_id,
|
|
skuCode: task.sku_code || '',
|
|
skuName: task.sku_name || '',
|
|
status: task.task_status,
|
|
executorKey: task.executor_key || '',
|
|
deliveryStatus: task.delivery_status || '',
|
|
resultCode: task.result_code || '',
|
|
resultMessage: task.result_message || '',
|
|
systemBindingStatus: binding.systemBindingStatus,
|
|
userBindingStatus: binding.userBindingStatus,
|
|
loginType: task.login_type,
|
|
roleName: task.role_name,
|
|
roleId: task.role_id,
|
|
browserSessionId: task.browser_session_id,
|
|
claimedAt: task.claimed_at,
|
|
roleConfirmedAt: task.role_confirmed_at,
|
|
redeemedAt: task.redeemed_at,
|
|
retryCount: getTaskRetryCount(task),
|
|
bindingSummary,
|
|
agisoAutoDelivery: mapAgisoAutoDeliveryContext(taskContext.agisoAutoDelivery),
|
|
lastError: task.last_error,
|
|
createdAt: task.created_at,
|
|
updatedAt: task.updated_at,
|
|
inventoryDisplayMasked: viewerContext.canViewSensitiveTaskData ? maskCode(task.primary_inventory_display_value) : '',
|
|
inventoryCredentialType: String(task.primary_inventory_credential_type || '').trim(),
|
|
claimToken: viewerContext.canViewSensitiveTaskData ? (task.primary_claim_token || task.claim_token || '') : '',
|
|
screenshotPath: viewerContext.role === 'support' ? '' : (task.screenshot_path || ''),
|
|
}
|
|
}
|
|
|
|
/** @param {TaskEventRow} event */
|
|
export function mapAdminTaskEvent(event) {
|
|
const payload = safeParseJson(event.payload_json)
|
|
|
|
return {
|
|
eventId: event.id,
|
|
eventType: String(event.event_type || '').trim(),
|
|
payload: normalizeRecord(payload),
|
|
createdAt: event.created_at,
|
|
}
|
|
}
|
|
|
|
/** @returns {AdminTaskBindingSummary} */
|
|
export function createEmptyTaskBindingSummary() {
|
|
return {
|
|
totalBindingCount: 0,
|
|
reservedBindingCount: 0,
|
|
consumedBindingCount: 0,
|
|
releasedBindingCount: 0,
|
|
roleKeys: [],
|
|
}
|
|
}
|
|
|
|
export async function getTaskBindingSummaryMap(taskIds = []) {
|
|
const rows = await listTaskInventoryBindingSummariesByTaskIds(taskIds)
|
|
const output = new Map()
|
|
|
|
for (const row of rows) {
|
|
output.set(Number(row.task_id), {
|
|
totalBindingCount: Number(row.total_binding_count || 0),
|
|
reservedBindingCount: Number(row.reserved_binding_count || 0),
|
|
consumedBindingCount: Number(row.consumed_binding_count || 0),
|
|
releasedBindingCount: Number(row.released_binding_count || 0),
|
|
roleKeys: Array.isArray(row.role_keys)
|
|
? row.role_keys.map((value) => String(value || '').trim()).filter(Boolean)
|
|
: [],
|
|
})
|
|
}
|
|
|
|
return output
|
|
}
|
|
|
|
export function getTaskBindingSummary(summaryMap, taskId) {
|
|
return summaryMap.get(Number(taskId)) || createEmptyTaskBindingSummary()
|
|
}
|
|
|
|
export function createTaskBindingSummaryFromBindings(bindings = []) {
|
|
const normalizedBindings = Array.isArray(bindings) ? bindings : []
|
|
const roleKeys = Array.from(new Set(normalizedBindings
|
|
.map((binding) => String(binding?.role_key || '').trim())
|
|
.filter(Boolean)))
|
|
|
|
return {
|
|
totalBindingCount: normalizedBindings.length,
|
|
reservedBindingCount: normalizedBindings.filter((binding) => String(binding?.binding_status || '') === 'reserved').length,
|
|
consumedBindingCount: normalizedBindings.filter((binding) => String(binding?.binding_status || '') === 'consumed').length,
|
|
releasedBindingCount: normalizedBindings.filter((binding) => String(binding?.binding_status || '') === 'released').length,
|
|
roleKeys,
|
|
}
|
|
}
|
|
|
|
export function mapAdminTaskInventoryBinding(binding, task, viewerContext = createAdminViewerContext()) {
|
|
const metadata = safeParseJson(binding.metadata_json)
|
|
|
|
return {
|
|
bindingId: binding.id,
|
|
inventoryItemId: binding.inventory_item_id,
|
|
roleKey: String(binding.role_key || '').trim(),
|
|
quantity: Math.max(1, Number(binding.quantity || 1)),
|
|
bindingStatus: String(binding.binding_status || '').trim(),
|
|
inventoryStatus: String(binding.inventory_item_status || '').trim(),
|
|
skuCode: String(binding.sku_code || '').trim(),
|
|
batchNo: String(binding.batch_no || '').trim(),
|
|
credentialType: String(binding.credential_type || 'tencent_code').trim() || 'tencent_code',
|
|
displayValue: viewerContext.canViewSensitiveTaskData ? String(binding.display_value || '').trim() : '',
|
|
invalidReason: String(binding.invalid_reason || '').trim(),
|
|
consumedAt: binding.consumed_at || null,
|
|
releasedAt: binding.released_at || null,
|
|
createdAt: binding.created_at,
|
|
updatedAt: binding.updated_at,
|
|
metadata: normalizeRecord(metadata),
|
|
isPrimary: Number(binding.inventory_item_id || 0) === getTaskPrimaryInventoryItemId(task),
|
|
canRelease: viewerContext.canManageSensitiveInventory && canReleaseTaskInventoryBinding(task, binding),
|
|
}
|
|
}
|
|
|
|
export function buildOrderBindingSummary(tasks, taskBindingSummaryMap = new Map()) {
|
|
const normalizedTasks = Array.isArray(tasks) ? tasks : []
|
|
const totalTaskCount = normalizedTasks.length
|
|
const taskBindings = normalizedTasks.map((task) => buildTaskBindingState(task))
|
|
const systemBoundTaskCount = normalizedTasks.filter((task) => isTaskSystemBound(task)).length
|
|
const completedBindingTaskCount = normalizedTasks.filter((task) => String(task?.task_status || '') === 'redeemed').length
|
|
const bindingSummaries = normalizedTasks.map((task) => getTaskBindingSummary(taskBindingSummaryMap, task.id))
|
|
const totalBindingCount = bindingSummaries.reduce((sum, item) => sum + item.totalBindingCount, 0)
|
|
const reservedBindingCount = bindingSummaries.reduce((sum, item) => sum + item.reservedBindingCount, 0)
|
|
const consumedBindingCount = bindingSummaries.reduce((sum, item) => sum + item.consumedBindingCount, 0)
|
|
const releasedBindingCount = bindingSummaries.reduce((sum, item) => sum + item.releasedBindingCount, 0)
|
|
|
|
let systemBindingStatus = 'pending_binding'
|
|
let userBindingStatus = 'not_started'
|
|
|
|
if (totalTaskCount === 0) {
|
|
return {
|
|
totalTaskCount,
|
|
systemBoundTaskCount,
|
|
completedBindingTaskCount,
|
|
totalBindingCount,
|
|
reservedBindingCount,
|
|
consumedBindingCount,
|
|
releasedBindingCount,
|
|
systemBindingStatus,
|
|
userBindingStatus,
|
|
}
|
|
}
|
|
|
|
if (normalizedTasks.every((task) => String(task.task_status || '') === 'redeemed')) {
|
|
systemBindingStatus = 'system_bound'
|
|
userBindingStatus = 'binding_completed'
|
|
} else if (taskBindings.some((item) => ['binding_in_progress', 'binding_confirmed', 'link_opened'].includes(item.userBindingStatus))) {
|
|
systemBindingStatus = systemBoundTaskCount > 0 ? 'system_bound' : 'pending_binding'
|
|
userBindingStatus = 'user_binding'
|
|
} else if (taskBindings.some((item) => item.userBindingStatus === 'waiting_user_claim')) {
|
|
systemBindingStatus = systemBoundTaskCount > 0 ? 'system_bound' : 'pending_binding'
|
|
userBindingStatus = 'waiting_user_claim'
|
|
} else if (taskBindings.some((item) => item.userBindingStatus === 'binding_exception')) {
|
|
systemBindingStatus = systemBoundTaskCount > 0 ? 'system_bound' : 'pending_binding'
|
|
userBindingStatus = 'binding_exception'
|
|
} else if (systemBoundTaskCount > 0) {
|
|
systemBindingStatus = 'system_bound'
|
|
} else if (taskBindings.some((item) => ['manual_review', 'retry_pending', 'waiting_inventory'].includes(item.systemBindingStatus))) {
|
|
systemBindingStatus = 'binding_exception'
|
|
}
|
|
|
|
return {
|
|
totalTaskCount,
|
|
systemBoundTaskCount,
|
|
completedBindingTaskCount,
|
|
totalBindingCount,
|
|
reservedBindingCount,
|
|
consumedBindingCount,
|
|
releasedBindingCount,
|
|
systemBindingStatus,
|
|
userBindingStatus,
|
|
}
|
|
}
|
|
|
|
export function buildOrderAgisoAutoDeliverySummary(order, tasks = []) {
|
|
if (String(order?.provider || '').trim() !== 'agiso' || String(order?.platform || '').trim() !== 'xianyu') {
|
|
return null
|
|
}
|
|
|
|
const normalizedTasks = Array.isArray(tasks) ? tasks.filter(Boolean) : []
|
|
const totalTaskCount = normalizedTasks.length
|
|
const deliveredTaskCount = normalizedTasks.filter((task) => String(task?.delivery_status || '').trim() === 'delivered').length
|
|
const latest = normalizedTasks.reduce((best, task) => {
|
|
const autoDelivery = mapAgisoAutoDeliveryContext(parseTaskContext(task).agisoAutoDelivery)
|
|
|
|
if (!autoDelivery) {
|
|
return best
|
|
}
|
|
|
|
const candidate = {
|
|
...autoDelivery,
|
|
sourceTaskId: Number(task.id || 0) || null,
|
|
sourceTaskNo: String(task.task_no || '').trim(),
|
|
}
|
|
const candidateTime = Date.parse(String(candidate.updatedAt || task.updated_at || ''))
|
|
const bestTime = Date.parse(String(best?.updatedAt || ''))
|
|
|
|
if (!best || (Number.isFinite(candidateTime) && (!Number.isFinite(bestTime) || candidateTime >= bestTime))) {
|
|
return candidate
|
|
}
|
|
|
|
return best
|
|
}, null)
|
|
|
|
if (latest) {
|
|
return {
|
|
...latest,
|
|
totalTaskCount,
|
|
deliveredTaskCount,
|
|
}
|
|
}
|
|
|
|
return {
|
|
status: totalTaskCount === 0 ? 'not_started' : deliveredTaskCount >= totalTaskCount ? 'pending' : 'waiting',
|
|
trigger: '',
|
|
reason: deliveredTaskCount >= totalTaskCount ? '' : 'waiting_other_tasks',
|
|
platformOrderId: String(order?.platform_order_id || '').trim(),
|
|
responseStatus: 0,
|
|
errorMessage: '',
|
|
requestId: '',
|
|
updatedAt: null,
|
|
sourceTaskId: null,
|
|
sourceTaskNo: '',
|
|
totalTaskCount,
|
|
deliveredTaskCount,
|
|
}
|
|
}
|
|
|
|
function canReleaseTaskInventoryBinding(task, binding) {
|
|
if (!task || !binding) {
|
|
return false
|
|
}
|
|
|
|
if (String(binding.binding_status || '').trim() !== 'reserved') {
|
|
return false
|
|
}
|
|
|
|
return !['redeemed', 'expired'].includes(String(task.task_status || '').trim())
|
|
}
|
|
|
|
function buildTaskBindingState(task) {
|
|
const normalizedStatus = String(task?.task_status || '').trim()
|
|
|
|
if (!normalizedStatus) {
|
|
return {
|
|
systemBindingStatus: 'pending_binding',
|
|
userBindingStatus: 'not_started',
|
|
}
|
|
}
|
|
|
|
if (normalizedStatus === 'pending_payment') {
|
|
return { systemBindingStatus: 'pending_payment', userBindingStatus: 'not_started' }
|
|
}
|
|
|
|
if (normalizedStatus === 'paid') {
|
|
return { systemBindingStatus: 'pending_binding', userBindingStatus: 'not_started' }
|
|
}
|
|
|
|
if (normalizedStatus === 'waiting_inventory') {
|
|
return { systemBindingStatus: 'waiting_inventory', userBindingStatus: 'not_started' }
|
|
}
|
|
|
|
if (normalizedStatus === 'pending_binding_prepare') {
|
|
return { systemBindingStatus: 'pending_binding', userBindingStatus: 'not_started' }
|
|
}
|
|
|
|
if (normalizedStatus === 'waiting_binding') {
|
|
return { systemBindingStatus: 'system_bound', userBindingStatus: 'waiting_user_claim' }
|
|
}
|
|
|
|
if (normalizedStatus === 'dispatched_pending_return') {
|
|
return { systemBindingStatus: 'system_bound', userBindingStatus: 'binding_completed' }
|
|
}
|
|
|
|
if (normalizedStatus === 'completed') {
|
|
return { systemBindingStatus: 'system_bound', userBindingStatus: 'binding_completed' }
|
|
}
|
|
|
|
if (normalizedStatus === 'manual_review') {
|
|
return { systemBindingStatus: 'manual_review', userBindingStatus: 'binding_exception' }
|
|
}
|
|
|
|
if (normalizedStatus === 'retry_pending') {
|
|
return { systemBindingStatus: 'retry_pending', userBindingStatus: 'binding_exception' }
|
|
}
|
|
|
|
if (normalizedStatus === 'closed') {
|
|
return { systemBindingStatus: 'closed', userBindingStatus: 'closed' }
|
|
}
|
|
|
|
if (normalizedStatus === 'expired') {
|
|
return { systemBindingStatus: 'expired', userBindingStatus: 'expired' }
|
|
}
|
|
|
|
if (normalizedStatus === 'link_generated') {
|
|
return { systemBindingStatus: 'system_bound', userBindingStatus: 'waiting_user_claim' }
|
|
}
|
|
|
|
if (normalizedStatus === 'claimed') {
|
|
return { systemBindingStatus: 'system_bound', userBindingStatus: 'link_opened' }
|
|
}
|
|
|
|
if (normalizedStatus === 'role_confirmed') {
|
|
return { systemBindingStatus: 'system_bound', userBindingStatus: 'binding_confirmed' }
|
|
}
|
|
|
|
if (normalizedStatus === 'redeeming') {
|
|
return { systemBindingStatus: 'system_bound', userBindingStatus: 'binding_in_progress' }
|
|
}
|
|
|
|
if (normalizedStatus === 'redeemed') {
|
|
return { systemBindingStatus: 'system_bound', userBindingStatus: 'binding_completed' }
|
|
}
|
|
|
|
if (isTaskSystemBound(task)) {
|
|
return { systemBindingStatus: 'system_bound', userBindingStatus: 'waiting_user_claim' }
|
|
}
|
|
|
|
return { systemBindingStatus: 'pending_binding', userBindingStatus: 'not_started' }
|
|
}
|
|
|
|
function isTaskSystemBound(task) {
|
|
return Boolean(task && (getTaskPrimaryInventoryItemId(task) || getTaskPrimaryClaimTokenId(task)))
|
|
}
|
|
|
|
function mapAgisoAutoDeliveryContext(value) {
|
|
if (!value || typeof value !== 'object') {
|
|
return null
|
|
}
|
|
|
|
return {
|
|
status: String(value.status || '').trim(),
|
|
trigger: String(value.trigger || '').trim(),
|
|
reason: String(value.reason || '').trim(),
|
|
platformOrderId: String(value.platformOrderId || '').trim(),
|
|
responseStatus: Number(value.responseStatus || 0),
|
|
errorMessage: String(value.errorMessage || '').trim(),
|
|
requestId: String(value.requestId || '').trim(),
|
|
updatedAt: value.updatedAt || null,
|
|
}
|
|
}
|
|
|
|
function normalizeRecord(value) {
|
|
return value && typeof value === 'object' && !Array.isArray(value) ? value : {}
|
|
}
|
|
|
|
function maskCode(value) {
|
|
const text = String(value || '').trim()
|
|
if (!text) {
|
|
return ''
|
|
}
|
|
if (text.length <= 8) {
|
|
return `${text.slice(0, 2)}****${text.slice(-2)}`
|
|
}
|
|
return `${text.slice(0, 4)}****${text.slice(-4)}`
|
|
}
|
|
|
|
function getTaskRetryCount(task) {
|
|
return Number(task?.attempt_count || 0)
|
|
}
|