后端迁移后台管理共享读辅助
This commit is contained in:
+99
-59
@@ -1,48 +1,80 @@
|
|||||||
// @ts-check
|
|
||||||
|
|
||||||
import { getAgisoShopConfig } from '../platforms/agiso/shop-config-service.js'
|
import { getAgisoShopConfig } from '../platforms/agiso/shop-config-service.js'
|
||||||
import { safeParseJson } from './admin-query-utils.js'
|
import { safeParseJson } from './admin-query-utils.js'
|
||||||
import { normalizeAdminRole } from './admin-auth-service.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<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') {
|
if (!value || typeof value !== 'object') {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const record = value as JsonRecord
|
||||||
return {
|
return {
|
||||||
outcome: String(value.outcome || '').trim(),
|
outcome: String(record.outcome || '').trim(),
|
||||||
deliveryReference: String(value.deliveryReference || '').trim(),
|
deliveryReference: String(record.deliveryReference || '').trim(),
|
||||||
deliveredCredential: viewerContext.canViewSensitiveTaskData ? String(value.deliveredCredential || '').trim() : '',
|
deliveredCredential: viewerContext.canViewSensitiveTaskData ? String(record.deliveredCredential || '').trim() : '',
|
||||||
resultMessage: String(value.resultMessage || '').trim(),
|
resultMessage: String(record.resultMessage || '').trim(),
|
||||||
completedAt: value.completedAt || null,
|
completedAt: record.completedAt || null,
|
||||||
completedBy: value.completedBy && typeof value.completedBy === 'object'
|
completedBy: record.completedBy && typeof record.completedBy === 'object'
|
||||||
? {
|
? {
|
||||||
userId: Number(value.completedBy.userId || 0) || 0,
|
userId: Number(record.completedBy.userId || 0) || 0,
|
||||||
username: String(value.completedBy.username || '').trim(),
|
username: String(record.completedBy.username || '').trim(),
|
||||||
role: String(value.completedBy.role || '').trim(),
|
role: String(record.completedBy.role || '').trim(),
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function mapKuaishouCloudFulfillmentContext(value) {
|
export function mapKuaishouCloudFulfillmentContext(value: unknown): JsonRecord | null {
|
||||||
if (!value || typeof value !== 'object') {
|
if (!value || typeof value !== 'object') {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
const ticket = value.ticket && typeof value.ticket === 'object' ? value.ticket : {}
|
const record = value as JsonRecord
|
||||||
const binding = value.binding && typeof value.binding === 'object' ? value.binding : {}
|
const ticket = record.ticket && typeof record.ticket === 'object' ? record.ticket : {}
|
||||||
const role = value.role && typeof value.role === 'object' ? value.role : {}
|
const binding = record.binding && typeof record.binding === 'object' ? record.binding : {}
|
||||||
const purchase = value.purchase && typeof value.purchase === 'object' ? value.purchase : {}
|
const role = record.role && typeof record.role === 'object' ? record.role : {}
|
||||||
const dispatch = value.dispatch && typeof value.dispatch === 'object' ? value.dispatch : {}
|
const purchase = record.purchase && typeof record.purchase === 'object' ? record.purchase : {}
|
||||||
const returnNumber = value.returnNumber && typeof value.returnNumber === 'object' ? value.returnNumber : {}
|
const dispatch = record.dispatch && typeof record.dispatch === 'object' ? record.dispatch : {}
|
||||||
const consume = value.consume && typeof value.consume === 'object' ? value.consume : {}
|
const returnNumber = record.returnNumber && typeof record.returnNumber === 'object' ? record.returnNumber : {}
|
||||||
|
const consume = record.consume && typeof record.consume === 'object' ? record.consume : {}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
flowType: String(value.flowType || '').trim(),
|
flowType: String(record.flowType || '').trim(),
|
||||||
configId: String(value.configId || '').trim(),
|
configId: String(record.configId || '').trim(),
|
||||||
internalSkuCode: String(value.internalSkuCode || '').trim(),
|
internalSkuCode: String(record.internalSkuCode || '').trim(),
|
||||||
internalSkuName: String(value.internalSkuName || '').trim(),
|
internalSkuName: String(record.internalSkuName || '').trim(),
|
||||||
ticket: {
|
ticket: {
|
||||||
code: String(ticket.code || '').trim(),
|
code: String(ticket.code || '').trim(),
|
||||||
capturedAt: ticket.capturedAt || null,
|
capturedAt: ticket.capturedAt || null,
|
||||||
@@ -102,41 +134,42 @@ export function mapKuaishouCloudFulfillmentContext(value) {
|
|||||||
consumedAt: consume.consumedAt || null,
|
consumedAt: consume.consumedAt || null,
|
||||||
errorMessage: String(consume.errorMessage || '').trim(),
|
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') {
|
if (!value || typeof value !== 'object') {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
const attempts = Array.isArray(value.attempts)
|
const record = value as JsonRecord
|
||||||
? value.attempts
|
const attempts = Array.isArray(record.attempts)
|
||||||
|
? record.attempts
|
||||||
.map((attempt) => mapRedeemResolutionAttempt(attempt))
|
.map((attempt) => mapRedeemResolutionAttempt(attempt))
|
||||||
.filter(Boolean)
|
.filter((attempt): attempt is RedeemResolutionAttempt => Boolean(attempt))
|
||||||
: []
|
: []
|
||||||
|
|
||||||
return {
|
return {
|
||||||
status: String(value.status || '').trim(),
|
status: String(record.status || '').trim(),
|
||||||
taskStatus: String(value.taskStatus || '').trim(),
|
taskStatus: String(record.taskStatus || '').trim(),
|
||||||
replacementCount: Math.max(0, Number(value.replacementCount || 0)),
|
replacementCount: Math.max(0, Number(record.replacementCount || 0)),
|
||||||
finishedAt: value.finishedAt || null,
|
finishedAt: record.finishedAt || null,
|
||||||
attempts,
|
attempts,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getTaskPrimaryInventoryItemId(task) {
|
export function getTaskPrimaryInventoryItemId(task: TaskLike | null | undefined): number | null {
|
||||||
const value = Number(task?.primary_inventory_item_id || 0)
|
const value = Number(task?.primary_inventory_item_id || 0)
|
||||||
return Number.isFinite(value) && value > 0 ? value : null
|
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)
|
const value = Number(task?.primary_claim_token_id || 0)
|
||||||
return Number.isFinite(value) && value > 0 ? value : null
|
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 role = normalizeAdminRole(session?.role)
|
||||||
const allowedInventoryGroupCodes = role === 'support'
|
const allowedInventoryGroupCodes = role === 'support'
|
||||||
? normalizeInventoryGroupCodes(session?.allowedInventoryGroups)
|
? 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<string> {
|
||||||
if (task.screenshot_path || task.browser_session_id) {
|
if (task.screenshot_path || task.browser_session_id) {
|
||||||
return `/api/v1/admin/tasks/${task.id}/screenshot`
|
return `/api/v1/admin/tasks/${task.id}/screenshot`
|
||||||
}
|
}
|
||||||
@@ -161,7 +197,7 @@ export async function resolveAdminTaskScreenshotUrl(task, viewerContext) {
|
|||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
|
|
||||||
export function canViewerCloseTask(task, viewerContext) {
|
export function canViewerCloseTask(task: TaskLike, viewerContext: AdminViewerContext): boolean {
|
||||||
if (!['admin', 'operator', 'support'].includes(viewerContext.role)) {
|
if (!['admin', 'operator', 'support'].includes(viewerContext.role)) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -169,7 +205,7 @@ export function canViewerCloseTask(task, viewerContext) {
|
|||||||
return !['redeemed', 'completed', 'closed'].includes(String(task?.task_status || '').trim())
|
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()
|
const normalizedShopName = String(shopName || '').trim()
|
||||||
if (normalizedShopName) {
|
if (normalizedShopName) {
|
||||||
return normalizedShopName
|
return normalizedShopName
|
||||||
@@ -190,7 +226,7 @@ export function resolveDisplayShopName(provider, shopId, shopName) {
|
|||||||
return normalizedShopId
|
return normalizedShopId
|
||||||
}
|
}
|
||||||
|
|
||||||
export function parseTaskContext(task) {
|
export function parseTaskContext(task: TaskLike | null | undefined): JsonRecord {
|
||||||
const value = task?.context_json
|
const value = task?.context_json
|
||||||
|
|
||||||
if (!value) {
|
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
|
const value = task?.state_json
|
||||||
|
|
||||||
if (!value) {
|
if (!value) {
|
||||||
@@ -226,7 +262,7 @@ export function parseTaskState(task) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function resolveOrderItemTitle(item) {
|
export function resolveOrderItemTitle(item: Partial<OrderItemRow> | null | undefined): string {
|
||||||
if (!item) {
|
if (!item) {
|
||||||
return ''
|
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)
|
const task = (Array.isArray(tasks) ? tasks : []).find((item) => item.order_item_id === orderItemId)
|
||||||
|
|
||||||
if (!task) {
|
if (!task) {
|
||||||
@@ -267,15 +303,15 @@ export function resolveOrderItemDeliveryMode(tasks, orderItemId) {
|
|||||||
return String(task.executor_key || '').trim()
|
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'
|
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'
|
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)) {
|
if (isManualDispatchTask(task)) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -295,7 +331,7 @@ export function canRegenerateClaimLinkForViewer(task, viewerContext) {
|
|||||||
&& canViewerAccessTaskInventoryGroup(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)) {
|
if (!viewerContext.canOperateAssistedTask || !isAssistedClaimTask(task) || !canViewerAccessTaskInventoryGroup(task, viewerContext)) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -303,7 +339,7 @@ export function canViewerConfirmAssistedRole(task, viewerContext) {
|
|||||||
return String(task?.task_status || '').trim() === 'claimed'
|
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)) {
|
if (!viewerContext.canOperateAssistedTask || !isAssistedClaimTask(task) || !canViewerAccessTaskInventoryGroup(task, viewerContext)) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -311,7 +347,10 @@ export function canViewerRedeemAssistedTask(task, viewerContext) {
|
|||||||
return ['role_confirmed', 'redeeming'].includes(String(task?.task_status || '').trim())
|
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) {
|
if (!viewerContext.restrictInventoryGroups) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -334,29 +373,30 @@ export function canViewerAccessTaskInventoryGroup(task, viewerContext = createAd
|
|||||||
return allowedInventoryGroupCodes.includes(inventoryGroupCode)
|
return allowedInventoryGroupCodes.includes(inventoryGroupCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function normalizeInventoryGroupCodes(values) {
|
export function normalizeInventoryGroupCodes(values: unknown): string[] {
|
||||||
return Array.from(new Set((Array.isArray(values) ? values : [])
|
return Array.from(new Set((Array.isArray(values) ? values : [])
|
||||||
.map((value) => String(value || '').trim())
|
.map((value) => String(value || '').trim())
|
||||||
.filter(Boolean)))
|
.filter(Boolean)))
|
||||||
}
|
}
|
||||||
|
|
||||||
function mapRedeemResolutionAttempt(value) {
|
function mapRedeemResolutionAttempt(value: unknown): RedeemResolutionAttempt | null {
|
||||||
if (!value || typeof value !== 'object') {
|
if (!value || typeof value !== 'object') {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const record = value as JsonRecord
|
||||||
return {
|
return {
|
||||||
attempt: Math.max(1, Number(value.attempt || 1)),
|
attempt: Math.max(1, Number(record.attempt || 1)),
|
||||||
inventoryItemId: Number(value.inventoryItemId || 0) || null,
|
inventoryItemId: Number(record.inventoryItemId || 0) || null,
|
||||||
codeMasked: String(value.codeMasked || '').trim(),
|
codeMasked: String(record.codeMasked || '').trim(),
|
||||||
credentialType: String(value.credentialType || '').trim(),
|
credentialType: String(record.credentialType || '').trim(),
|
||||||
outcome: String(value.outcome || '').trim(),
|
outcome: String(record.outcome || '').trim(),
|
||||||
resultCode: String(value.resultCode || '').trim(),
|
resultCode: String(record.resultCode || '').trim(),
|
||||||
resultMessage: String(value.resultMessage || '').trim(),
|
resultMessage: String(record.resultMessage || '').trim(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function pickFirstNonEmpty(values) {
|
function pickFirstNonEmpty(values: unknown[]): string {
|
||||||
for (const value of values) {
|
for (const value of values) {
|
||||||
if (typeof value === 'string' && value.trim()) {
|
if (typeof value === 'string' && value.trim()) {
|
||||||
return value.trim()
|
return value.trim()
|
||||||
@@ -7,15 +7,11 @@ import {
|
|||||||
parseTaskContext,
|
parseTaskContext,
|
||||||
} from '../admin-read-shared-helpers.js'
|
} from '../admin-read-shared-helpers.js'
|
||||||
|
|
||||||
|
import type { AdminViewerContext } from '../admin-read-shared-helpers.js'
|
||||||
import type { TaskRow } from '../../../types/repository-rows.js'
|
import type { TaskRow } from '../../../types/repository-rows.js'
|
||||||
|
|
||||||
type AssistedTaskAction = 'confirm' | 'redeem'
|
type AssistedTaskAction = 'confirm' | 'redeem'
|
||||||
|
|
||||||
type AdminViewerContextLike = {
|
|
||||||
canOperateAssistedTask?: boolean
|
|
||||||
[key: string]: unknown
|
|
||||||
}
|
|
||||||
|
|
||||||
type TaskClaimLink = {
|
type TaskClaimLink = {
|
||||||
token: string
|
token: string
|
||||||
expiredAt: string | null
|
expiredAt: string | null
|
||||||
@@ -61,7 +57,7 @@ export function resolveTaskInventoryGroupCodes(task: TaskRow): string[] | null {
|
|||||||
|
|
||||||
export function ensureViewerCanOperateAssistedTask(
|
export function ensureViewerCanOperateAssistedTask(
|
||||||
task: TaskRow,
|
task: TaskRow,
|
||||||
viewerContext: AdminViewerContextLike,
|
viewerContext: AdminViewerContext,
|
||||||
action: AssistedTaskAction,
|
action: AssistedTaskAction,
|
||||||
): void {
|
): void {
|
||||||
if (!viewerContext.canOperateAssistedTask || !isAssistedClaimTask(task)) {
|
if (!viewerContext.canOperateAssistedTask || !isAssistedClaimTask(task)) {
|
||||||
|
|||||||
@@ -743,6 +743,15 @@
|
|||||||
- `npm run typecheck`
|
- `npm run typecheck`
|
||||||
- `npm run build`
|
- `npm run build`
|
||||||
- `npm test` 共 139 个用例通过
|
- `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 个用例通过
|
||||||
|
|
||||||
## 下一步建议
|
## 下一步建议
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user