Files
order_site/apps/backend/src/services/admin/admin-read-service.ts
T

284 lines
11 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 { 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 {
canViewerCloseTask,
canRegenerateClaimLinkForViewer,
createAdminViewerContext,
getTaskPrimaryClaimTokenId,
isManualDispatchTask,
mapKuaishouCloudFulfillmentContext,
mapManualDispatchContext,
mapRedeemResolutionContext,
parseTaskContext,
parseTaskState,
resolveAdminTaskScreenshotUrl,
resolveDisplayShopName,
resolveOrderItemDeliveryMode,
resolveOrderItemTitle,
} from './admin-read-shared-helpers.js'
import { mapAdminOrderListItem, summarizeOrderItems } from './admin-order-read-helpers.js'
import {
buildOrderBindingSummary,
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'
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): 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 itemSummary = summarizeOrderItems(items)
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 || '{}')),
bindingSummary: buildOrderBindingSummary(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)),
}
}
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] = await Promise.all([
getOrderById(task.order_id),
primaryClaimTokenId ? getClaimTokenById(primaryClaimTokenId) : Promise.resolve(null),
listTaskEventsByTaskId(task.id),
])
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)
const claimUrl = claimToken ? buildClaimUrl(claimToken.token) : ''
const screenshotUrl = await resolveAdminTaskScreenshotUrl(task, viewerContext)
const kuaishouCloudFulfillment = mapKuaishouCloudFulfillmentContext(
taskContext.kuaishouCloudFulfillment,
{ cloudSourceLabelMap: buildCloudSourceLabelMap() },
)
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,
manualDispatch: mapManualDispatchContext(taskContext.manualDispatch, viewerContext),
events: taskEvents.map(mapAdminTaskEvent),
operations: {
canRetry: viewerContext.canManageTaskLifecycle
&& !isManualDispatchTask(task)
&& ['retry_pending', 'manual_review'].includes(task.task_status),
canRegenerateClaimLink: canRegenerateClaimLinkForViewer(task, viewerContext),
canClose: canViewerCloseTask(task, viewerContext),
canMarkManualReview: viewerContext.canManageTaskLifecycle && !['redeemed', 'closed', 'manual_review'].includes(task.task_status),
canCompleteManualDispatch: viewerContext.canManageTaskLifecycle && isManualDispatchTask(task) && !['redeemed', 'closed'].includes(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'
&& ['pending_binding_prepare', 'manual_review', 'failed'].includes(String(task.task_status || '').trim()),
canRefreshKuaishouCloudRoleInfo: viewerContext.canOperateAssistedTask
&& String(task.executor_key || '').trim() === 'kuaishou_ct_assisted'
&& Number(kuaishouCloudFulfillment?.binding.vnId || 0) > 0,
canDispatchKuaishouCloudFulfillment: viewerContext.canManageTaskLifecycle
&& String(task.executor_key || '').trim() === 'kuaishou_ct_assisted'
&& ['waiting_binding'].includes(String(task.task_status || '').trim()),
canReturnKuaishouCloudFulfillment: viewerContext.canManageTaskLifecycle
&& String(task.executor_key || '').trim() === 'kuaishou_ct_assisted'
&& ['dispatched_pending_return'].includes(String(task.task_status || '').trim()),
canViewSensitiveTaskData: viewerContext.canViewSensitiveTaskData,
},
}
}
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',
})
}