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

642 lines
24 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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',
})
}