- 前后端接入 ESLint 10 扁平配置(typescript-eslint 类型感知检查 + react-hooks/react-refresh),新增 lint / lint:check / lint:fix 脚本并纳入 check 链路 - 清理全部 no-unused-vars 与 no-useless-assignment 警告,修复 floating promises、正则转义等问题 - 删除确认无引用的死代码(markKuaishouCloudBindUrlRefreshFailed、未使用 helper 等)
490 lines
18 KiB
TypeScript
490 lines
18 KiB
TypeScript
import { buildClaimUrl } from '../claim/claim-service.js'
|
|
import { buildClaimIdentityAdminSummary } from '../claim/claim-identity.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 type { JsonRecord } from '../../types/json.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 { AdminTaskDetailView } from '../../types/admin/task-detail.js'
|
|
import type {
|
|
AdminEntityIdInput,
|
|
AdminOrderListQueryInput,
|
|
AdminTaskListQueryInput,
|
|
AdminViewerSessionInput,
|
|
} from '../../types/admin/read-inputs.js'
|
|
import type { KuaishouIndustryVoucherRow, 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,
|
|
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<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)
|
|
// 统一对外领取入口为本站 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 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,
|
|
}
|
|
: 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),
|
|
claimIdentity,
|
|
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.canOperateAssistedTask &&
|
|
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',
|
|
})
|
|
}
|