284 lines
11 KiB
TypeScript
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',
|
|
})
|
|
}
|