init
This commit is contained in:
@@ -0,0 +1,924 @@
|
||||
import { getDb } from '../db/client.js'
|
||||
import { createTaskClaimToken } from './claim-service.js'
|
||||
import { getClaimTokenById, updateClaimToken } from '../repositories/claim-token-repo.js'
|
||||
import { createCdks, getCdkById, invalidateCdk, listCdks, releaseReservedCdk } from '../repositories/cdk-repo.js'
|
||||
import { listOrderItemsByOrderId } from '../repositories/order-item-repo.js'
|
||||
import { getOrderById, listOrders } from '../repositories/order-repo.js'
|
||||
import { getTaskById, listTasks, listTasksByOrderId, updateTask } from '../repositories/task-repo.js'
|
||||
import { getWebhookEventById, listWebhookEvents, listWebhookEventsByOrderId } from '../repositories/webhook-event-repo.js'
|
||||
import { createHttpError } from '../utils/http.js'
|
||||
import { nowIso } from '../utils/time.js'
|
||||
import { reserveCdkForTask } from './cdk-service.js'
|
||||
import { replayAgisoTradeWebhookEvent } from './webhook-service.js'
|
||||
|
||||
export function getAdminDashboardSummary() {
|
||||
const db = getDb()
|
||||
const todayPrefix = nowIso().slice(0, 10)
|
||||
const summary = db.prepare(`
|
||||
SELECT
|
||||
(SELECT COUNT(*) FROM orders WHERE substr(created_at, 1, 10) = ?) AS today_orders,
|
||||
(SELECT COUNT(*) FROM delivery_tasks WHERE task_status IN ('paid', 'cdk_reserved', 'link_generated')) AS paid_pending_claim,
|
||||
(SELECT COUNT(*) FROM delivery_tasks WHERE task_status IN ('claimed', 'role_confirmed', 'redeeming')) AS claiming_tasks,
|
||||
(SELECT COUNT(*) FROM delivery_tasks WHERE task_status = 'redeemed' AND substr(updated_at, 1, 10) = ?) AS redeemed_today,
|
||||
(SELECT COUNT(*) FROM delivery_tasks WHERE task_status IN ('retry_pending', 'manual_review', 'waiting_inventory')) AS abnormal_tasks,
|
||||
(SELECT COUNT(DISTINCT sku_code) FROM cdk_inventory WHERE status = 'available') AS sku_with_inventory
|
||||
`).get(todayPrefix, todayPrefix)
|
||||
|
||||
return {
|
||||
todayOrders: Number(summary?.today_orders || 0),
|
||||
paidPendingClaim: Number(summary?.paid_pending_claim || 0),
|
||||
claimingTasks: Number(summary?.claiming_tasks || 0),
|
||||
redeemedToday: Number(summary?.redeemed_today || 0),
|
||||
abnormalTasks: Number(summary?.abnormal_tasks || 0),
|
||||
skuWithInventory: Number(summary?.sku_with_inventory || 0),
|
||||
}
|
||||
}
|
||||
|
||||
export function getAdminOrders(query = {}) {
|
||||
const page = normalizePage(query.page)
|
||||
const pageSize = normalizePageSize(query.pageSize)
|
||||
const { items, total } = listOrders({
|
||||
page,
|
||||
pageSize,
|
||||
platformOrderId: String(query.platformOrderId || '').trim(),
|
||||
payStatus: String(query.payStatus || '').trim(),
|
||||
skuCode: String(query.skuCode || '').trim(),
|
||||
dateFrom: normalizeDateQuery(query.dateFrom),
|
||||
dateTo: normalizeDateQuery(query.dateTo, true),
|
||||
})
|
||||
|
||||
return {
|
||||
items: items.map(mapAdminOrderListItem),
|
||||
pagination: { page, pageSize, total },
|
||||
}
|
||||
}
|
||||
|
||||
export function getAdminOrderDetail(orderId) {
|
||||
const order = getOrderById(Number(orderId))
|
||||
|
||||
if (!order) {
|
||||
throw createHttpError('订单不存在', {
|
||||
statusCode: 404,
|
||||
errorCode: 'admin_order_not_found',
|
||||
})
|
||||
}
|
||||
|
||||
const items = listOrderItemsByOrderId(order.id)
|
||||
const tasks = listTasksByOrderId(order.id)
|
||||
const webhookEvents = listWebhookEventsByOrderId(order.id)
|
||||
|
||||
return {
|
||||
order: {
|
||||
orderId: order.id,
|
||||
platform: order.platform,
|
||||
platformOrderId: order.platform_order_id,
|
||||
orderStatus: order.order_status,
|
||||
payStatus: order.pay_status,
|
||||
buyerId: order.buyer_id,
|
||||
buyerName: order.buyer_name,
|
||||
receiverContact: order.receiver_contact,
|
||||
totalAmount: order.total_amount,
|
||||
currency: order.currency,
|
||||
paidAt: order.paid_at,
|
||||
createdAt: order.created_at,
|
||||
updatedAt: order.updated_at,
|
||||
rawPayload: safeParseJson(order.raw_payload_json),
|
||||
bindingSummary: buildOrderBindingSummary(tasks),
|
||||
},
|
||||
items: items.map((item) => ({
|
||||
orderItemId: item.id,
|
||||
skuCode: item.sku_code,
|
||||
skuName: item.sku_name,
|
||||
quantity: item.quantity,
|
||||
deliveryMode: item.delivery_mode,
|
||||
spec: safeParseJson(item.spec_json),
|
||||
})),
|
||||
tasks: tasks.map(mapAdminTaskSummary),
|
||||
webhookEvents: webhookEvents.map((event) => ({
|
||||
eventId: event.id,
|
||||
platform: event.platform,
|
||||
eventType: event.event_type,
|
||||
eventKey: event.event_key,
|
||||
signatureValid: Boolean(event.signature_valid),
|
||||
processed: Boolean(event.processed),
|
||||
processError: event.process_error,
|
||||
createdAt: event.created_at,
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
export function getAdminTasks(query = {}) {
|
||||
const page = normalizePage(query.page)
|
||||
const pageSize = normalizePageSize(query.pageSize)
|
||||
const { items, total } = listTasks({
|
||||
page,
|
||||
pageSize,
|
||||
status: String(query.status || '').trim(),
|
||||
platformOrderId: String(query.platformOrderId || '').trim(),
|
||||
taskNo: String(query.taskNo || '').trim(),
|
||||
skuCode: String(query.skuCode || '').trim(),
|
||||
roleId: String(query.roleId || '').trim(),
|
||||
dateFrom: normalizeDateQuery(query.dateFrom),
|
||||
dateTo: normalizeDateQuery(query.dateTo, true),
|
||||
})
|
||||
|
||||
return {
|
||||
items: items.map(mapAdminTaskListItem),
|
||||
pagination: { page, pageSize, total },
|
||||
}
|
||||
}
|
||||
|
||||
export function getAdminTaskDetail(taskId) {
|
||||
const task = getTaskById(Number(taskId))
|
||||
|
||||
if (!task) {
|
||||
throw createHttpError('任务不存在', {
|
||||
statusCode: 404,
|
||||
errorCode: 'admin_task_not_found',
|
||||
})
|
||||
}
|
||||
|
||||
const order = getOrderById(task.order_id)
|
||||
const orderItem = order ? listOrderItemsByOrderId(order.id).find((item) => item.id === task.order_item_id) || null : null
|
||||
const claimToken = task.claim_token_id ? getClaimTokenById(task.claim_token_id) : null
|
||||
const cdk = task.reserved_cdk_id ? getCdkById(task.reserved_cdk_id) : null
|
||||
|
||||
return {
|
||||
task: mapAdminTaskListItem({
|
||||
...task,
|
||||
sku_code: orderItem?.sku_code || '',
|
||||
sku_name: orderItem?.sku_name || '',
|
||||
cdk_code: cdk?.cdk_code || '',
|
||||
claim_token: claimToken?.token || '',
|
||||
}),
|
||||
order: order
|
||||
? {
|
||||
orderId: order.id,
|
||||
platformOrderId: order.platform_order_id,
|
||||
payStatus: order.pay_status,
|
||||
orderStatus: order.order_status,
|
||||
}
|
||||
: null,
|
||||
orderItem: orderItem
|
||||
? {
|
||||
orderItemId: orderItem.id,
|
||||
skuCode: orderItem.sku_code,
|
||||
skuName: orderItem.sku_name,
|
||||
quantity: orderItem.quantity,
|
||||
}
|
||||
: null,
|
||||
claimToken: claimToken
|
||||
? {
|
||||
claimTokenId: claimToken.id,
|
||||
token: claimToken.token,
|
||||
status: claimToken.status,
|
||||
expiredAt: claimToken.expired_at,
|
||||
}
|
||||
: null,
|
||||
cdk: cdk
|
||||
? {
|
||||
cdkId: cdk.id,
|
||||
skuCode: cdk.sku_code,
|
||||
batchNo: cdk.batch_no,
|
||||
cdkCode: cdk.cdk_code,
|
||||
status: cdk.status,
|
||||
}
|
||||
: null,
|
||||
artifacts: safeParseJson(task.artifacts_json),
|
||||
screenshotUrl: task.screenshot_path ? `/api/v1/admin/tasks/${task.id}/screenshot` : '',
|
||||
operations: {
|
||||
canRetry: ['retry_pending', 'manual_review', 'waiting_inventory'].includes(task.task_status),
|
||||
canReleaseCdk: Boolean(task.reserved_cdk_id && ['link_generated', 'retry_pending', 'manual_review', 'waiting_inventory', 'closed'].includes(task.task_status)),
|
||||
canRegenerateClaimLink: ['link_generated', 'claimed', 'role_confirmed', 'retry_pending', 'manual_review'].includes(task.task_status),
|
||||
canClose: !['redeemed', 'closed'].includes(task.task_status),
|
||||
canMarkManualReview: !['redeemed', 'closed', 'manual_review'].includes(task.task_status),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export function getAdminTaskScreenshotPath(taskId) {
|
||||
const task = getRequiredTask(taskId)
|
||||
|
||||
if (!task.screenshot_path) {
|
||||
throw createHttpError('当前任务还没有截图', {
|
||||
statusCode: 404,
|
||||
errorCode: 'admin_task_screenshot_not_found',
|
||||
})
|
||||
}
|
||||
|
||||
return task.screenshot_path
|
||||
}
|
||||
|
||||
export function getAdminCdks(query = {}) {
|
||||
const page = normalizePage(query.page)
|
||||
const pageSize = normalizePageSize(query.pageSize)
|
||||
const { items, total } = listCdks({
|
||||
page,
|
||||
pageSize,
|
||||
skuCode: String(query.skuCode || '').trim(),
|
||||
status: String(query.status || '').trim(),
|
||||
batchNo: String(query.batchNo || '').trim(),
|
||||
})
|
||||
|
||||
return {
|
||||
items: items.map(mapAdminCdkListItem),
|
||||
pagination: { page, pageSize, total },
|
||||
}
|
||||
}
|
||||
|
||||
export function createAdminCdk(payload = {}) {
|
||||
const skuCode = String(payload.skuCode || '').trim()
|
||||
const cdkCode = String(payload.cdkCode || '').trim()
|
||||
const batchNo = String(payload.batchNo || '').trim()
|
||||
|
||||
if (!skuCode || !cdkCode) {
|
||||
throw createHttpError('缺少 skuCode 或 cdkCode', {
|
||||
statusCode: 400,
|
||||
errorCode: 'admin_cdk_create_invalid',
|
||||
})
|
||||
}
|
||||
|
||||
const now = nowIso()
|
||||
const created = createCdks([
|
||||
{
|
||||
skuCode,
|
||||
cdkCode,
|
||||
batchNo,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
},
|
||||
])
|
||||
|
||||
if (created === 0) {
|
||||
throw createHttpError('CDK 已存在,不能重复新增', {
|
||||
statusCode: 409,
|
||||
errorCode: 'admin_cdk_duplicate',
|
||||
})
|
||||
}
|
||||
|
||||
const { items } = listCdks({
|
||||
page: 1,
|
||||
pageSize: 1,
|
||||
skuCode,
|
||||
})
|
||||
const createdItem = items.find((item) => item.cdk_code === cdkCode) || null
|
||||
|
||||
return {
|
||||
cdk: createdItem ? mapAdminCdkListItem(createdItem) : null,
|
||||
}
|
||||
}
|
||||
|
||||
export function importAdminCdks(payload = {}) {
|
||||
const rows = normalizeCdkImportRows(payload)
|
||||
|
||||
if (rows.length === 0) {
|
||||
throw createHttpError('没有可导入的 CDK 数据', {
|
||||
statusCode: 400,
|
||||
errorCode: 'admin_cdk_import_empty',
|
||||
})
|
||||
}
|
||||
|
||||
const now = nowIso()
|
||||
const normalizedRows = rows.map((row) => ({
|
||||
skuCode: row.skuCode,
|
||||
batchNo: row.batchNo,
|
||||
cdkCode: row.cdkCode,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
}))
|
||||
|
||||
const created = createCdks(normalizedRows)
|
||||
|
||||
return {
|
||||
total: normalizedRows.length,
|
||||
created,
|
||||
duplicated: normalizedRows.length - created,
|
||||
}
|
||||
}
|
||||
|
||||
export function releaseAdminInventoryCdk(cdkId) {
|
||||
const cdk = getRequiredCdk(cdkId)
|
||||
|
||||
if (cdk.status !== 'reserved') {
|
||||
throw createHttpError('当前 CDK 不是预占状态,不能释放', {
|
||||
statusCode: 409,
|
||||
errorCode: 'admin_cdk_release_not_allowed',
|
||||
})
|
||||
}
|
||||
|
||||
const updated = releaseReservedCdk(cdk.id, nowIso())
|
||||
|
||||
return {
|
||||
cdk: mapAdminCdkListItem(updated),
|
||||
}
|
||||
}
|
||||
|
||||
export function invalidateAdminCdk(cdkId, payload = {}) {
|
||||
const cdk = getRequiredCdk(cdkId)
|
||||
|
||||
if (cdk.status !== 'available') {
|
||||
throw createHttpError('只有可用 CDK 才能作废,请先释放预占', {
|
||||
statusCode: 409,
|
||||
errorCode: 'admin_cdk_invalidate_not_allowed',
|
||||
})
|
||||
}
|
||||
|
||||
const reason = String(payload.reason || '').trim() || '后台手动作废'
|
||||
const updated = invalidateCdk(cdk.id, reason, nowIso())
|
||||
|
||||
return {
|
||||
cdk: mapAdminCdkListItem(updated),
|
||||
}
|
||||
}
|
||||
|
||||
export function getAdminWebhookEvents(query = {}) {
|
||||
const page = normalizePage(query.page)
|
||||
const pageSize = normalizePageSize(query.pageSize)
|
||||
const { items, total } = listWebhookEvents({
|
||||
page,
|
||||
pageSize,
|
||||
platform: String(query.platform || '').trim(),
|
||||
processed: String(query.processed || '').trim(),
|
||||
relatedOrderId: String(query.relatedOrderId || '').trim(),
|
||||
dateFrom: normalizeDateQuery(query.dateFrom),
|
||||
dateTo: normalizeDateQuery(query.dateTo, true),
|
||||
})
|
||||
|
||||
return {
|
||||
items: items.map((item) => ({
|
||||
eventId: item.id,
|
||||
platform: item.platform,
|
||||
eventType: item.event_type,
|
||||
eventKey: item.event_key,
|
||||
signatureValid: Boolean(item.signature_valid),
|
||||
processed: Boolean(item.processed),
|
||||
processError: item.process_error,
|
||||
relatedOrderId: item.related_order_id,
|
||||
createdAt: item.created_at,
|
||||
})),
|
||||
pagination: { page, pageSize, total },
|
||||
}
|
||||
}
|
||||
|
||||
export function getAdminWebhookEventDetail(eventId) {
|
||||
const event = getWebhookEventById(Number(eventId))
|
||||
|
||||
if (!event) {
|
||||
throw createHttpError('Webhook 事件不存在', {
|
||||
statusCode: 404,
|
||||
errorCode: 'admin_webhook_not_found',
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
eventId: event.id,
|
||||
platform: event.platform,
|
||||
eventType: event.event_type,
|
||||
eventKey: event.event_key,
|
||||
signatureValid: Boolean(event.signature_valid),
|
||||
processed: Boolean(event.processed),
|
||||
processError: event.process_error,
|
||||
relatedOrderId: event.related_order_id,
|
||||
createdAt: event.created_at,
|
||||
headers: safeParseJson(event.headers_json),
|
||||
query: safeParseJson(event.query_json),
|
||||
body: safeParseJson(event.body_json),
|
||||
}
|
||||
}
|
||||
|
||||
export async function replayAdminWebhookEvent(eventId) {
|
||||
const event = getWebhookEventById(Number(eventId))
|
||||
|
||||
if (!event) {
|
||||
throw createHttpError('Webhook 事件不存在', {
|
||||
statusCode: 404,
|
||||
errorCode: 'admin_webhook_not_found',
|
||||
})
|
||||
}
|
||||
|
||||
if (event.platform !== 'agiso') {
|
||||
throw createHttpError('当前只支持重放 agiso webhook', {
|
||||
statusCode: 409,
|
||||
errorCode: 'admin_webhook_replay_not_supported',
|
||||
})
|
||||
}
|
||||
|
||||
const result = await replayAgisoTradeWebhookEvent(event)
|
||||
|
||||
return {
|
||||
eventId: event.id,
|
||||
replayed: true,
|
||||
result,
|
||||
}
|
||||
}
|
||||
|
||||
export function retryAdminTask(taskId) {
|
||||
const task = getRequiredTask(taskId)
|
||||
const now = nowIso()
|
||||
|
||||
if (!['retry_pending', 'manual_review', 'waiting_inventory'].includes(task.task_status)) {
|
||||
throw createHttpError('当前任务状态不允许重试', {
|
||||
statusCode: 409,
|
||||
errorCode: 'admin_task_retry_not_allowed',
|
||||
})
|
||||
}
|
||||
|
||||
const orderItem = listOrderItemsByOrderId(task.order_id).find((item) => item.id === task.order_item_id) || null
|
||||
let reservedCdkId = task.reserved_cdk_id
|
||||
let claimTokenId = task.claim_token_id
|
||||
let nextStatus = 'link_generated'
|
||||
let lastError = ''
|
||||
let expiresAt = task.expires_at
|
||||
let claimUrl = ''
|
||||
let token = ''
|
||||
|
||||
if (!reservedCdkId) {
|
||||
const reserved = reserveCdkForTask(orderItem?.sku_code || '', task.id)
|
||||
|
||||
if (!reserved) {
|
||||
nextStatus = 'waiting_inventory'
|
||||
lastError = '库存不足,等待可用 CDK'
|
||||
} else {
|
||||
reservedCdkId = reserved.id
|
||||
}
|
||||
}
|
||||
|
||||
if (nextStatus === 'link_generated' && !claimTokenId) {
|
||||
const claimToken = createTaskClaimToken(task.id)
|
||||
claimTokenId = claimToken.id
|
||||
expiresAt = claimToken.expired_at
|
||||
claimUrl = claimToken.claimUrl
|
||||
token = claimToken.token
|
||||
}
|
||||
|
||||
const updatedTask = updateTask(task.id, {
|
||||
task_status: nextStatus,
|
||||
reserved_cdk_id: reservedCdkId,
|
||||
claim_token_id: claimTokenId,
|
||||
expires_at: expiresAt,
|
||||
last_error: lastError,
|
||||
updated_at: now,
|
||||
})
|
||||
|
||||
const payload = {
|
||||
task: mapTaskActionPayload(updatedTask),
|
||||
}
|
||||
|
||||
if (claimUrl) {
|
||||
payload.claimUrl = claimUrl
|
||||
payload.token = token
|
||||
}
|
||||
|
||||
return payload
|
||||
}
|
||||
|
||||
export function releaseAdminTaskCdk(taskId) {
|
||||
const task = getRequiredTask(taskId)
|
||||
|
||||
if (!task.reserved_cdk_id) {
|
||||
throw createHttpError('当前任务没有预占 CDK', {
|
||||
statusCode: 409,
|
||||
errorCode: 'admin_task_no_reserved_cdk',
|
||||
})
|
||||
}
|
||||
|
||||
if (task.task_status === 'redeemed') {
|
||||
throw createHttpError('已兑换任务不能释放 CDK', {
|
||||
statusCode: 409,
|
||||
errorCode: 'admin_task_release_not_allowed',
|
||||
})
|
||||
}
|
||||
|
||||
releaseReservedCdk(task.reserved_cdk_id, nowIso())
|
||||
const updatedTask = updateTask(task.id, {
|
||||
reserved_cdk_id: null,
|
||||
task_status: 'waiting_inventory',
|
||||
last_error: '已手动释放预占 CDK',
|
||||
updated_at: nowIso(),
|
||||
})
|
||||
|
||||
return {
|
||||
task: mapTaskActionPayload(updatedTask),
|
||||
}
|
||||
}
|
||||
|
||||
export function regenerateAdminTaskClaimLink(taskId) {
|
||||
const task = getRequiredTask(taskId)
|
||||
|
||||
if (['redeemed', 'closed'].includes(task.task_status)) {
|
||||
throw createHttpError('当前任务状态不允许重新生成领取链接', {
|
||||
statusCode: 409,
|
||||
errorCode: 'admin_task_regenerate_not_allowed',
|
||||
})
|
||||
}
|
||||
|
||||
if (task.claim_token_id) {
|
||||
updateClaimToken(task.claim_token_id, {
|
||||
status: 'revoked',
|
||||
updated_at: nowIso(),
|
||||
})
|
||||
}
|
||||
|
||||
const claimToken = createTaskClaimToken(task.id)
|
||||
const updatedTask = updateTask(task.id, {
|
||||
claim_token_id: claimToken.id,
|
||||
expires_at: claimToken.expired_at,
|
||||
task_status: 'link_generated',
|
||||
last_error: '',
|
||||
updated_at: nowIso(),
|
||||
})
|
||||
|
||||
return {
|
||||
task: mapTaskActionPayload(updatedTask),
|
||||
claimUrl: claimToken.claimUrl,
|
||||
token: claimToken.token,
|
||||
}
|
||||
}
|
||||
|
||||
export function closeAdminTask(taskId) {
|
||||
const task = getRequiredTask(taskId)
|
||||
|
||||
if (task.task_status === 'redeemed') {
|
||||
throw createHttpError('已兑换任务不能关闭', {
|
||||
statusCode: 409,
|
||||
errorCode: 'admin_task_close_not_allowed',
|
||||
})
|
||||
}
|
||||
|
||||
const updatedTask = updateTask(task.id, {
|
||||
task_status: 'closed',
|
||||
last_error: task.last_error || '已手动关闭任务',
|
||||
updated_at: nowIso(),
|
||||
})
|
||||
|
||||
return {
|
||||
task: mapTaskActionPayload(updatedTask),
|
||||
}
|
||||
}
|
||||
|
||||
export function markAdminTaskManualReview(taskId) {
|
||||
const task = getRequiredTask(taskId)
|
||||
const updatedTask = updateTask(task.id, {
|
||||
task_status: 'manual_review',
|
||||
last_error: task.last_error || '已转人工处理',
|
||||
updated_at: nowIso(),
|
||||
})
|
||||
|
||||
return {
|
||||
task: mapTaskActionPayload(updatedTask),
|
||||
}
|
||||
}
|
||||
|
||||
function mapAdminTaskSummary(task) {
|
||||
const binding = buildTaskBindingState(task)
|
||||
|
||||
return {
|
||||
taskId: task.id,
|
||||
taskNo: task.task_no,
|
||||
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: task.retry_count,
|
||||
createdAt: task.created_at,
|
||||
updatedAt: task.updated_at,
|
||||
}
|
||||
}
|
||||
|
||||
function mapAdminCdkListItem(item) {
|
||||
const task = item.reserved_by_task_id ? getTaskById(item.reserved_by_task_id) : null
|
||||
const order = task?.order_id ? getOrderById(task.order_id) : null
|
||||
const binding = buildTaskBindingState(task)
|
||||
|
||||
return {
|
||||
cdkId: item.id,
|
||||
skuCode: item.sku_code,
|
||||
batchNo: item.batch_no,
|
||||
cdkCode: item.cdk_code,
|
||||
status: item.status,
|
||||
reservedByTaskId: item.reserved_by_task_id,
|
||||
reservedByTaskNo: task?.task_no || '',
|
||||
platformOrderId: order?.platform_order_id || '',
|
||||
systemBindingStatus: item.status === 'delivered' ? 'system_bound' : binding.systemBindingStatus,
|
||||
userBindingStatus: item.status === 'delivered' ? 'binding_completed' : binding.userBindingStatus,
|
||||
invalidReason: item.invalid_reason || '',
|
||||
deliveredAt: item.delivered_at,
|
||||
createdAt: item.created_at,
|
||||
updatedAt: item.updated_at,
|
||||
}
|
||||
}
|
||||
|
||||
function mapTaskActionPayload(task) {
|
||||
return {
|
||||
taskId: task.id,
|
||||
taskNo: task.task_no,
|
||||
status: task.task_status,
|
||||
reservedCdkId: task.reserved_cdk_id,
|
||||
claimTokenId: task.claim_token_id,
|
||||
lastError: task.last_error,
|
||||
updatedAt: task.updated_at,
|
||||
}
|
||||
}
|
||||
|
||||
function getRequiredTask(taskId) {
|
||||
const task = getTaskById(Number(taskId))
|
||||
|
||||
if (!task) {
|
||||
throw createHttpError('任务不存在', {
|
||||
statusCode: 404,
|
||||
errorCode: 'admin_task_not_found',
|
||||
})
|
||||
}
|
||||
|
||||
return task
|
||||
}
|
||||
|
||||
function getRequiredCdk(cdkId) {
|
||||
const cdk = getCdkById(Number(cdkId))
|
||||
|
||||
if (!cdk) {
|
||||
throw createHttpError('CDK 不存在', {
|
||||
statusCode: 404,
|
||||
errorCode: 'admin_cdk_not_found',
|
||||
})
|
||||
}
|
||||
|
||||
return cdk
|
||||
}
|
||||
|
||||
function mapAdminTaskListItem(task) {
|
||||
const binding = buildTaskBindingState(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,
|
||||
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: task.retry_count,
|
||||
lastError: task.last_error,
|
||||
createdAt: task.created_at,
|
||||
updatedAt: task.updated_at,
|
||||
reservedCdkCodeMasked: maskCode(task.cdk_code),
|
||||
claimToken: task.claim_token || '',
|
||||
screenshotPath: task.screenshot_path || '',
|
||||
}
|
||||
}
|
||||
|
||||
function mapAdminOrderListItem(item) {
|
||||
const tasks = listTasksByOrderId(item.id)
|
||||
const bindingSummary = buildOrderBindingSummary(tasks)
|
||||
|
||||
return {
|
||||
orderId: item.id,
|
||||
platform: item.platform,
|
||||
platformOrderId: item.platform_order_id,
|
||||
orderStatus: item.order_status,
|
||||
payStatus: item.pay_status,
|
||||
buyerName: item.buyer_name,
|
||||
totalAmount: item.total_amount,
|
||||
currency: item.currency,
|
||||
createdAt: item.created_at,
|
||||
updatedAt: item.updated_at,
|
||||
taskCount: Number(item.task_count || tasks.length || 0),
|
||||
systemBindingStatus: bindingSummary.systemBindingStatus,
|
||||
userBindingStatus: bindingSummary.userBindingStatus,
|
||||
systemBoundTaskCount: bindingSummary.systemBoundTaskCount,
|
||||
completedBindingTaskCount: bindingSummary.completedBindingTaskCount,
|
||||
}
|
||||
}
|
||||
|
||||
function buildOrderBindingSummary(tasks) {
|
||||
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
|
||||
|
||||
let systemBindingStatus = 'pending_binding'
|
||||
let userBindingStatus = 'not_started'
|
||||
|
||||
if (totalTaskCount === 0) {
|
||||
return {
|
||||
totalTaskCount,
|
||||
systemBoundTaskCount,
|
||||
completedBindingTaskCount,
|
||||
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,
|
||||
systemBindingStatus,
|
||||
userBindingStatus,
|
||||
}
|
||||
}
|
||||
|
||||
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 === '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 && (task.reserved_cdk_id || task.claim_token_id))
|
||||
}
|
||||
|
||||
function normalizeCdkImportRows(payload) {
|
||||
const rows = []
|
||||
const directRows = Array.isArray(payload.rows) ? payload.rows : []
|
||||
const bulkCodes = Array.isArray(payload.codes) ? payload.codes : []
|
||||
|
||||
if (directRows.length > 0) {
|
||||
for (const row of directRows) {
|
||||
const skuCode = String(row?.skuCode || '').trim()
|
||||
const cdkCode = String(row?.cdkCode || '').trim()
|
||||
const batchNo = String(row?.batchNo || '').trim()
|
||||
|
||||
if (!skuCode || !cdkCode) {
|
||||
continue
|
||||
}
|
||||
|
||||
rows.push({ skuCode, cdkCode, batchNo })
|
||||
}
|
||||
}
|
||||
|
||||
if (bulkCodes.length > 0) {
|
||||
const skuCode = String(payload.skuCode || '').trim()
|
||||
const batchNo = String(payload.batchNo || '').trim()
|
||||
|
||||
if (!skuCode) {
|
||||
throw createHttpError('批量导入时缺少 skuCode', {
|
||||
statusCode: 400,
|
||||
errorCode: 'admin_cdk_import_missing_sku',
|
||||
})
|
||||
}
|
||||
|
||||
for (const code of bulkCodes) {
|
||||
const cdkCode = String(code || '').trim()
|
||||
if (!cdkCode) {
|
||||
continue
|
||||
}
|
||||
rows.push({ skuCode, cdkCode, batchNo })
|
||||
}
|
||||
}
|
||||
|
||||
return dedupeRows(rows)
|
||||
}
|
||||
|
||||
function dedupeRows(rows) {
|
||||
const seen = new Set()
|
||||
const output = []
|
||||
|
||||
for (const row of rows) {
|
||||
const key = `${row.skuCode}::${row.cdkCode}`
|
||||
if (seen.has(key)) {
|
||||
continue
|
||||
}
|
||||
seen.add(key)
|
||||
output.push(row)
|
||||
}
|
||||
|
||||
return output
|
||||
}
|
||||
|
||||
function normalizePage(rawValue) {
|
||||
const parsed = Number(rawValue)
|
||||
return Number.isFinite(parsed) && parsed > 0 ? Math.floor(parsed) : 1
|
||||
}
|
||||
|
||||
function normalizePageSize(rawValue) {
|
||||
const parsed = Number(rawValue)
|
||||
if (!Number.isFinite(parsed) || parsed <= 0) {
|
||||
return 20
|
||||
}
|
||||
return Math.min(100, Math.floor(parsed))
|
||||
}
|
||||
|
||||
function normalizeDateQuery(rawValue, endOfDay = false) {
|
||||
const normalized = String(rawValue || '').trim()
|
||||
|
||||
if (!normalized) {
|
||||
return ''
|
||||
}
|
||||
|
||||
if (/^\d{4}-\d{2}-\d{2}$/.test(normalized)) {
|
||||
return endOfDay ? `${normalized}T23:59:59.999Z` : `${normalized}T00:00:00.000Z`
|
||||
}
|
||||
|
||||
return normalized
|
||||
}
|
||||
|
||||
function safeParseJson(rawText) {
|
||||
try {
|
||||
return JSON.parse(String(rawText || '{}'))
|
||||
} catch {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
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)}`
|
||||
}
|
||||
Reference in New Issue
Block a user