642 lines
24 KiB
TypeScript
642 lines
24 KiB
TypeScript
import { buildClaimUrl, buildConfiguredClaimUrl } from '../claim/claim-service.js'
|
||
import { buildClaimIdentityAdminSummary } from '../claim/claim-identity.js'
|
||
import { getClaimTokenById } from '../../repositories/claim-token-repo.js'
|
||
import {
|
||
listOrderItemsByOrderId,
|
||
listOrderItemsByOrderIds,
|
||
} from '../../repositories/order-item-repo.js'
|
||
import { getOrderById, listOrders } from '../../repositories/order-repo.js'
|
||
import { listOrderRefunds } from '../finance/order-refund-service.js'
|
||
import { getManualFulfillmentCountsByOrderIds } from '../finance/manual-fulfillment-service.js'
|
||
import {
|
||
getTaskById,
|
||
listTasks,
|
||
listTasksByOrderId,
|
||
listTasksByOrderIds,
|
||
} from '../../repositories/task-repo.js'
|
||
import { listTaskEventsByTaskId } from '../../repositories/task-event-repo.js'
|
||
import type { JsonRecord } from '../../types/json.js'
|
||
import {
|
||
listKuaishouIndustryVouchersByOid,
|
||
listKuaishouIndustryVouchersByOids,
|
||
listKuaishouIndustryVouchersByTaskId,
|
||
} from '../../repositories/kuaishou-industry-voucher-repo.js'
|
||
|
||
import { createHttpError } from '../../utils/http.js'
|
||
import { formatFenToAmount, normalizeFen } from '../../utils/money.js'
|
||
import {
|
||
normalizeDateQuery,
|
||
normalizeDateString,
|
||
normalizePage,
|
||
normalizePageSize,
|
||
safeParseJson,
|
||
} from './admin-query-utils.js'
|
||
import {
|
||
canCompleteManualDispatchTaskStatus,
|
||
canMarkManualReviewTaskStatus,
|
||
canRetryTaskStatus,
|
||
} from '../../domain/task-status.js'
|
||
import { getAffiliateDashConfig } from '../platforms/affiliate-dash/config.js'
|
||
import { getDouyinMashidaSourceConfig } from '../platforms/douyin-mashida/source-config-service.js'
|
||
import { isDouyinMashidaProvider } from '../platforms/douyin-mashida/order-mapper.js'
|
||
import {
|
||
mergeAffiliateDashSkuMapping,
|
||
resolveAffiliateDashDisplayProductName,
|
||
} from '../platforms/affiliate-dash/sku-mapping.js'
|
||
import {
|
||
canViewerCloseTask,
|
||
canRegenerateClaimLinkForViewer,
|
||
createAdminViewerContext,
|
||
getTaskPrimaryClaimTokenId,
|
||
isManualDispatchTask,
|
||
mapKuaishouCloudFulfillmentContext,
|
||
mapKuaishouCloudTaskStateProjection,
|
||
mapManualDispatchContext,
|
||
mapRedeemResolutionContext,
|
||
parseTaskContext,
|
||
parseTaskState,
|
||
resolveAdminTaskScreenshotUrl,
|
||
resolveDisplayShopName,
|
||
resolveOrderItemDeliveryMode,
|
||
resolveOrderItemMatchSku,
|
||
resolveOrderItemSkuNick,
|
||
resolveOrderItemTitle,
|
||
} from './admin-read-shared-helpers.js'
|
||
import { isRetiredFulfillmentExecutor } from '../fulfillment/executors/types.js'
|
||
import {
|
||
mapAdminOrderListItem,
|
||
normalizeOrderItemFulfillmentMode,
|
||
resolveOrderFulfillmentSummary,
|
||
summarizeOrderItems,
|
||
} from './admin-order-read-helpers.js'
|
||
import {
|
||
listWorkOrdersByOrderId,
|
||
listWorkOrdersByOrderIds,
|
||
} from '../../repositories/worker-platform/work-order-query-repo.js'
|
||
import {
|
||
buildOrderFulfillmentProgress,
|
||
getRequiredTask,
|
||
mapAdminTaskEvent,
|
||
mapAdminTaskListItem,
|
||
mapAdminTaskSummary,
|
||
} from './admin-task-read-helpers.js'
|
||
|
||
import type {
|
||
AdminOrderListResponse,
|
||
AdminTaskListResponse,
|
||
} from '../../types/admin/read-models.js'
|
||
import type { AdminTaskDetailView } from '../../types/admin/task-detail.js'
|
||
import type {
|
||
AdminEntityIdInput,
|
||
AdminOrderListQueryInput,
|
||
AdminTaskListQueryInput,
|
||
AdminViewerSessionInput,
|
||
} from '../../types/admin/read-inputs.js'
|
||
import type { KuaishouIndustryVoucherRow, OrderRow, TaskRow } from '../../types/repository/rows.js'
|
||
|
||
export async function getAdminOrders(
|
||
query: AdminOrderListQueryInput = {},
|
||
): Promise<AdminOrderListResponse> {
|
||
const page = normalizePage(query.page)
|
||
const pageSize = normalizePageSize(query.pageSize)
|
||
const { items, total } = await listOrders({
|
||
page,
|
||
pageSize,
|
||
keyword: String(query.keyword || '').trim(),
|
||
provider: String(query.provider || '').trim(),
|
||
executorKey: String(query.executorKey || '').trim(),
|
||
fulfillmentMode: normalizeOrderFulfillmentModeFilter(query.fulfillmentMode),
|
||
platformOrderId: String(query.platformOrderId || '').trim(),
|
||
payStatus: String(query.payStatus || '').trim(),
|
||
taskStatus: String(query.taskStatus || '').trim(),
|
||
skuCode: String(query.skuCode || '').trim(),
|
||
dateFrom: normalizeDateString(query.dateFrom),
|
||
dateTo: normalizeDateString(query.dateTo),
|
||
})
|
||
|
||
const orderIds = items.map((item) => Number(item.id)).filter((id) => id > 0)
|
||
const oids = items.map((item) => item.platform_order_id).filter(Boolean)
|
||
const [orderItems, tasks, vouchers, workOrders, manualCounts] = await Promise.all([
|
||
listOrderItemsByOrderIds(orderIds),
|
||
listTasksByOrderIds(orderIds),
|
||
listKuaishouIndustryVouchersByOids(oids),
|
||
listWorkOrdersByOrderIds(orderIds),
|
||
getManualFulfillmentCountsByOrderIds(orderIds),
|
||
])
|
||
const orderItemsByOrderId = groupRowsByOrderId(orderItems)
|
||
const tasksByOrderId = groupRowsByOrderId(tasks)
|
||
const vouchersByOid = groupRowsByOid(vouchers)
|
||
const workOrdersByOrderId = groupRowsByOrderId(workOrders)
|
||
|
||
return {
|
||
items: items.map((item) => {
|
||
const orderId = Number(item.id)
|
||
const orderTasks = tasksByOrderId.get(orderId) || []
|
||
const orderVouchers = vouchersByOid.get(item.platform_order_id) || []
|
||
const orderWorkOrders = workOrdersByOrderId.get(orderId) || []
|
||
return mapAdminOrderListItem(item, {
|
||
orderItems: orderItemsByOrderId.get(orderId) || [],
|
||
tasks: orderTasks,
|
||
vouchers: orderVouchers,
|
||
workOrderCount: orderWorkOrders.length,
|
||
manualConfig: manualCounts.get(orderId) || { pending: 0, unconfigured: 0 },
|
||
})
|
||
}),
|
||
pagination: { page, pageSize, total },
|
||
}
|
||
}
|
||
|
||
const ADMIN_ORDER_FULFILLMENT_MODES = new Set([
|
||
'work_order',
|
||
'affiliate_dash',
|
||
'legacy_platform',
|
||
'manual',
|
||
'mixed',
|
||
'pending',
|
||
])
|
||
|
||
function normalizeOrderFulfillmentModeFilter(value: unknown): string {
|
||
const mode = String(value || '')
|
||
.trim()
|
||
.toLowerCase()
|
||
return ADMIN_ORDER_FULFILLMENT_MODES.has(mode) ? mode : ''
|
||
}
|
||
|
||
function groupRowsByOrderId<T extends { order_id?: number | string | null }>(
|
||
rows: T[],
|
||
): Map<number, T[]> {
|
||
const grouped = new Map<number, T[]>()
|
||
for (const row of rows) {
|
||
// pg BIGINT 常以 string 返回;统一 Number 键,避免列表挂不上 order_items →「商品 0」。
|
||
const orderId = Number(row.order_id)
|
||
if (!Number.isFinite(orderId) || orderId <= 0) {
|
||
continue
|
||
}
|
||
const group = grouped.get(orderId) || []
|
||
group.push(row)
|
||
grouped.set(orderId, group)
|
||
}
|
||
return grouped
|
||
}
|
||
|
||
function groupRowsByOid<T extends { oid: string }>(rows: T[]): Map<string, T[]> {
|
||
const grouped = new Map<string, T[]>()
|
||
for (const row of rows) {
|
||
const group = grouped.get(row.oid) || []
|
||
group.push(row)
|
||
grouped.set(row.oid, group)
|
||
}
|
||
return grouped
|
||
}
|
||
|
||
export async function getAdminOrderDetail(
|
||
orderId: AdminEntityIdInput,
|
||
session: AdminViewerSessionInput | null = null,
|
||
): Promise<JsonRecord> {
|
||
const order = await getOrderById(Number(orderId))
|
||
|
||
if (!order) {
|
||
throw createHttpError('订单不存在', {
|
||
statusCode: 404,
|
||
errorCode: 'admin_order_not_found',
|
||
})
|
||
}
|
||
|
||
const [items, tasks, workOrders, refunds, manualCounts] = await Promise.all([
|
||
listOrderItemsByOrderId(order.id),
|
||
listTasksByOrderId(order.id),
|
||
listWorkOrdersByOrderId(order.id),
|
||
listOrderRefunds(order.id),
|
||
getManualFulfillmentCountsByOrderIds([order.id]),
|
||
])
|
||
const manualConfig = manualCounts.get(Number(order.id)) || { pending: 0, unconfigured: 0 }
|
||
const kuaishouIndustryVouchers = await listKuaishouIndustryVouchersByOid(order.platform_order_id)
|
||
const itemSummary = summarizeOrderItems(items, order, kuaishouIndustryVouchers)
|
||
const orderItemTitle = resolveOrderItemTitle(items[0], order, kuaishouIndustryVouchers)
|
||
const orderSkuNick = resolveOrderItemSkuNick(items[0], order, kuaishouIndustryVouchers)
|
||
const viewerContext = createAdminViewerContext(session)
|
||
const fulfillment = resolveOrderFulfillmentSummary({
|
||
items,
|
||
tasks,
|
||
workOrderCount: workOrders.length,
|
||
vouchers: kuaishouIndustryVouchers,
|
||
})
|
||
|
||
return {
|
||
order: {
|
||
orderId: order.id,
|
||
provider: order.provider || '',
|
||
platform: order.platform,
|
||
shopId: order.shop_id || '',
|
||
shopName: resolveDisplayShopName(order.provider, order.shop_id, order.shop_name),
|
||
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: formatFenToAmount(order.total_amount),
|
||
totalAmountFen: normalizeFen(order.total_amount),
|
||
currency: order.currency,
|
||
paidAt: order.paid_at,
|
||
createdAt: order.created_at,
|
||
updatedAt: order.updated_at,
|
||
itemSummary,
|
||
itemTitle: orderItemTitle,
|
||
skuNick: orderSkuNick,
|
||
rawPayload:
|
||
order.raw_payload_json && typeof order.raw_payload_json === 'object'
|
||
? order.raw_payload_json
|
||
: JSON.parse(String(order.raw_payload_json || '{}')),
|
||
fulfillmentProgress: buildOrderFulfillmentProgress(tasks),
|
||
claimStatus: order.claim_status || 'active',
|
||
claimStatusReason: order.claim_status_reason || '',
|
||
claimStatusUpdatedAt: order.claim_status_updated_at || null,
|
||
fulfillmentMode: fulfillment.fulfillmentMode,
|
||
manualItemCount: fulfillment.manualItemCount,
|
||
pendingItemCount: fulfillment.pendingItemCount,
|
||
manualPendingCount: manualConfig.pending,
|
||
manualUnconfiguredCount: manualConfig.unconfigured,
|
||
hasVoucher: fulfillment.hasVoucher,
|
||
voucherState: fulfillment.voucherState,
|
||
workOrderCount: workOrders.length,
|
||
canClaimH5: fulfillment.canClaimH5,
|
||
},
|
||
items: items.map((item) => {
|
||
const revenueFen = Math.max(0, Number(item.revenue_amount_fen || 0))
|
||
const refundedFen = refunds
|
||
.filter((refund) => refund.status === 'confirmed' && refund.orderItemId === Number(item.id))
|
||
.reduce((sum, refund) => sum + refund.amountFen, 0)
|
||
return {
|
||
orderItemId: Number(item.id),
|
||
skuCode: item.sku_code,
|
||
skuName: item.sku_name,
|
||
itemTitle: resolveOrderItemTitle(item, order, kuaishouIndustryVouchers),
|
||
skuNick: resolveOrderItemSkuNick(item, order, kuaishouIndustryVouchers),
|
||
matchSku: resolveOrderItemMatchSku(item, order, kuaishouIndustryVouchers),
|
||
quantity: item.quantity,
|
||
revenueFen,
|
||
refundedFen,
|
||
refundableFen: Math.max(0, revenueFen - refundedFen),
|
||
fulfillmentMode: normalizeOrderItemFulfillmentMode(item.fulfillment_mode),
|
||
deliveryMode: resolveOrderItemDeliveryMode(tasks, item.id),
|
||
spec:
|
||
item.spec_json && typeof item.spec_json === 'object'
|
||
? item.spec_json
|
||
: JSON.parse(String(item.spec_json || '{}')),
|
||
}
|
||
}),
|
||
refunds,
|
||
tasks: tasks.map((task) => mapAdminTaskSummary(task)),
|
||
kuaishouIndustryVouchers: kuaishouIndustryVouchers.map(mapAdminKuaishouIndustryVoucher),
|
||
operations: {
|
||
canResendKuaishouIndustryVoucherCode:
|
||
viewerContext.canOperateKuaishouIndustryVoucher && kuaishouIndustryVouchers.length > 0,
|
||
},
|
||
}
|
||
}
|
||
|
||
function mapAdminKuaishouIndustryVoucher(voucher: {
|
||
voucher_code?: unknown
|
||
oid?: unknown
|
||
unit_index?: unknown
|
||
status?: unknown
|
||
send_callback_status?: unknown
|
||
send_callback_attempt_count?: unknown
|
||
send_callback_last_error?: unknown
|
||
send_callback_sent_at?: unknown
|
||
consume_serial_num?: unknown
|
||
consume_details_json?: unknown
|
||
consumed_at?: unknown
|
||
destroyed_at?: unknown
|
||
}) {
|
||
return {
|
||
oid: String(voucher.oid || '').trim(),
|
||
voucherCode: String(voucher.voucher_code || '').trim(),
|
||
unitIndex: Number(voucher.unit_index || 0) || 0,
|
||
status: String(voucher.status || '').trim(),
|
||
sendCallbackStatus: String(voucher.send_callback_status || '').trim(),
|
||
sendCallbackAttemptCount: Number(voucher.send_callback_attempt_count || 0) || 0,
|
||
sendCallbackLastError: String(voucher.send_callback_last_error || '').trim(),
|
||
sendCallbackSentAt: voucher.send_callback_sent_at || null,
|
||
consumeSerialNum: String(voucher.consume_serial_num || '').trim(),
|
||
consumeDetails: normalizeAdminJsonArray(voucher.consume_details_json),
|
||
consumedAt: voucher.consumed_at || null,
|
||
destroyedAt: voucher.destroyed_at || null,
|
||
}
|
||
}
|
||
|
||
export async function getAdminTasks(
|
||
query: AdminTaskListQueryInput = {},
|
||
session: AdminViewerSessionInput | null = null,
|
||
): Promise<AdminTaskListResponse> {
|
||
const page = normalizePage(query.page)
|
||
const pageSize = normalizePageSize(query.pageSize)
|
||
const { items, total } = await listTasks({
|
||
page,
|
||
pageSize,
|
||
keyword: String(query.keyword || '').trim(),
|
||
executorKey: String(query.executorKey || '').trim(),
|
||
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),
|
||
})
|
||
const viewerContext = createAdminViewerContext(session)
|
||
|
||
return {
|
||
items: items.map((item) => mapAdminTaskListItem(item, viewerContext)),
|
||
pagination: { page, pageSize, total },
|
||
}
|
||
}
|
||
|
||
export async function getAdminTaskDetail(
|
||
taskId: AdminEntityIdInput,
|
||
session: AdminViewerSessionInput | null = null,
|
||
): Promise<AdminTaskDetailView> {
|
||
const task = await getTaskById(Number(taskId))
|
||
|
||
if (!task) {
|
||
throw createHttpError('任务不存在', {
|
||
statusCode: 404,
|
||
errorCode: 'admin_task_not_found',
|
||
})
|
||
}
|
||
|
||
const primaryClaimTokenId = getTaskPrimaryClaimTokenId(task)
|
||
const [order, claimToken, taskEvents, taskKuaishouIndustryVouchers, oidKuaishouIndustryVouchers] =
|
||
await Promise.all([
|
||
getOrderById(task.order_id),
|
||
primaryClaimTokenId ? getClaimTokenById(primaryClaimTokenId) : Promise.resolve(null),
|
||
listTaskEventsByTaskId(task.id),
|
||
listKuaishouIndustryVouchersByTaskId(task.id),
|
||
task.platform_order_id
|
||
? listKuaishouIndustryVouchersByOid(task.platform_order_id)
|
||
: Promise.resolve([]),
|
||
])
|
||
const orderItems = order ? await listOrderItemsByOrderId(order.id) : []
|
||
const orderItem = orderItems.find((item) => item.id === task.order_item_id) || null
|
||
const taskContext = parseTaskContext(task)
|
||
const taskState = parseTaskState(task)
|
||
const viewerContext = createAdminViewerContext(session)
|
||
// 后台回显的领取链接口径按**订单来源**决定(不再按 executor_key):
|
||
// 抖音码速达订单用后台配置的 `claimLinkBase`(买家领取页,走 affiliate_dash 领取流程);
|
||
// 其它来源沿用全局 buildClaimUrl(orders.claimBaseUrl)。
|
||
const claimUrl = resolveAdminTaskClaimUrl(claimToken, order)
|
||
const screenshotUrl = await resolveAdminTaskScreenshotUrl(task, viewerContext)
|
||
const cloudSourceLabelMap = buildCloudSourceLabelMap()
|
||
const kuaishouCloudFulfillment =
|
||
mapKuaishouCloudFulfillmentContext(taskContext.kuaishouCloudFulfillment, {
|
||
cloudSourceLabelMap,
|
||
}) || mapKuaishouCloudTaskStateProjection(task, { cloudSourceLabelMap })
|
||
const claimIdentity = buildClaimIdentityAdminSummary(taskContext, {
|
||
flowLike: taskContext.kuaishouCloudFulfillment || taskContext.kuaishouFeifei || null,
|
||
taskRoleId: task.role_id,
|
||
taskRoleName: task.role_name,
|
||
})
|
||
const firstKuaishouIndustryVoucher = resolveKuaishouIndustryVoucherForTask(
|
||
task,
|
||
taskContext,
|
||
taskKuaishouIndustryVouchers,
|
||
oidKuaishouIndustryVouchers,
|
||
)
|
||
const kuaishouIndustryVoucher = firstKuaishouIndustryVoucher
|
||
? mapAdminKuaishouIndustryVoucher(firstKuaishouIndustryVoucher)
|
||
: mapKuaishouIndustryVoucherContext(taskContext.kuaishouIndustryVoucher)
|
||
const hasKuaishouIndustryVoucher = Boolean(
|
||
kuaishouIndustryVoucher && String(kuaishouIndustryVoucher.voucherCode || '').trim(),
|
||
)
|
||
const kuaishouIndustryVoucherStatus = String(kuaishouIndustryVoucher?.status || '')
|
||
.trim()
|
||
.toUpperCase()
|
||
const kuaishouIndustryVoucherSendCallbackStatus = String(
|
||
kuaishouIndustryVoucher?.sendCallbackStatus || '',
|
||
)
|
||
.trim()
|
||
.toLowerCase()
|
||
|
||
return {
|
||
task: mapAdminTaskListItem(
|
||
{
|
||
...task,
|
||
sku_code: orderItem?.sku_code || '',
|
||
sku_name: orderItem?.sku_name || '',
|
||
claim_token: claimToken?.token || '',
|
||
},
|
||
viewerContext,
|
||
),
|
||
order: order
|
||
? {
|
||
orderId: order.id,
|
||
provider: order.provider || '',
|
||
platform: order.platform,
|
||
shopId: order.shop_id || '',
|
||
shopName: order.shop_name || '',
|
||
platformOrderId: order.platform_order_id,
|
||
payStatus: order.pay_status,
|
||
orderStatus: order.order_status,
|
||
claimStatus: (order.claim_status as 'active' | 'paused' | 'closed') || 'active',
|
||
claimStatusReason: order.claim_status_reason || null,
|
||
claimStatusUpdatedAt: order.claim_status_updated_at || null,
|
||
}
|
||
: null,
|
||
orderItem: orderItem
|
||
? {
|
||
orderItemId: orderItem.id,
|
||
skuCode: orderItem.sku_code,
|
||
skuName: resolveAffiliateDashDisplayProductName(
|
||
String(orderItem.sku_code || ''),
|
||
String(orderItem.sku_name || ''),
|
||
mergeAffiliateDashSkuMapping(getAffiliateDashConfig().skuMapping),
|
||
),
|
||
quantity: orderItem.quantity,
|
||
}
|
||
: null,
|
||
claimToken: claimToken
|
||
? {
|
||
primaryClaimTokenId: claimToken.id,
|
||
token: viewerContext.canViewSensitiveTaskData ? claimToken.token || '' : '',
|
||
status: claimToken.status || '',
|
||
expiredAt: claimToken.expired_at || '',
|
||
claimUrl,
|
||
}
|
||
: null,
|
||
artifacts: viewerContext.canViewSensitiveTaskData ? safeParseJson(task.artifacts_json) : {},
|
||
screenshotUrl,
|
||
review: {
|
||
required: false,
|
||
screenshotCapturedAt: String(taskState.reviewCapturedAt || '').trim() || null,
|
||
roleId: String(taskState.reviewRoleId || '').trim() || '',
|
||
roleName: String(taskState.reviewRoleName || '').trim() || '',
|
||
},
|
||
redeemResolution: mapRedeemResolutionContext(taskContext.redeemResolution),
|
||
claimIdentity,
|
||
kuaishouCloudFulfillment,
|
||
kuaishouIndustryVoucher,
|
||
manualDispatch: mapManualDispatchContext(taskContext.manualDispatch, viewerContext),
|
||
affiliateDash: taskContext.affiliateDash || null,
|
||
events: taskEvents.map(mapAdminTaskEvent),
|
||
operations: {
|
||
canRetry:
|
||
viewerContext.canManageTaskLifecycle &&
|
||
!isManualDispatchTask(task) &&
|
||
!isRetiredFulfillmentExecutor(task.executor_key) &&
|
||
canRetryTaskStatus(task.task_status),
|
||
canRegenerateClaimLink: canRegenerateClaimLinkForViewer(task, viewerContext),
|
||
canClose: canViewerCloseTask(task, viewerContext),
|
||
canMarkManualReview:
|
||
viewerContext.canManageTaskLifecycle && canMarkManualReviewTaskStatus(task.task_status),
|
||
canCompleteManualDispatch:
|
||
viewerContext.canManageTaskLifecycle &&
|
||
(isManualDispatchTask(task) || isRetiredFulfillmentExecutor(task.executor_key)) &&
|
||
canCompleteManualDispatchTaskStatus(task.task_status),
|
||
canPrepareKuaishouCloudFulfillment: false,
|
||
canRefreshKuaishouCloudRoleInfo: false,
|
||
canRebindKuaishouCloudRole: false,
|
||
canDispatchKuaishouCloudFulfillment: false,
|
||
canReturnKuaishouCloudFulfillment: false,
|
||
canResendKuaishouIndustryVoucherCode:
|
||
viewerContext.canOperateKuaishouIndustryVoucher && hasKuaishouIndustryVoucher,
|
||
canConsumeKuaishouIndustryVoucher:
|
||
viewerContext.canOperateKuaishouIndustryVoucher &&
|
||
hasKuaishouIndustryVoucher &&
|
||
kuaishouIndustryVoucherStatus !== 'CONSUMED' &&
|
||
kuaishouIndustryVoucherSendCallbackStatus === 'success',
|
||
canViewSensitiveTaskData: viewerContext.canViewSensitiveTaskData,
|
||
},
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 后台展示的领取链接。
|
||
*
|
||
* - 抖音码速达订单(`provider='douyin_mashida'`):`claimLinkBase` + token(买家领取页,
|
||
* 走 affiliate_dash 领取流程);未配 `claimLinkBase` 时退回全局 claim 链接,至少链接不会为空。
|
||
* - 其它来源:沿用全局 `buildClaimUrl`(orders.claimBaseUrl),历史行为不变。
|
||
*
|
||
* ⚠️ 这里按 **provider** 判断,不按 executor_key:抖音皮肤单已改走 `affiliate_dash` 通道,
|
||
* 按执行器判断会失效。拿不到 provider(order 为空)时按「其它来源」处理。
|
||
*/
|
||
function resolveAdminTaskClaimUrl(
|
||
claimToken: { token: string } | null | undefined,
|
||
order: Pick<OrderRow, 'provider'> | null | undefined,
|
||
): string {
|
||
const token = String(claimToken?.token || '').trim()
|
||
if (!token) {
|
||
return ''
|
||
}
|
||
|
||
if (isDouyinMashidaProvider(order?.provider)) {
|
||
const claimUrl = buildConfiguredClaimUrl(getDouyinMashidaSourceConfig().claimLinkBase, token)
|
||
if (claimUrl) {
|
||
return claimUrl
|
||
}
|
||
}
|
||
|
||
return buildClaimUrl(token)
|
||
}
|
||
|
||
function mapKuaishouIndustryVoucherContext(value: unknown): JsonRecord | null {
|
||
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
||
return null
|
||
}
|
||
|
||
const record = value as JsonRecord
|
||
return {
|
||
oid: String(record.oid || '').trim(),
|
||
voucherCode: String(record.voucherCode || record.eticketId || '').trim(),
|
||
unitIndex: Number(record.unitIndex || 0) || 0,
|
||
status: String(record.status || '').trim(),
|
||
sendCallbackStatus: String(record.sendCallbackStatus || '').trim(),
|
||
sendCallbackLastError: String(record.sendCallbackLastError || '').trim(),
|
||
sendCallbackAttemptCount: Number(record.sendCallbackAttemptCount || 0) || 0,
|
||
sendCallbackSentAt: record.sendCallbackSentAt || null,
|
||
consumeSerialNum: String(record.consumeSerialNum || '').trim(),
|
||
consumeDetails: normalizeAdminJsonArray(record.consumeDetails),
|
||
consumedAt: record.consumedAt || null,
|
||
destroyedAt: record.destroyedAt || null,
|
||
}
|
||
}
|
||
|
||
function normalizeAdminJsonArray(value: unknown): JsonRecord[] {
|
||
if (Array.isArray(value)) {
|
||
return value.filter((item): item is JsonRecord =>
|
||
Boolean(item && typeof item === 'object' && !Array.isArray(item)),
|
||
)
|
||
}
|
||
|
||
try {
|
||
const parsed = JSON.parse(String(value || '[]'))
|
||
return Array.isArray(parsed)
|
||
? parsed.filter((item): item is JsonRecord =>
|
||
Boolean(item && typeof item === 'object' && !Array.isArray(item)),
|
||
)
|
||
: []
|
||
} catch {
|
||
return []
|
||
}
|
||
}
|
||
|
||
function resolveKuaishouIndustryVoucherForTask(
|
||
task: TaskRow,
|
||
taskContext: JsonRecord,
|
||
taskVouchers: KuaishouIndustryVoucherRow[],
|
||
oidVouchers: KuaishouIndustryVoucherRow[],
|
||
): KuaishouIndustryVoucherRow | null {
|
||
const linkedVoucher = taskVouchers[0] || null
|
||
if (linkedVoucher) {
|
||
return linkedVoucher
|
||
}
|
||
|
||
const voucherContext =
|
||
taskContext.kuaishouIndustryVoucher &&
|
||
typeof taskContext.kuaishouIndustryVoucher === 'object' &&
|
||
!Array.isArray(taskContext.kuaishouIndustryVoucher)
|
||
? (taskContext.kuaishouIndustryVoucher as JsonRecord)
|
||
: {}
|
||
const voucherCode = String(voucherContext.voucherCode || voucherContext.eticketId || '').trim()
|
||
if (voucherCode) {
|
||
const matchedByCode = oidVouchers.find(
|
||
(voucher) => String(voucher.voucher_code || '').trim() === voucherCode,
|
||
)
|
||
if (matchedByCode) {
|
||
return matchedByCode
|
||
}
|
||
}
|
||
|
||
const unitIndex = Number(task.unit_index || 0) || 0
|
||
if (unitIndex > 0) {
|
||
const matchedByUnit = oidVouchers.find(
|
||
(voucher) => Number(voucher.unit_index || 0) === unitIndex,
|
||
)
|
||
if (matchedByUnit) {
|
||
return matchedByUnit
|
||
}
|
||
}
|
||
|
||
return oidVouchers.length === 1 ? oidVouchers[0] || null : null
|
||
}
|
||
|
||
function buildCloudSourceLabelMap() {
|
||
return new Map<string, string>()
|
||
}
|
||
|
||
export async function getAdminTaskScreenshotPath(
|
||
taskId: AdminEntityIdInput,
|
||
_session: AdminViewerSessionInput | null = null,
|
||
): Promise<string> {
|
||
const task = await getRequiredTask(taskId)
|
||
|
||
if (task.screenshot_path) {
|
||
return task.screenshot_path
|
||
}
|
||
|
||
throw createHttpError('当前任务还没有可查看截图', {
|
||
statusCode: 404,
|
||
errorCode: 'admin_task_screenshot_not_found',
|
||
})
|
||
}
|