Files
order_site/apps/backend/src/services/admin/admin-read-service.ts
T
yml2213 bb060e81a4 统一领取流:Step1 填 UID,按平台分叉履约
领取页改为统一 UID 入口;lewan 强制角色 ID 匹配后才发货核销,feifei 将 uid 拼入 H5 并去掉 claim 短链,对外仍返回本站 claimUrl。
2026-07-10 13:30:22 +08:00

482 lines
17 KiB
TypeScript

import { buildClaimUrl } from '../claim/claim-service.js'
import { getClaimTokenById } from '../../repositories/claim-token-repo.js'
import { listOrderItemsByOrderId } from '../../repositories/order-item-repo.js'
import { getOrderById, listOrders } from '../../repositories/order-repo.js'
import { getTaskById, listTasks, listTasksByOrderId } from '../../repositories/task-repo.js'
import { listTaskEventsByTaskId } from '../../repositories/task-event-repo.js'
import {
listKuaishouIndustryVouchersByOid,
listKuaishouIndustryVouchersByTaskId,
} from '../../repositories/kuaishou-industry-voucher-repo.js'
import { listCloudtentaclesSources } from '../platforms/cloudtentacles/source-config-service.js'
import { createHttpError } from '../../utils/http.js'
import { formatFenToAmount, normalizeFen } from '../../utils/money.js'
import {
normalizeDateQuery,
normalizePage,
normalizePageSize,
safeParseJson,
} from './admin-query-utils.js'
import {
canCompleteManualDispatchTaskStatus,
canDispatchKuaishouCloudFulfillmentStatus,
canMarkManualReviewTaskStatus,
canPrepareKuaishouCloudFulfillmentStatus,
canRebindKuaishouCloudRoleStatus,
canReturnKuaishouCloudFulfillmentStatus,
canRetryTaskStatus,
} from '../../domain/task-status.js'
import {
canViewerCloseTask,
canRegenerateClaimLinkForViewer,
createAdminViewerContext,
getTaskPrimaryClaimTokenId,
isManualDispatchTask,
mapKuaishouCloudFulfillmentContext,
mapKuaishouCloudTaskStateProjection,
mapManualDispatchContext,
mapRedeemResolutionContext,
parseTaskContext,
parseTaskState,
resolveAdminTaskScreenshotUrl,
resolveDisplayShopName,
resolveOrderItemDeliveryMode,
resolveOrderItemTitle,
} from './admin-read-shared-helpers.js'
import { mapAdminOrderListItem, summarizeOrderItems } from './admin-order-read-helpers.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 {
AdminEntityIdInput,
AdminOrderListQueryInput,
AdminTaskListQueryInput,
AdminViewerSessionInput,
} from '../../types/admin/read-inputs.js'
import type {
KuaishouIndustryVoucherRow,
TaskRow,
} from '../../types/repository/rows.js'
type JsonRecord = Record<string, any>
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,
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: await Promise.all(items.map((item) => mapAdminOrderListItem(item))),
pagination: { page, pageSize, total },
}
}
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] = await Promise.all([
listOrderItemsByOrderId(order.id),
listTasksByOrderId(order.id),
])
const kuaishouIndustryVouchers = await listKuaishouIndustryVouchersByOid(order.platform_order_id)
const itemSummary = summarizeOrderItems(items)
const viewerContext = createAdminViewerContext(session)
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,
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),
},
items: items.map((item) => ({
orderItemId: item.id,
skuCode: item.sku_code,
skuName: item.sku_name,
itemTitle: resolveOrderItemTitle(item),
quantity: item.quantity,
deliveryMode: resolveOrderItemDeliveryMode(tasks, item.id),
spec:
item.spec_json && typeof item.spec_json === 'object'
? item.spec_json
: JSON.parse(String(item.spec_json || '{}')),
})),
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,
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<JsonRecord> {
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)
// 统一对外领取入口为本站 claim 链接;平台 H5 仅作内部上下文
const claimUrl = claimToken ? buildClaimUrl(claimToken.token) : ''
const screenshotUrl = await resolveAdminTaskScreenshotUrl(task, viewerContext)
const cloudSourceLabelMap = buildCloudSourceLabelMap()
const kuaishouCloudFulfillment =
mapKuaishouCloudFulfillmentContext(taskContext.kuaishouCloudFulfillment, {
cloudSourceLabelMap,
}) || mapKuaishouCloudTaskStateProjection(task, { cloudSourceLabelMap })
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,
}
: null,
orderItem: orderItem
? {
orderItemId: orderItem.id,
skuCode: orderItem.sku_code,
skuName: orderItem.sku_name,
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),
kuaishouCloudFulfillment,
kuaishouIndustryVoucher,
manualDispatch: mapManualDispatchContext(taskContext.manualDispatch, viewerContext),
events: taskEvents.map(mapAdminTaskEvent),
operations: {
canRetry:
viewerContext.canManageTaskLifecycle &&
!isManualDispatchTask(task) &&
canRetryTaskStatus(task.task_status),
canRegenerateClaimLink: canRegenerateClaimLinkForViewer(task, viewerContext),
canClose: canViewerCloseTask(task, viewerContext),
canMarkManualReview:
viewerContext.canManageTaskLifecycle && canMarkManualReviewTaskStatus(task.task_status),
canCompleteManualDispatch:
viewerContext.canManageTaskLifecycle &&
isManualDispatchTask(task) &&
canCompleteManualDispatchTaskStatus(task.task_status),
canPrepareKuaishouCloudFulfillment:
viewerContext.canManageTaskLifecycle &&
String(task.executor_key || '').trim() === 'kuaishou_ct_assisted' &&
String(kuaishouCloudFulfillment?.dispatch.status || 'pending').trim() === 'pending' &&
String(kuaishouCloudFulfillment?.returnNumber.status || 'pending').trim() === 'pending' &&
canPrepareKuaishouCloudFulfillmentStatus(task.task_status),
canRefreshKuaishouCloudRoleInfo:
viewerContext.canOperateAssistedTask &&
String(task.executor_key || '').trim() === 'kuaishou_ct_assisted' &&
Number(kuaishouCloudFulfillment?.binding.vnId || 0) > 0,
canRebindKuaishouCloudRole:
viewerContext.canManageTaskLifecycle &&
String(task.executor_key || '').trim() === 'kuaishou_ct_assisted' &&
canRebindKuaishouCloudRoleStatus(task.task_status) &&
String(kuaishouCloudFulfillment?.dispatch.status || 'pending').trim() !== 'success' &&
Number(kuaishouCloudFulfillment?.binding.vnId || 0) > 0,
canDispatchKuaishouCloudFulfillment:
viewerContext.canManageTaskLifecycle &&
String(task.executor_key || '').trim() === 'kuaishou_ct_assisted' &&
canDispatchKuaishouCloudFulfillmentStatus(task.task_status),
canReturnKuaishouCloudFulfillment:
viewerContext.canManageTaskLifecycle &&
String(task.executor_key || '').trim() === 'kuaishou_ct_assisted' &&
canReturnKuaishouCloudFulfillmentStatus(task.task_status),
canResendKuaishouIndustryVoucherCode:
viewerContext.canOperateKuaishouIndustryVoucher && hasKuaishouIndustryVoucher,
canConsumeKuaishouIndustryVoucher:
viewerContext.canOperateKuaishouIndustryVoucher &&
hasKuaishouIndustryVoucher &&
kuaishouIndustryVoucherStatus !== 'CONSUMED' &&
kuaishouIndustryVoucherSendCallbackStatus === 'success',
canViewSensitiveTaskData: viewerContext.canViewSensitiveTaskData,
},
}
}
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() {
const labelMap = new Map<string, string>()
for (const source of listCloudtentaclesSources().sources) {
const key = String(source.key || '').trim()
if (!key) continue
const label = String(source.label || source.username || key).trim() || key
labelMap.set(key, label)
}
return labelMap
}
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',
})
}