lewan 发货强制 expectedUid 并取消无 UID 旧路径回落;任务详情展示 UID 匹配态;领取页匹配后可一键兑换;补充 identity 单测并收口短链说明。
530 lines
17 KiB
TypeScript
530 lines
17 KiB
TypeScript
import { safeParseJson } from './admin-query-utils.js'
|
|
import { normalizeAdminRole } from './admin-auth-service.js'
|
|
import {
|
|
parseTaskContext as parseTaskContextValue,
|
|
parseTaskState as parseTaskStateValue,
|
|
} from '../../utils/task-json.js'
|
|
import { canRegenerateClaimLinkStatus, isTaskFinalStatus } from '../../domain/task-status.js'
|
|
|
|
import type { AdminViewerSessionInput } from '../../types/admin/read-inputs.js'
|
|
import type { OrderItemRow, TaskRow } from '../../types/repository/rows.js'
|
|
|
|
type JsonRecord = Record<string, any>
|
|
type CloudSourceLabelMap = Map<string, string> | Record<string, string>
|
|
|
|
type KuaishouCloudFulfillmentMapOptions = {
|
|
cloudSourceLabelMap?: CloudSourceLabelMap
|
|
}
|
|
|
|
type TaskLike = Partial<TaskRow> & {
|
|
state_json?: string | JsonRecord
|
|
}
|
|
|
|
export type AdminViewerContext = {
|
|
role: string
|
|
canViewSensitiveTaskData: boolean
|
|
canManageTaskLifecycle: boolean
|
|
canOperateAssistedTask: boolean
|
|
canOperateKuaishouIndustryVoucher: boolean
|
|
}
|
|
|
|
type RedeemResolutionAttempt = {
|
|
attempt: number
|
|
codeMasked: string
|
|
credentialType: string
|
|
outcome: string
|
|
resultCode: string
|
|
resultMessage: string
|
|
}
|
|
|
|
export function mapManualDispatchContext(
|
|
value: unknown,
|
|
viewerContext: AdminViewerContext = createAdminViewerContext(),
|
|
): JsonRecord | null {
|
|
if (!value || typeof value !== 'object') {
|
|
return null
|
|
}
|
|
|
|
const record = value as JsonRecord
|
|
return {
|
|
outcome: String(record.outcome || '').trim(),
|
|
deliveryReference: String(record.deliveryReference || '').trim(),
|
|
deliveredCredential: viewerContext.canViewSensitiveTaskData
|
|
? String(record.deliveredCredential || '').trim()
|
|
: '',
|
|
resultMessage: String(record.resultMessage || '').trim(),
|
|
completedAt: record.completedAt || null,
|
|
completedBy:
|
|
record.completedBy && typeof record.completedBy === 'object'
|
|
? {
|
|
userId: Number(record.completedBy.userId || 0) || 0,
|
|
username: String(record.completedBy.username || '').trim(),
|
|
role: String(record.completedBy.role || '').trim(),
|
|
}
|
|
: null,
|
|
}
|
|
}
|
|
|
|
export function mapKuaishouCloudFulfillmentContext(
|
|
value: unknown,
|
|
options: KuaishouCloudFulfillmentMapOptions = {},
|
|
): JsonRecord | null {
|
|
if (!value || typeof value !== 'object') {
|
|
return null
|
|
}
|
|
|
|
const record = value as JsonRecord
|
|
const ticket = record.ticket && typeof record.ticket === 'object' ? record.ticket : {}
|
|
const binding = record.binding && typeof record.binding === 'object' ? record.binding : {}
|
|
const role = record.role && typeof record.role === 'object' ? record.role : {}
|
|
const purchase = record.purchase && typeof record.purchase === 'object' ? record.purchase : {}
|
|
const dispatch = record.dispatch && typeof record.dispatch === 'object' ? record.dispatch : {}
|
|
const returnNumber =
|
|
record.returnNumber && typeof record.returnNumber === 'object' ? record.returnNumber : {}
|
|
const consume = record.consume && typeof record.consume === 'object' ? record.consume : {}
|
|
const cloudSourceKeys = Array.isArray(binding.cloudSourceKeys)
|
|
? binding.cloudSourceKeys.map((value: unknown) => String(value || '').trim()).filter(Boolean)
|
|
: []
|
|
const resolvedSourceKey = String(binding.resolvedSourceKey || '').trim()
|
|
|
|
return {
|
|
flowType: String(record.flowType || '').trim(),
|
|
configId: String(record.configId || '').trim(),
|
|
internalSkuCode: String(record.internalSkuCode || '').trim(),
|
|
internalSkuName: String(record.internalSkuName || '').trim(),
|
|
ticket: {
|
|
code: String(ticket.code || '').trim(),
|
|
capturedAt: ticket.capturedAt || null,
|
|
status: String(ticket.status || 'pending').trim() || 'pending',
|
|
verifiedAt: ticket.verifiedAt || null,
|
|
goodsTitle: String(ticket.goodsTitle || '').trim(),
|
|
leftCount: Number(ticket.leftCount || 0) || 0,
|
|
},
|
|
binding: {
|
|
prepareStatus: String(binding.prepareStatus || 'pending').trim() || 'pending',
|
|
cloudSourceKeys,
|
|
cloudSourceLabels: cloudSourceKeys.map((sourceKey: string) =>
|
|
resolveCloudSourceLabel(sourceKey, options.cloudSourceLabelMap),
|
|
),
|
|
resolvedSourceKey,
|
|
resolvedSourceLabel: resolvedSourceKey
|
|
? resolveCloudSourceLabel(resolvedSourceKey, options.cloudSourceLabelMap)
|
|
: '',
|
|
skuId: Number(binding.skuId || 0) || 0,
|
|
skuName: String(binding.skuName || '').trim(),
|
|
vnKey: String(binding.vnKey || '').trim(),
|
|
vnId: Number(binding.vnId || 0) || 0,
|
|
vnPhone: String(binding.vnPhone || '').trim(),
|
|
bindUrl: String(binding.bindUrl || '').trim(),
|
|
bindPreparedAt: binding.bindPreparedAt || null,
|
|
bindExpiresAt: binding.bindExpiresAt || null,
|
|
bindProbeAt: binding.bindProbeAt || null,
|
|
bindProbeStatus: String(binding.bindProbeStatus || '').trim(),
|
|
bindProbeMessage: String(binding.bindProbeMessage || '').trim(),
|
|
roleName: String(binding.roleName || role.name || '').trim(),
|
|
roleId: String(binding.roleId || role.rid || '').trim(),
|
|
},
|
|
role: {
|
|
status: String(role.status || 'pending').trim() || 'pending',
|
|
name: String(role.name || binding.roleName || '').trim(),
|
|
rid: String(role.rid || binding.roleId || '').trim(),
|
|
refreshedAt: role.refreshedAt || null,
|
|
errorMessage: String(role.errorMessage || '').trim(),
|
|
rawInfo: role.rawInfo && typeof role.rawInfo === 'object' ? role.rawInfo : null,
|
|
},
|
|
purchase: {
|
|
autoBuyEnabled: purchase.autoBuyEnabled !== false,
|
|
minAssetReserve: Number(purchase.minAssetReserve || 0) || 0,
|
|
usedKnapsack: purchase.usedKnapsack === true,
|
|
purchaseTriggered: purchase.purchaseTriggered === true,
|
|
assetBefore: Number(purchase.assetBefore || 0) || 0,
|
|
assetAfter: Number(purchase.assetAfter || 0) || 0,
|
|
purchaseAt: purchase.purchaseAt || null,
|
|
},
|
|
dispatch: {
|
|
status: String(dispatch.status || 'pending').trim() || 'pending',
|
|
dispatchAt: dispatch.dispatchAt || null,
|
|
sendType: Number(dispatch.sendType || 0) || 0,
|
|
note: String(dispatch.note || '').trim(),
|
|
},
|
|
returnNumber: {
|
|
status: String(returnNumber.status || 'pending').trim() || 'pending',
|
|
returnedAt: returnNumber.returnedAt || null,
|
|
autoReturnEnabled: returnNumber.autoReturnEnabled === true,
|
|
},
|
|
consume: {
|
|
status: String(consume.status || 'pending').trim() || 'pending',
|
|
shopId: String(consume.shopId || '').trim(),
|
|
shopName: String(consume.shopName || '').trim(),
|
|
autoConsumeEnabled: consume.autoConsumeEnabled === true,
|
|
consumedAt: consume.consumedAt || null,
|
|
errorMessage: String(consume.errorMessage || '').trim(),
|
|
},
|
|
rebind: mapKuaishouCloudRebindContext(record.rebind),
|
|
notes: String(record.notes || '').trim(),
|
|
}
|
|
}
|
|
|
|
export function mapKuaishouCloudTaskStateProjection(
|
|
task: TaskLike | null | undefined,
|
|
options: KuaishouCloudFulfillmentMapOptions = {},
|
|
): JsonRecord | null {
|
|
if (!task || String(task.executor_key || '').trim() !== 'kuaishou_ct_assisted') {
|
|
return null
|
|
}
|
|
|
|
const hasProjection = [
|
|
task.kuaishou_ticket_status,
|
|
task.kuaishou_ticket_code_masked,
|
|
task.kuaishou_bind_url,
|
|
task.kuaishou_vn_phone,
|
|
task.kuaishou_role_name,
|
|
task.kuaishou_role_id,
|
|
task.kuaishou_dispatch_status,
|
|
task.kuaishou_consume_status,
|
|
task.kuaishou_source_key,
|
|
].some((value) => String(value || '').trim())
|
|
|
|
if (!hasProjection) {
|
|
return null
|
|
}
|
|
|
|
const sourceKey = String(task.kuaishou_source_key || '').trim()
|
|
const roleName = String(task.kuaishou_role_name || '').trim()
|
|
const roleId = String(task.kuaishou_role_id || '').trim()
|
|
|
|
return {
|
|
flowType: 'kuaishou_cloud_fulfillment',
|
|
configId: '',
|
|
internalSkuCode: '',
|
|
internalSkuName: '',
|
|
ticket: {
|
|
code: String(task.kuaishou_ticket_code_masked || '').trim(),
|
|
capturedAt: null,
|
|
status: String(task.kuaishou_ticket_status || 'pending').trim() || 'pending',
|
|
verifiedAt: null,
|
|
goodsTitle: '',
|
|
leftCount: 0,
|
|
},
|
|
binding: {
|
|
prepareStatus: task.kuaishou_bind_url ? 'ready' : 'pending',
|
|
cloudSourceKeys: sourceKey ? [sourceKey] : [],
|
|
cloudSourceLabels: sourceKey
|
|
? [resolveCloudSourceLabel(sourceKey, options.cloudSourceLabelMap)]
|
|
: [],
|
|
resolvedSourceKey: sourceKey,
|
|
resolvedSourceLabel: sourceKey
|
|
? resolveCloudSourceLabel(sourceKey, options.cloudSourceLabelMap)
|
|
: '',
|
|
skuId: 0,
|
|
skuName: '',
|
|
vnKey: '',
|
|
vnId: 0,
|
|
vnPhone: String(task.kuaishou_vn_phone || '').trim(),
|
|
bindUrl: String(task.kuaishou_bind_url || '').trim(),
|
|
bindPreparedAt: null,
|
|
bindExpiresAt: null,
|
|
bindProbeAt: null,
|
|
bindProbeStatus: '',
|
|
bindProbeMessage: '',
|
|
},
|
|
role: {
|
|
status: roleName || roleId ? 'ready' : 'pending',
|
|
name: roleName,
|
|
rid: roleId,
|
|
refreshedAt: null,
|
|
errorMessage: '',
|
|
rawInfo: null,
|
|
},
|
|
purchase: {
|
|
autoBuyEnabled: true,
|
|
minAssetReserve: 0,
|
|
usedKnapsack: false,
|
|
purchaseTriggered: false,
|
|
assetBefore: 0,
|
|
assetAfter: 0,
|
|
purchaseAt: null,
|
|
},
|
|
dispatch: {
|
|
status: String(task.kuaishou_dispatch_status || 'pending').trim() || 'pending',
|
|
dispatchAt: null,
|
|
sendType: 0,
|
|
note: '',
|
|
},
|
|
returnNumber: {
|
|
status: 'pending',
|
|
returnedAt: null,
|
|
autoReturnEnabled: false,
|
|
},
|
|
consume: {
|
|
status: String(task.kuaishou_consume_status || 'pending').trim() || 'pending',
|
|
shopId: '',
|
|
shopName: '',
|
|
autoConsumeEnabled: false,
|
|
consumedAt: null,
|
|
errorMessage: '',
|
|
},
|
|
rebind: {
|
|
currentAttempt: 0,
|
|
history: [],
|
|
},
|
|
notes: '',
|
|
}
|
|
}
|
|
|
|
function resolveCloudSourceLabel(sourceKey: string, labelMap: CloudSourceLabelMap | undefined) {
|
|
const key = String(sourceKey || '').trim()
|
|
if (!key) return ''
|
|
|
|
const label = labelMap instanceof Map ? labelMap.get(key) : labelMap?.[key]
|
|
|
|
return String(label || key).trim() || key
|
|
}
|
|
|
|
function mapKuaishouCloudRebindContext(value: unknown): JsonRecord {
|
|
const record = value && typeof value === 'object' ? (value as JsonRecord) : {}
|
|
const history = Array.isArray(record.history) ? record.history : []
|
|
|
|
return {
|
|
currentAttempt: Math.max(0, Number(record.currentAttempt || 0) || 0),
|
|
history: history.map((item) => mapKuaishouCloudRebindHistoryItem(item)).filter(Boolean),
|
|
}
|
|
}
|
|
|
|
function mapKuaishouCloudRebindHistoryItem(value: unknown): JsonRecord | null {
|
|
if (!value || typeof value !== 'object') {
|
|
return null
|
|
}
|
|
|
|
const record = value as JsonRecord
|
|
const oldBinding =
|
|
record.oldBinding && typeof record.oldBinding === 'object'
|
|
? (record.oldBinding as JsonRecord)
|
|
: {}
|
|
const newBinding =
|
|
record.newBinding && typeof record.newBinding === 'object'
|
|
? (record.newBinding as JsonRecord)
|
|
: null
|
|
|
|
return {
|
|
attempt: Math.max(1, Number(record.attempt || 1) || 1),
|
|
source: String(record.source || '').trim(),
|
|
requestedAt: record.requestedAt || null,
|
|
requestedBy:
|
|
record.requestedBy && typeof record.requestedBy === 'object' ? record.requestedBy : null,
|
|
status: String(record.status || '').trim(),
|
|
errorMessage: String(record.errorMessage || '').trim(),
|
|
oldBinding: {
|
|
vnKey: String(oldBinding.vnKey || '').trim(),
|
|
vnId: Number(oldBinding.vnId || 0) || 0,
|
|
vnPhone: String(oldBinding.vnPhone || '').trim(),
|
|
bindUrl: String(oldBinding.bindUrl || '').trim(),
|
|
bindPreparedAt: oldBinding.bindPreparedAt || null,
|
|
bindExpiresAt: oldBinding.bindExpiresAt || null,
|
|
roleName: String(oldBinding.roleName || '').trim(),
|
|
roleId: String(oldBinding.roleId || '').trim(),
|
|
},
|
|
newBinding: newBinding
|
|
? {
|
|
vnKey: String(newBinding.vnKey || '').trim(),
|
|
vnId: Number(newBinding.vnId || 0) || 0,
|
|
vnPhone: String(newBinding.vnPhone || '').trim(),
|
|
bindUrl: String(newBinding.bindUrl || '').trim(),
|
|
bindPreparedAt: newBinding.bindPreparedAt || null,
|
|
bindExpiresAt: newBinding.bindExpiresAt || null,
|
|
}
|
|
: null,
|
|
}
|
|
}
|
|
|
|
export function mapRedeemResolutionContext(value: unknown): JsonRecord | null {
|
|
if (!value || typeof value !== 'object') {
|
|
return null
|
|
}
|
|
|
|
const record = value as JsonRecord
|
|
const attempts = Array.isArray(record.attempts)
|
|
? record.attempts
|
|
.map((attempt) => mapRedeemResolutionAttempt(attempt))
|
|
.filter((attempt): attempt is RedeemResolutionAttempt => Boolean(attempt))
|
|
: []
|
|
|
|
return {
|
|
status: String(record.status || '').trim(),
|
|
taskStatus: String(record.taskStatus || '').trim(),
|
|
replacementCount: Math.max(0, Number(record.replacementCount || 0)),
|
|
finishedAt: record.finishedAt || null,
|
|
attempts,
|
|
}
|
|
}
|
|
|
|
export function getTaskPrimaryClaimTokenId(task: TaskLike | null | undefined): number | null {
|
|
const value = Number(task?.primary_claim_token_id || 0)
|
|
return Number.isFinite(value) && value > 0 ? value : null
|
|
}
|
|
|
|
export function createAdminViewerContext(
|
|
session: AdminViewerSessionInput | null = null,
|
|
): AdminViewerContext {
|
|
const role = normalizeAdminRole(session?.role)
|
|
|
|
return {
|
|
role,
|
|
canViewSensitiveTaskData: role === 'admin' || role === 'operator',
|
|
canManageTaskLifecycle: role === 'admin' || role === 'operator',
|
|
canOperateAssistedTask: role === 'admin' || role === 'operator' || role === 'support',
|
|
canOperateKuaishouIndustryVoucher: role === 'admin' || role === 'operator' || role === 'support',
|
|
}
|
|
}
|
|
|
|
export async function resolveAdminTaskScreenshotUrl(
|
|
task: TaskLike,
|
|
viewerContext: AdminViewerContext,
|
|
): Promise<string> {
|
|
if (task.screenshot_path || task.runtime_session_id) {
|
|
return `/api/v1/admin/tasks/${task.id}/screenshot`
|
|
}
|
|
|
|
return ''
|
|
}
|
|
|
|
export function canViewerCloseTask(task: TaskLike, viewerContext: AdminViewerContext): boolean {
|
|
if (!['admin', 'operator', 'support'].includes(viewerContext.role)) {
|
|
return false
|
|
}
|
|
|
|
return !isTaskFinalStatus(task?.task_status)
|
|
}
|
|
|
|
export function resolveDisplayShopName(
|
|
provider: unknown,
|
|
shopId: unknown,
|
|
shopName: unknown,
|
|
): string {
|
|
const normalizedShopName = String(shopName || '').trim()
|
|
if (normalizedShopName) {
|
|
return normalizedShopName
|
|
}
|
|
|
|
const normalizedShopId = String(shopId || '').trim()
|
|
if (!normalizedShopId) {
|
|
return ''
|
|
}
|
|
|
|
return normalizedShopId
|
|
}
|
|
|
|
export function parseTaskContext(task: TaskLike | null | undefined): JsonRecord {
|
|
return parseTaskContextValue(task)
|
|
}
|
|
|
|
export function parseTaskState(task: TaskLike | null | undefined): JsonRecord {
|
|
return parseTaskStateValue(task)
|
|
}
|
|
|
|
export function resolveOrderItemTitle(item: Partial<OrderItemRow> | null | undefined): string {
|
|
if (!item) {
|
|
return ''
|
|
}
|
|
|
|
const spec = safeParseJson(item.spec_json)
|
|
|
|
return pickFirstNonEmpty([
|
|
spec.title,
|
|
spec.Title,
|
|
spec.itemTitle,
|
|
spec.item_title,
|
|
spec.goods_name,
|
|
spec.goodsName,
|
|
item.sku_name,
|
|
item.sku_code,
|
|
])
|
|
}
|
|
|
|
export function resolveOrderItemDeliveryMode(
|
|
tasks: TaskLike[] | null | undefined,
|
|
orderItemId: number | string,
|
|
): string {
|
|
const task = (Array.isArray(tasks) ? tasks : []).find(
|
|
(item) => item.order_item_id === orderItemId,
|
|
)
|
|
|
|
if (!task) {
|
|
return ''
|
|
}
|
|
|
|
if (String(task.executor_key || '').trim() === 'manual_dispatch') {
|
|
return 'manual_dispatch'
|
|
}
|
|
|
|
if (String(task.executor_key || '').trim() === 'kuaishou_ct_assisted') {
|
|
return 'kuaishou_cloud'
|
|
}
|
|
|
|
if (
|
|
task.requires_claim ||
|
|
getTaskPrimaryClaimTokenId(task) ||
|
|
task.primary_claim_token ||
|
|
task.claim_token
|
|
) {
|
|
return 'claim_link'
|
|
}
|
|
|
|
return String(task.executor_key || '').trim()
|
|
}
|
|
|
|
export function isManualDispatchTask(task: TaskLike | null | undefined): boolean {
|
|
return String(task?.executor_key || '').trim() === 'manual_dispatch'
|
|
}
|
|
|
|
export function canRegenerateClaimLinkForViewer(
|
|
task: TaskLike,
|
|
viewerContext: AdminViewerContext,
|
|
): boolean {
|
|
if (isManualDispatchTask(task)) {
|
|
return false
|
|
}
|
|
|
|
const baseAllowed = canRegenerateClaimLinkStatus(task.task_status)
|
|
|
|
if (!baseAllowed) {
|
|
return false
|
|
}
|
|
|
|
if (viewerContext.canManageTaskLifecycle) {
|
|
return true
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
function mapRedeemResolutionAttempt(value: unknown): RedeemResolutionAttempt | null {
|
|
if (!value || typeof value !== 'object') {
|
|
return null
|
|
}
|
|
|
|
const record = value as JsonRecord
|
|
return {
|
|
attempt: Math.max(1, Number(record.attempt || 1)),
|
|
codeMasked: String(record.codeMasked || '').trim(),
|
|
credentialType: String(record.credentialType || '').trim(),
|
|
outcome: String(record.outcome || '').trim(),
|
|
resultCode: String(record.resultCode || '').trim(),
|
|
resultMessage: String(record.resultMessage || '').trim(),
|
|
}
|
|
}
|
|
|
|
function pickFirstNonEmpty(values: unknown[]): string {
|
|
for (const value of values) {
|
|
if (typeof value === 'string' && value.trim()) {
|
|
return value.trim()
|
|
}
|
|
|
|
if (typeof value === 'number' && Number.isFinite(value)) {
|
|
return String(value)
|
|
}
|
|
}
|
|
|
|
return ''
|
|
}
|