430 lines
18 KiB
JavaScript
430 lines
18 KiB
JavaScript
// @ts-check
|
|
|
|
import { buildClaimUrl } from '../claim/claim-service.js'
|
|
import { getClaimTokenById } from '../../repositories/claim-token-repo.js'
|
|
import {
|
|
getInventoryItemById,
|
|
listInventoryItems,
|
|
listInventorySkuSuggestions,
|
|
} from '../../repositories/inventory-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 { listTaskInventoryBindingsByTaskId } from '../../repositories/task-inventory-binding-repo.js'
|
|
import { listTaskEventsByTaskId } from '../../repositories/task-event-repo.js'
|
|
import { getWebhookEventById, listWebhookEvents, listWebhookEventsByOrderId } from '../../repositories/webhook-event-repo.js'
|
|
import { createHttpError } from '../../utils/http.js'
|
|
import { formatFenToAmount, normalizeFen } from '../../utils/money.js'
|
|
import { getTencentBrowserSessionReviewScreenshotPath } from '../session/session.js'
|
|
import { normalizeDateQuery, normalizePage, normalizePageSize, safeParseJson } from './admin-query-utils.js'
|
|
import {
|
|
canViewerCloseTask,
|
|
canRegenerateClaimLinkForViewer,
|
|
canViewerConfirmAssistedRole,
|
|
canViewerRedeemAssistedTask,
|
|
createAdminViewerContext,
|
|
getTaskPrimaryClaimTokenId,
|
|
getTaskPrimaryInventoryItemId,
|
|
isAssistedClaimTask,
|
|
isManualDispatchTask,
|
|
mapKuaishouCloudFulfillmentContext,
|
|
mapManualDispatchContext,
|
|
mapRedeemResolutionContext,
|
|
parseTaskContext,
|
|
parseTaskState,
|
|
resolveAdminTaskScreenshotUrl,
|
|
resolveDisplayShopName,
|
|
resolveOrderItemDeliveryMode,
|
|
resolveOrderItemTitle,
|
|
} from './admin-read-shared-helpers.js'
|
|
import { mapAdminInventoryListItem } from './admin-inventory-read-helpers.js'
|
|
import { mapAdminOrderListItem, summarizeOrderItems } from './admin-order-read-helpers.js'
|
|
import { mapAdminWebhookEvent } from './admin-webhook-read-helpers.js'
|
|
import {
|
|
buildOrderAgisoAutoDeliverySummary,
|
|
buildOrderBindingSummary,
|
|
createTaskBindingSummaryFromBindings,
|
|
getRequiredTask,
|
|
getTaskBindingSummary,
|
|
getTaskBindingSummaryMap,
|
|
mapAdminTaskEvent,
|
|
mapAdminTaskInventoryBinding,
|
|
mapAdminTaskListItem,
|
|
mapAdminTaskSummary,
|
|
} from './admin-task-read-helpers.js'
|
|
|
|
/** @typedef {import('../../types/admin-read-models.js').AdminInventoryListResponse} AdminInventoryListResponse */
|
|
/** @typedef {import('../../types/admin-read-models.js').AdminInventorySkuSuggestionResponse} AdminInventorySkuSuggestionResponse */
|
|
/** @typedef {import('../../types/admin-read-models.js').AdminOrderListResponse} AdminOrderListResponse */
|
|
/** @typedef {import('../../types/admin-read-models.js').AdminTaskListResponse} AdminTaskListResponse */
|
|
/** @typedef {import('../../types/admin-read-models.js').AdminWebhookEventListResponse} AdminWebhookEventListResponse */
|
|
/** @typedef {import('../../types/admin-read-inputs.js').AdminEntityIdInput} AdminEntityIdInput */
|
|
/** @typedef {import('../../types/admin-read-inputs.js').AdminInventoryListQueryInput} AdminInventoryListQueryInput */
|
|
/** @typedef {import('../../types/admin-read-inputs.js').AdminInventorySkuSuggestionQueryInput} AdminInventorySkuSuggestionQueryInput */
|
|
/** @typedef {import('../../types/admin-read-inputs.js').AdminOrderListQueryInput} AdminOrderListQueryInput */
|
|
/** @typedef {import('../../types/admin-read-inputs.js').AdminTaskListQueryInput} AdminTaskListQueryInput */
|
|
/** @typedef {import('../../types/admin-read-inputs.js').AdminViewerSessionInput} AdminViewerSessionInput */
|
|
/** @typedef {import('../../types/admin-read-inputs.js').AdminWebhookEventListQueryInput} AdminWebhookEventListQueryInput */
|
|
|
|
/** @returns {Promise<AdminOrderListResponse>} */
|
|
/** @param {AdminOrderListQueryInput} [query] */
|
|
export async function getAdminOrders(query = /** @type {AdminOrderListQueryInput} */ ({})) {
|
|
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 },
|
|
}
|
|
}
|
|
|
|
/** @param {AdminEntityIdInput} orderId */
|
|
export async function getAdminOrderDetail(orderId) {
|
|
const order = await getOrderById(Number(orderId))
|
|
|
|
if (!order) {
|
|
throw createHttpError('订单不存在', {
|
|
statusCode: 404,
|
|
errorCode: 'admin_order_not_found',
|
|
})
|
|
}
|
|
|
|
const [items, tasks, webhookEvents] = await Promise.all([
|
|
listOrderItemsByOrderId(order.id),
|
|
listTasksByOrderId(order.id),
|
|
listWebhookEventsByOrderId(order.id),
|
|
])
|
|
const taskBindingSummaryMap = await getTaskBindingSummaryMap(tasks.map((task) => task.id))
|
|
const itemSummary = summarizeOrderItems(items)
|
|
|
|
return {
|
|
order: {
|
|
orderId: order.id,
|
|
provider: order.provider || 'agiso',
|
|
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, taskBindingSummaryMap),
|
|
agisoAutoDelivery: buildOrderAgisoAutoDeliverySummary(order, 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, getTaskBindingSummary(taskBindingSummaryMap, task.id))),
|
|
webhookEvents: webhookEvents.map((event) => ({
|
|
eventId: event.id,
|
|
provider: event.provider || 'agiso',
|
|
platform: event.platform,
|
|
shopId: event.shop_id || '',
|
|
shopName: resolveDisplayShopName(event.provider, event.shop_id, event.shop_name),
|
|
eventType: event.event_type,
|
|
eventKey: event.event_key,
|
|
signatureValid: Boolean(event.signature_valid),
|
|
processed: Boolean(event.processed),
|
|
processError: event.process_error,
|
|
createdAt: event.created_at,
|
|
})),
|
|
}
|
|
}
|
|
|
|
/** @returns {Promise<AdminTaskListResponse>} */
|
|
/** @param {AdminTaskListQueryInput} [query] */
|
|
/** @param {AdminViewerSessionInput | null} [session] */
|
|
export async function getAdminTasks(
|
|
query = /** @type {AdminTaskListQueryInput} */ ({}),
|
|
session = null,
|
|
) {
|
|
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 taskBindingSummaryMap = await getTaskBindingSummaryMap(items.map((item) => item.id))
|
|
const viewerContext = createAdminViewerContext(session)
|
|
|
|
return {
|
|
items: items.map((item) => mapAdminTaskListItem(
|
|
item,
|
|
getTaskBindingSummary(taskBindingSummaryMap, item.id),
|
|
viewerContext,
|
|
)),
|
|
pagination: { page, pageSize, total },
|
|
}
|
|
}
|
|
|
|
/** @param {AdminEntityIdInput} taskId */
|
|
/** @param {AdminViewerSessionInput | null} [session] */
|
|
export async function getAdminTaskDetail(taskId, session = null) {
|
|
const task = await getTaskById(Number(taskId))
|
|
|
|
if (!task) {
|
|
throw createHttpError('任务不存在', {
|
|
statusCode: 404,
|
|
errorCode: 'admin_task_not_found',
|
|
})
|
|
}
|
|
|
|
const [order, claimToken, inventoryItem, taskEvents, inventoryBindings] = await Promise.all([
|
|
getOrderById(task.order_id),
|
|
getTaskPrimaryClaimTokenId(task) ? getClaimTokenById(getTaskPrimaryClaimTokenId(task)) : Promise.resolve(null),
|
|
getTaskPrimaryInventoryItemId(task) ? getInventoryItemById(getTaskPrimaryInventoryItemId(task)) : Promise.resolve(null),
|
|
listTaskEventsByTaskId(task.id),
|
|
listTaskInventoryBindingsByTaskId(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 taskBindingSummary = createTaskBindingSummaryFromBindings(inventoryBindings)
|
|
const viewerContext = createAdminViewerContext(session)
|
|
const mappedInventoryBindings = inventoryBindings.map((binding) => mapAdminTaskInventoryBinding(binding, task, viewerContext))
|
|
const claimUrl = claimToken ? buildClaimUrl(claimToken.token) : ''
|
|
const screenshotUrl = await resolveAdminTaskScreenshotUrl(task, viewerContext)
|
|
const kuaishouCloudFulfillment = mapKuaishouCloudFulfillmentContext(taskContext.kuaishouCloudFulfillment)
|
|
|
|
return {
|
|
task: mapAdminTaskListItem({
|
|
...task,
|
|
sku_code: orderItem?.sku_code || '',
|
|
sku_name: orderItem?.sku_name || '',
|
|
inventory_display_value: inventoryItem?.display_value || '',
|
|
claim_token: claimToken?.token || '',
|
|
}, taskBindingSummary, viewerContext),
|
|
order: order
|
|
? {
|
|
orderId: order.id,
|
|
provider: order.provider || 'agiso',
|
|
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,
|
|
inventory: inventoryItem
|
|
? {
|
|
inventoryItemId: inventoryItem.id,
|
|
skuCode: inventoryItem.sku_code,
|
|
batchNo: inventoryItem.batch_no,
|
|
credentialType: inventoryItem.credential_type || 'tencent_code',
|
|
displayValue: viewerContext.canViewSensitiveTaskData ? inventoryItem.display_value : '',
|
|
status: inventoryItem.status,
|
|
}
|
|
: null,
|
|
inventoryBindings: mappedInventoryBindings,
|
|
artifacts: viewerContext.canViewSensitiveTaskData ? safeParseJson(task.artifacts_json) : {},
|
|
screenshotUrl,
|
|
review: {
|
|
required: isAssistedClaimTask(task),
|
|
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', 'waiting_inventory'].includes(task.task_status),
|
|
canReleaseInventory: mappedInventoryBindings.some((binding) => binding.canRelease),
|
|
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()),
|
|
canSupportConfirmRole: canViewerConfirmAssistedRole(task, viewerContext),
|
|
canSupportRedeem: canViewerRedeemAssistedTask(task, viewerContext),
|
|
canViewSensitiveTaskData: viewerContext.canViewSensitiveTaskData,
|
|
},
|
|
}
|
|
}
|
|
|
|
/** @param {AdminEntityIdInput} taskId */
|
|
/** @param {AdminViewerSessionInput | null} [session] */
|
|
export async function getAdminTaskScreenshotPath(taskId, session = null) {
|
|
const task = await getRequiredTask(taskId)
|
|
|
|
if (task.screenshot_path) {
|
|
return task.screenshot_path
|
|
}
|
|
|
|
if (task.browser_session_id) {
|
|
return getTencentBrowserSessionReviewScreenshotPath(task.browser_session_id)
|
|
}
|
|
|
|
throw createHttpError('当前任务还没有可查看截图', {
|
|
statusCode: 404,
|
|
errorCode: 'admin_task_screenshot_not_found',
|
|
})
|
|
}
|
|
|
|
/** @returns {Promise<AdminInventoryListResponse>} */
|
|
/** @param {AdminInventoryListQueryInput} [query] */
|
|
/** @param {AdminViewerSessionInput | null} [session] */
|
|
export async function getAdminInventoryItems(query = /** @type {AdminInventoryListQueryInput} */ ({}), session = null) {
|
|
const page = normalizePage(query.page)
|
|
const pageSize = normalizePageSize(query.pageSize)
|
|
const viewerContext = createAdminViewerContext(session)
|
|
const { items, total } = await listInventoryItems({
|
|
page,
|
|
pageSize,
|
|
skuCode: String(query.skuCode || '').trim(),
|
|
credentialType: String(query.credentialType || '').trim(),
|
|
status: String(query.status || '').trim(),
|
|
batchNo: String(query.batchNo || '').trim(),
|
|
inventoryGroupCode: String(query.inventoryGroupCode || '').trim(),
|
|
allowedInventoryGroupCodes: viewerContext.allowedInventoryGroupCodes,
|
|
})
|
|
|
|
return {
|
|
items: await Promise.all(items.map((item) => mapAdminInventoryListItem(item))),
|
|
pagination: { page, pageSize, total },
|
|
}
|
|
}
|
|
|
|
/** @returns {Promise<AdminInventorySkuSuggestionResponse>} */
|
|
/** @param {AdminInventorySkuSuggestionQueryInput} [query] */
|
|
export async function getAdminInventorySkuSuggestions(
|
|
query = /** @type {AdminInventorySkuSuggestionQueryInput} */ ({}),
|
|
session = null,
|
|
) {
|
|
const viewerContext = createAdminViewerContext(session)
|
|
const credentialType = String(query.credentialType || '').trim()
|
|
const keyword = String(query.keyword || '').trim()
|
|
const inventoryGroupCode = String(query.inventoryGroupCode || '').trim()
|
|
const requestedLimit = Number(query.limit)
|
|
const limit = Number.isFinite(requestedLimit) && requestedLimit > 0
|
|
? Math.min(100, Math.floor(requestedLimit))
|
|
: 50
|
|
const items = await listInventorySkuSuggestions({
|
|
credentialType,
|
|
keyword,
|
|
limit,
|
|
inventoryGroupCode,
|
|
allowedInventoryGroupCodes: viewerContext.allowedInventoryGroupCodes,
|
|
})
|
|
|
|
return {
|
|
items: items.map((item) => ({
|
|
skuCode: String(item.sku_code || '').trim(),
|
|
credentialType: String(item.credential_type || '').trim() || 'tencent_code',
|
|
inventoryGroupCode: String(item.inventory_group_code || '').trim(),
|
|
totalCount: Number(item.total_count || 0),
|
|
availableCount: Number(item.available_count || 0),
|
|
latestUpdatedAt: item.latest_updated_at || null,
|
|
})),
|
|
}
|
|
}
|
|
|
|
/** @returns {Promise<AdminWebhookEventListResponse>} */
|
|
/** @param {AdminWebhookEventListQueryInput} [query] */
|
|
export async function getAdminWebhookEvents(query = /** @type {AdminWebhookEventListQueryInput} */ ({})) {
|
|
const page = normalizePage(query.page)
|
|
const pageSize = normalizePageSize(query.pageSize)
|
|
const { items, total } = await listWebhookEvents({
|
|
page,
|
|
pageSize,
|
|
provider: String(query.provider || '').trim(),
|
|
platform: String(query.platform || '').trim(),
|
|
platformOrderId: String(query.platformOrderId || '').trim(),
|
|
processed: String(query.processed || '').trim(),
|
|
visibility: String(query.visibility || '').trim() || 'important',
|
|
relatedOrderId: String(query.relatedOrderId || '').trim(),
|
|
dateFrom: normalizeDateQuery(query.dateFrom),
|
|
dateTo: normalizeDateQuery(query.dateTo, true),
|
|
})
|
|
|
|
return {
|
|
items: await Promise.all(items.map((item) => mapAdminWebhookEvent(item))),
|
|
pagination: { page, pageSize, total },
|
|
}
|
|
}
|
|
|
|
/** @param {AdminEntityIdInput} eventId */
|
|
export async function getAdminWebhookEventDetail(eventId) {
|
|
const event = await getWebhookEventById(Number(eventId))
|
|
|
|
if (!event) {
|
|
throw createHttpError('Webhook 事件不存在', {
|
|
statusCode: 404,
|
|
errorCode: 'admin_webhook_not_found',
|
|
})
|
|
}
|
|
|
|
return mapAdminWebhookEvent(event, { includeRaw: true })
|
|
}
|