后端迁移后台管理共享读辅助
This commit is contained in:
@@ -0,0 +1,411 @@
|
||||
import { getAgisoShopConfig } from '../platforms/agiso/shop-config-service.js'
|
||||
import { safeParseJson } from './admin-query-utils.js'
|
||||
import { normalizeAdminRole } from './admin-auth-service.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 TaskLike = Partial<TaskRow> & {
|
||||
state_json?: string | JsonRecord
|
||||
}
|
||||
|
||||
export type AdminViewerContext = {
|
||||
role: string
|
||||
allowedInventoryGroupCodes: string[] | null
|
||||
restrictInventoryGroups: boolean
|
||||
canViewSensitiveTaskData: boolean
|
||||
canManageSensitiveInventory: boolean
|
||||
canManageTaskLifecycle: boolean
|
||||
canOperateAssistedTask: boolean
|
||||
}
|
||||
|
||||
type RedeemResolutionAttempt = {
|
||||
attempt: number
|
||||
inventoryItemId: number | null
|
||||
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): 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 : {}
|
||||
|
||||
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',
|
||||
cloudSourceKey: String(binding.cloudSourceKey || '').trim(),
|
||||
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(),
|
||||
},
|
||||
role: {
|
||||
status: String(role.status || 'pending').trim() || 'pending',
|
||||
name: String(role.name || '').trim(),
|
||||
rid: String(role.rid || '').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(),
|
||||
},
|
||||
notes: String(record.notes || '').trim(),
|
||||
}
|
||||
}
|
||||
|
||||
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 getTaskPrimaryInventoryItemId(task: TaskLike | null | undefined): number | null {
|
||||
const value = Number(task?.primary_inventory_item_id || 0)
|
||||
return Number.isFinite(value) && value > 0 ? value : null
|
||||
}
|
||||
|
||||
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)
|
||||
const allowedInventoryGroupCodes = role === 'support'
|
||||
? normalizeInventoryGroupCodes(session?.allowedInventoryGroups)
|
||||
: null
|
||||
|
||||
return {
|
||||
role,
|
||||
allowedInventoryGroupCodes,
|
||||
restrictInventoryGroups: role === 'support',
|
||||
canViewSensitiveTaskData: role === 'admin' || role === 'operator',
|
||||
canManageSensitiveInventory: role === 'admin',
|
||||
canManageTaskLifecycle: role === 'admin' || role === 'operator',
|
||||
canOperateAssistedTask: role === 'admin' || role === 'operator' || role === 'support',
|
||||
}
|
||||
}
|
||||
|
||||
export async function resolveAdminTaskScreenshotUrl(
|
||||
task: TaskLike,
|
||||
viewerContext: AdminViewerContext,
|
||||
): Promise<string> {
|
||||
if (task.screenshot_path || task.browser_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 !['redeemed', 'completed', 'closed'].includes(String(task?.task_status || '').trim())
|
||||
}
|
||||
|
||||
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 ''
|
||||
}
|
||||
|
||||
if (String(provider || '').trim().toLowerCase() === 'agiso') {
|
||||
const configuredName = String(getAgisoShopConfig(normalizedShopId)?.shopName || '').trim()
|
||||
if (configuredName) {
|
||||
return configuredName
|
||||
}
|
||||
}
|
||||
|
||||
return normalizedShopId
|
||||
}
|
||||
|
||||
export function parseTaskContext(task: TaskLike | null | undefined): JsonRecord {
|
||||
const value = task?.context_json
|
||||
|
||||
if (!value) {
|
||||
return {}
|
||||
}
|
||||
|
||||
if (typeof value === 'object') {
|
||||
return value
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.parse(String(value || '{}'))
|
||||
} catch {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
export function parseTaskState(task: TaskLike | null | undefined): JsonRecord {
|
||||
const value = task?.state_json
|
||||
|
||||
if (!value) {
|
||||
return {}
|
||||
}
|
||||
|
||||
if (typeof value === 'object') {
|
||||
return value
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.parse(String(value || '{}'))
|
||||
} catch {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
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 isAssistedClaimTask(task: TaskLike | null | undefined): boolean {
|
||||
return String(task?.executor_key || '').trim() === 'tencent_claim_assisted'
|
||||
}
|
||||
|
||||
export function canRegenerateClaimLinkForViewer(task: TaskLike, viewerContext: AdminViewerContext): boolean {
|
||||
if (isManualDispatchTask(task)) {
|
||||
return false
|
||||
}
|
||||
|
||||
const baseAllowed = ['link_generated', 'claimed', 'role_confirmed', 'retry_pending', 'manual_review'].includes(task.task_status)
|
||||
|
||||
if (!baseAllowed) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (viewerContext.canManageTaskLifecycle) {
|
||||
return true
|
||||
}
|
||||
|
||||
return viewerContext.canOperateAssistedTask
|
||||
&& isAssistedClaimTask(task)
|
||||
&& canViewerAccessTaskInventoryGroup(task, viewerContext)
|
||||
}
|
||||
|
||||
export function canViewerConfirmAssistedRole(task: TaskLike, viewerContext: AdminViewerContext): boolean {
|
||||
if (!viewerContext.canOperateAssistedTask || !isAssistedClaimTask(task) || !canViewerAccessTaskInventoryGroup(task, viewerContext)) {
|
||||
return false
|
||||
}
|
||||
|
||||
return String(task?.task_status || '').trim() === 'claimed'
|
||||
}
|
||||
|
||||
export function canViewerRedeemAssistedTask(task: TaskLike, viewerContext: AdminViewerContext): boolean {
|
||||
if (!viewerContext.canOperateAssistedTask || !isAssistedClaimTask(task) || !canViewerAccessTaskInventoryGroup(task, viewerContext)) {
|
||||
return false
|
||||
}
|
||||
|
||||
return ['role_confirmed', 'redeeming'].includes(String(task?.task_status || '').trim())
|
||||
}
|
||||
|
||||
export function canViewerAccessTaskInventoryGroup(
|
||||
task: TaskLike,
|
||||
viewerContext: AdminViewerContext = createAdminViewerContext(),
|
||||
): boolean {
|
||||
if (!viewerContext.restrictInventoryGroups) {
|
||||
return true
|
||||
}
|
||||
|
||||
const allowedInventoryGroupCodes = Array.isArray(viewerContext.allowedInventoryGroupCodes)
|
||||
? viewerContext.allowedInventoryGroupCodes
|
||||
: []
|
||||
|
||||
if (allowedInventoryGroupCodes.length === 0) {
|
||||
return false
|
||||
}
|
||||
|
||||
const taskContext = parseTaskContext(task)
|
||||
const inventoryGroupCode = String(taskContext?.inventoryGroupCode || '').trim()
|
||||
|
||||
if (!inventoryGroupCode) {
|
||||
return true
|
||||
}
|
||||
|
||||
return allowedInventoryGroupCodes.includes(inventoryGroupCode)
|
||||
}
|
||||
|
||||
export function normalizeInventoryGroupCodes(values: unknown): string[] {
|
||||
return Array.from(new Set((Array.isArray(values) ? values : [])
|
||||
.map((value) => String(value || '').trim())
|
||||
.filter(Boolean)))
|
||||
}
|
||||
|
||||
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)),
|
||||
inventoryItemId: Number(record.inventoryItemId || 0) || null,
|
||||
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 ''
|
||||
}
|
||||
Reference in New Issue
Block a user