From 41a2360adf5f2a1f2ad8915c7d7283f8251538bf Mon Sep 17 00:00:00 2001 From: yml Date: Thu, 21 May 2026 16:55:42 +0800 Subject: [PATCH] =?UTF-8?q?=E5=90=8E=E7=AB=AF=E8=BF=81=E7=A7=BB=E5=90=8E?= =?UTF-8?q?=E5=8F=B0=E7=AE=A1=E7=90=86=E5=85=B1=E4=BA=AB=E8=AF=BB=E8=BE=85?= =?UTF-8?q?=E5=8A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...elpers.js => admin-read-shared-helpers.ts} | 158 +++++++++++------- .../src/services/admin/write/shared.ts | 8 +- docs/backend-typescript-migration-plan.md | 9 + 3 files changed, 110 insertions(+), 65 deletions(-) rename apps/backend/src/services/admin/{admin-read-shared-helpers.js => admin-read-shared-helpers.ts} (60%) diff --git a/apps/backend/src/services/admin/admin-read-shared-helpers.js b/apps/backend/src/services/admin/admin-read-shared-helpers.ts similarity index 60% rename from apps/backend/src/services/admin/admin-read-shared-helpers.js rename to apps/backend/src/services/admin/admin-read-shared-helpers.ts index e4604ec9..2ccc34db 100644 --- a/apps/backend/src/services/admin/admin-read-shared-helpers.js +++ b/apps/backend/src/services/admin/admin-read-shared-helpers.ts @@ -1,48 +1,80 @@ -// @ts-check - import { getAgisoShopConfig } from '../platforms/agiso/shop-config-service.js' import { safeParseJson } from './admin-query-utils.js' import { normalizeAdminRole } from './admin-auth-service.js' -export function mapManualDispatchContext(value, viewerContext = createAdminViewerContext()) { +import type { AdminViewerSessionInput } from '../../types/admin-read-inputs.js' +import type { OrderItemRow, TaskRow } from '../../types/repository-rows.js' + +type JsonRecord = Record + +type TaskLike = Partial & { + 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(value.outcome || '').trim(), - deliveryReference: String(value.deliveryReference || '').trim(), - deliveredCredential: viewerContext.canViewSensitiveTaskData ? String(value.deliveredCredential || '').trim() : '', - resultMessage: String(value.resultMessage || '').trim(), - completedAt: value.completedAt || null, - completedBy: value.completedBy && typeof value.completedBy === 'object' + 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(value.completedBy.userId || 0) || 0, - username: String(value.completedBy.username || '').trim(), - role: String(value.completedBy.role || '').trim(), + userId: Number(record.completedBy.userId || 0) || 0, + username: String(record.completedBy.username || '').trim(), + role: String(record.completedBy.role || '').trim(), } : null, } } -export function mapKuaishouCloudFulfillmentContext(value) { +export function mapKuaishouCloudFulfillmentContext(value: unknown): JsonRecord | null { if (!value || typeof value !== 'object') { return null } - const ticket = value.ticket && typeof value.ticket === 'object' ? value.ticket : {} - const binding = value.binding && typeof value.binding === 'object' ? value.binding : {} - const role = value.role && typeof value.role === 'object' ? value.role : {} - const purchase = value.purchase && typeof value.purchase === 'object' ? value.purchase : {} - const dispatch = value.dispatch && typeof value.dispatch === 'object' ? value.dispatch : {} - const returnNumber = value.returnNumber && typeof value.returnNumber === 'object' ? value.returnNumber : {} - const consume = value.consume && typeof value.consume === 'object' ? value.consume : {} + 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(value.flowType || '').trim(), - configId: String(value.configId || '').trim(), - internalSkuCode: String(value.internalSkuCode || '').trim(), - internalSkuName: String(value.internalSkuName || '').trim(), + 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, @@ -102,41 +134,42 @@ export function mapKuaishouCloudFulfillmentContext(value) { consumedAt: consume.consumedAt || null, errorMessage: String(consume.errorMessage || '').trim(), }, - notes: String(value.notes || '').trim(), + notes: String(record.notes || '').trim(), } } -export function mapRedeemResolutionContext(value) { +export function mapRedeemResolutionContext(value: unknown): JsonRecord | null { if (!value || typeof value !== 'object') { return null } - const attempts = Array.isArray(value.attempts) - ? value.attempts + const record = value as JsonRecord + const attempts = Array.isArray(record.attempts) + ? record.attempts .map((attempt) => mapRedeemResolutionAttempt(attempt)) - .filter(Boolean) + .filter((attempt): attempt is RedeemResolutionAttempt => Boolean(attempt)) : [] return { - status: String(value.status || '').trim(), - taskStatus: String(value.taskStatus || '').trim(), - replacementCount: Math.max(0, Number(value.replacementCount || 0)), - finishedAt: value.finishedAt || null, + 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) { +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) { +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 = null) { +export function createAdminViewerContext(session: AdminViewerSessionInput | null = null): AdminViewerContext { const role = normalizeAdminRole(session?.role) const allowedInventoryGroupCodes = role === 'support' ? normalizeInventoryGroupCodes(session?.allowedInventoryGroups) @@ -153,7 +186,10 @@ export function createAdminViewerContext(session = null) { } } -export async function resolveAdminTaskScreenshotUrl(task, viewerContext) { +export async function resolveAdminTaskScreenshotUrl( + task: TaskLike, + viewerContext: AdminViewerContext, +): Promise { if (task.screenshot_path || task.browser_session_id) { return `/api/v1/admin/tasks/${task.id}/screenshot` } @@ -161,7 +197,7 @@ export async function resolveAdminTaskScreenshotUrl(task, viewerContext) { return '' } -export function canViewerCloseTask(task, viewerContext) { +export function canViewerCloseTask(task: TaskLike, viewerContext: AdminViewerContext): boolean { if (!['admin', 'operator', 'support'].includes(viewerContext.role)) { return false } @@ -169,7 +205,7 @@ export function canViewerCloseTask(task, viewerContext) { return !['redeemed', 'completed', 'closed'].includes(String(task?.task_status || '').trim()) } -export function resolveDisplayShopName(provider, shopId, shopName) { +export function resolveDisplayShopName(provider: unknown, shopId: unknown, shopName: unknown): string { const normalizedShopName = String(shopName || '').trim() if (normalizedShopName) { return normalizedShopName @@ -190,7 +226,7 @@ export function resolveDisplayShopName(provider, shopId, shopName) { return normalizedShopId } -export function parseTaskContext(task) { +export function parseTaskContext(task: TaskLike | null | undefined): JsonRecord { const value = task?.context_json if (!value) { @@ -208,7 +244,7 @@ export function parseTaskContext(task) { } } -export function parseTaskState(task) { +export function parseTaskState(task: TaskLike | null | undefined): JsonRecord { const value = task?.state_json if (!value) { @@ -226,7 +262,7 @@ export function parseTaskState(task) { } } -export function resolveOrderItemTitle(item) { +export function resolveOrderItemTitle(item: Partial | null | undefined): string { if (!item) { return '' } @@ -245,7 +281,7 @@ export function resolveOrderItemTitle(item) { ]) } -export function resolveOrderItemDeliveryMode(tasks, orderItemId) { +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) { @@ -267,15 +303,15 @@ export function resolveOrderItemDeliveryMode(tasks, orderItemId) { return String(task.executor_key || '').trim() } -export function isManualDispatchTask(task) { +export function isManualDispatchTask(task: TaskLike | null | undefined): boolean { return String(task?.executor_key || '').trim() === 'manual_dispatch' } -export function isAssistedClaimTask(task) { +export function isAssistedClaimTask(task: TaskLike | null | undefined): boolean { return String(task?.executor_key || '').trim() === 'tencent_claim_assisted' } -export function canRegenerateClaimLinkForViewer(task, viewerContext) { +export function canRegenerateClaimLinkForViewer(task: TaskLike, viewerContext: AdminViewerContext): boolean { if (isManualDispatchTask(task)) { return false } @@ -295,7 +331,7 @@ export function canRegenerateClaimLinkForViewer(task, viewerContext) { && canViewerAccessTaskInventoryGroup(task, viewerContext) } -export function canViewerConfirmAssistedRole(task, viewerContext) { +export function canViewerConfirmAssistedRole(task: TaskLike, viewerContext: AdminViewerContext): boolean { if (!viewerContext.canOperateAssistedTask || !isAssistedClaimTask(task) || !canViewerAccessTaskInventoryGroup(task, viewerContext)) { return false } @@ -303,7 +339,7 @@ export function canViewerConfirmAssistedRole(task, viewerContext) { return String(task?.task_status || '').trim() === 'claimed' } -export function canViewerRedeemAssistedTask(task, viewerContext) { +export function canViewerRedeemAssistedTask(task: TaskLike, viewerContext: AdminViewerContext): boolean { if (!viewerContext.canOperateAssistedTask || !isAssistedClaimTask(task) || !canViewerAccessTaskInventoryGroup(task, viewerContext)) { return false } @@ -311,7 +347,10 @@ export function canViewerRedeemAssistedTask(task, viewerContext) { return ['role_confirmed', 'redeeming'].includes(String(task?.task_status || '').trim()) } -export function canViewerAccessTaskInventoryGroup(task, viewerContext = createAdminViewerContext()) { +export function canViewerAccessTaskInventoryGroup( + task: TaskLike, + viewerContext: AdminViewerContext = createAdminViewerContext(), +): boolean { if (!viewerContext.restrictInventoryGroups) { return true } @@ -334,29 +373,30 @@ export function canViewerAccessTaskInventoryGroup(task, viewerContext = createAd return allowedInventoryGroupCodes.includes(inventoryGroupCode) } -export function normalizeInventoryGroupCodes(values) { +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) { +function mapRedeemResolutionAttempt(value: unknown): RedeemResolutionAttempt | null { if (!value || typeof value !== 'object') { return null } + const record = value as JsonRecord return { - attempt: Math.max(1, Number(value.attempt || 1)), - inventoryItemId: Number(value.inventoryItemId || 0) || null, - codeMasked: String(value.codeMasked || '').trim(), - credentialType: String(value.credentialType || '').trim(), - outcome: String(value.outcome || '').trim(), - resultCode: String(value.resultCode || '').trim(), - resultMessage: String(value.resultMessage || '').trim(), + 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) { +function pickFirstNonEmpty(values: unknown[]): string { for (const value of values) { if (typeof value === 'string' && value.trim()) { return value.trim() diff --git a/apps/backend/src/services/admin/write/shared.ts b/apps/backend/src/services/admin/write/shared.ts index 2f6d6ca1..76d4f4da 100644 --- a/apps/backend/src/services/admin/write/shared.ts +++ b/apps/backend/src/services/admin/write/shared.ts @@ -7,15 +7,11 @@ import { parseTaskContext, } from '../admin-read-shared-helpers.js' +import type { AdminViewerContext } from '../admin-read-shared-helpers.js' import type { TaskRow } from '../../../types/repository-rows.js' type AssistedTaskAction = 'confirm' | 'redeem' -type AdminViewerContextLike = { - canOperateAssistedTask?: boolean - [key: string]: unknown -} - type TaskClaimLink = { token: string expiredAt: string | null @@ -61,7 +57,7 @@ export function resolveTaskInventoryGroupCodes(task: TaskRow): string[] | null { export function ensureViewerCanOperateAssistedTask( task: TaskRow, - viewerContext: AdminViewerContextLike, + viewerContext: AdminViewerContext, action: AssistedTaskAction, ): void { if (!viewerContext.canOperateAssistedTask || !isAssistedClaimTask(task)) { diff --git a/docs/backend-typescript-migration-plan.md b/docs/backend-typescript-migration-plan.md index 952424a1..260e9cd7 100644 --- a/docs/backend-typescript-migration-plan.md +++ b/docs/backend-typescript-migration-plan.md @@ -743,6 +743,15 @@ - `npm run typecheck` - `npm run build` - `npm test` 共 139 个用例通过 +186. 后台 Admin 读侧共享上下文 / 权限 helper 迁移到 `.ts`: + - `src/services/admin/admin-read-shared-helpers.ts` + - `src/services/admin/write/shared.ts` +187. Admin viewer context、敏感信息可见性、任务上下文解析、订单商品标题 / 发货模式解析、辅助领取权限判断、库存组访问控制与兑换 resolution 映射已进入 TS 编译链路;任务 row、订单商品 row、session input、动态 JSON record 与 viewer context 补齐类型,并让写侧 helper 复用共享 viewer context 类型 +188. Docker 内验证通过: + - `src/services/admin/*.test.js` 共 8 个用例通过 + - `npm run typecheck` + - `npm run build` + - `npm test` 共 139 个用例通过 ## 下一步建议