From 4d03b4ef8ddc54b4b7355222be2e79fd590c7be0 Mon Sep 17 00:00:00 2001 From: yml Date: Tue, 14 Apr 2026 20:36:52 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E5=AE=A2=E6=9C=8D=E6=9D=83?= =?UTF-8?q?=E9=99=90-=E5=A2=9E=E5=8A=A0=E5=85=91=E6=8D=A2=E6=88=AA?= =?UTF-8?q?=E5=9B=BE=E5=92=8C=E5=85=B3=E9=97=AD=E4=BB=BB=E5=8A=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/backend/src/routes/admin/tasks.js | 2 +- .../src/services/admin/admin-read-service.js | 17 ++------ .../admin/admin-read-shared-helpers.js | 12 ++++-- .../admin/admin-read-shared-helpers.test.js | 39 +++++++++++++++++++ .../src/views/admin/AdminTaskDetailView.vue | 18 +++++---- .../src/views/admin/AdminTasksView.vue | 26 ++++++++++--- 6 files changed, 82 insertions(+), 32 deletions(-) create mode 100644 apps/backend/src/services/admin/admin-read-shared-helpers.test.js diff --git a/apps/backend/src/routes/admin/tasks.js b/apps/backend/src/routes/admin/tasks.js index 72ea5101..5d46c032 100644 --- a/apps/backend/src/routes/admin/tasks.js +++ b/apps/backend/src/routes/admin/tasks.js @@ -198,7 +198,7 @@ router.post('/tasks/:taskId/support-redeem', requireAdminRoles(['admin', 'operat }, )) -router.post('/tasks/:taskId/close', requireAdminRoles(['admin', 'operator']), createJsonHandler( +router.post('/tasks/:taskId/close', requireAdminRoles(['admin', 'operator', 'support']), createJsonHandler( (req) => closeAdminTask((/** @type {AdminTaskRouteParams} */ (req.params)).taskId), { successMessage: '任务已关闭', diff --git a/apps/backend/src/services/admin/admin-read-service.js b/apps/backend/src/services/admin/admin-read-service.js index 513f5d14..25678d96 100644 --- a/apps/backend/src/services/admin/admin-read-service.js +++ b/apps/backend/src/services/admin/admin-read-service.js @@ -14,6 +14,7 @@ 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, @@ -281,7 +282,7 @@ export async function getAdminTaskDetail(taskId, session = null) { && ['retry_pending', 'manual_review', 'waiting_inventory'].includes(task.task_status), canReleaseInventory: mappedInventoryBindings.some((binding) => binding.canRelease), canRegenerateClaimLink: canRegenerateClaimLinkForViewer(task, viewerContext), - canClose: viewerContext.canManageTaskLifecycle && !['redeemed', 'closed'].includes(task.task_status), + 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), canSupportConfirmRole: canViewerConfirmAssistedRole(task, viewerContext), @@ -295,18 +296,6 @@ export async function getAdminTaskDetail(taskId, session = null) { /** @param {AdminViewerSessionInput | null} [session] */ export async function getAdminTaskScreenshotPath(taskId, session = null) { const task = await getRequiredTask(taskId) - const viewerContext = createAdminViewerContext(session) - - if (viewerContext.role === 'support') { - if (!task.browser_session_id) { - throw createHttpError('当前任务还没有客服复核截图', { - statusCode: 404, - errorCode: 'admin_task_review_screenshot_not_found', - }) - } - - return getTencentBrowserSessionReviewScreenshotPath(task.browser_session_id) - } if (task.screenshot_path) { return task.screenshot_path @@ -316,7 +305,7 @@ export async function getAdminTaskScreenshotPath(taskId, session = null) { return getTencentBrowserSessionReviewScreenshotPath(task.browser_session_id) } - throw createHttpError('当前任务还没有截图', { + throw createHttpError('当前任务还没有可查看截图', { statusCode: 404, errorCode: 'admin_task_screenshot_not_found', }) diff --git a/apps/backend/src/services/admin/admin-read-shared-helpers.js b/apps/backend/src/services/admin/admin-read-shared-helpers.js index 28b01ce9..7d2be6f1 100644 --- a/apps/backend/src/services/admin/admin-read-shared-helpers.js +++ b/apps/backend/src/services/admin/admin-read-shared-helpers.js @@ -68,10 +68,6 @@ export function createAdminViewerContext(session = null) { } export async function resolveAdminTaskScreenshotUrl(task, viewerContext) { - if (viewerContext.role === 'support') { - return task.browser_session_id ? `/api/v1/admin/tasks/${task.id}/screenshot` : '' - } - if (task.screenshot_path || task.browser_session_id) { return `/api/v1/admin/tasks/${task.id}/screenshot` } @@ -79,6 +75,14 @@ export async function resolveAdminTaskScreenshotUrl(task, viewerContext) { return '' } +export function canViewerCloseTask(task, viewerContext) { + if (!['admin', 'operator', 'support'].includes(viewerContext.role)) { + return false + } + + return !['redeemed', 'closed'].includes(String(task?.task_status || '').trim()) +} + export function resolveDisplayShopName(provider, shopId, shopName) { const normalizedShopName = String(shopName || '').trim() if (normalizedShopName) { diff --git a/apps/backend/src/services/admin/admin-read-shared-helpers.test.js b/apps/backend/src/services/admin/admin-read-shared-helpers.test.js new file mode 100644 index 00000000..94a82325 --- /dev/null +++ b/apps/backend/src/services/admin/admin-read-shared-helpers.test.js @@ -0,0 +1,39 @@ +import test from 'node:test' +import assert from 'node:assert/strict' + +import { + canViewerCloseTask, + createAdminViewerContext, + resolveAdminTaskScreenshotUrl, +} from './admin-read-shared-helpers.js' + +test('resolveAdminTaskScreenshotUrl lets support view final redeemed screenshot', async () => { + const task = { + id: 12, + screenshot_path: '/tmp/redeemed-12.png', + browser_session_id: '', + } + + const screenshotUrl = await resolveAdminTaskScreenshotUrl(task, createAdminViewerContext({ role: 'support' })) + + assert.equal(screenshotUrl, '/api/v1/admin/tasks/12/screenshot') +}) + +test('resolveAdminTaskScreenshotUrl falls back to review screenshot when only browser session exists', async () => { + const task = { + id: 13, + screenshot_path: '', + browser_session_id: 'browser-session-13', + } + + const screenshotUrl = await resolveAdminTaskScreenshotUrl(task, createAdminViewerContext({ role: 'support' })) + + assert.equal(screenshotUrl, '/api/v1/admin/tasks/13/screenshot') +}) + +test('canViewerCloseTask allows support to close active task but not redeemed task', () => { + const viewerContext = createAdminViewerContext({ role: 'support' }) + + assert.equal(canViewerCloseTask({ task_status: 'link_generated' }, viewerContext), true) + assert.equal(canViewerCloseTask({ task_status: 'redeemed' }, viewerContext), false) +}) diff --git a/apps/frontend/src/views/admin/AdminTaskDetailView.vue b/apps/frontend/src/views/admin/AdminTaskDetailView.vue index 3a75c9d9..b1518234 100644 --- a/apps/frontend/src/views/admin/AdminTaskDetailView.vue +++ b/apps/frontend/src/views/admin/AdminTaskDetailView.vue @@ -33,7 +33,8 @@ const manualDispatchForm = reactive({ deliveredCredential: '', resultMessage: '', }) -const canOperateTasks = computed(() => hasAdminRole('operator')) +const canManageTaskLifecycle = computed(() => hasAdminRole('operator')) +const canCloseTasks = computed(() => hasAdminRole('support')) const canViewSensitiveTaskData = computed(() => Boolean(detail.value?.operations.canViewSensitiveTaskData)) const claimUrl = computed(() => { const tokenStatus = String(detail.value?.claimToken?.status || '').trim() @@ -304,6 +305,7 @@ onBeforeUnmount(clearScreenshotPreview)
外部流水 / 单号 @@ -599,7 +601,7 @@ onBeforeUnmount(clearScreenshotPreview) 已交付内容 处理备注 (null) const errorMessage = ref('') const status = ref('') const taskNo = ref('') +const platformOrderId = ref('') const roleId = ref('') const skuCode = ref('') const dateFrom = ref('') @@ -32,6 +33,8 @@ const pagination = ref({ pageSize: 20, total: 0, }) +const canManageTaskLifecycle = hasAdminRole('operator') +const canCloseTasks = hasAdminRole('support') type AdminTaskActionKey = 'retry' | 'regenerate_claim_link' | 'manual_review' | 'close' @@ -45,6 +48,7 @@ async function loadTasks(page = pagination.value.page) { pageSize: pagination.value.pageSize, status: status.value.trim(), taskNo: taskNo.value.trim(), + platformOrderId: platformOrderId.value.trim(), roleId: roleId.value.trim(), skuCode: skuCode.value.trim(), dateFrom: dateFrom.value.trim(), @@ -188,6 +192,7 @@ function handleActionCommand(command: `${AdminTaskActionKey}:${number}`) { +
@@ -195,7 +200,7 @@ function handleActionCommand(command: `${AdminTaskActionKey}:${number}`) { - 查询 + 查询

{{ errorMessage }}

@@ -277,6 +282,7 @@ function handleActionCommand(command: `${AdminTaskActionKey}:${number}`) {
@@ -297,17 +303,27 @@ function handleActionCommand(command: `${AdminTaskActionKey}:${number}`) {
- -