diff --git a/apps/backend/README.md b/apps/backend/README.md index f0d88734..3f4109ac 100644 --- a/apps/backend/README.md +++ b/apps/backend/README.md @@ -2,7 +2,7 @@ 快手轻量后端服务。 -后端负责订单入库、履约任务编排、快手 Cloud / 91 卡券 / 快手小店核销配置、后台管理接口和数据库迁移。默认入口就是轻量后端,不再包含浏览器会话、OCR 或旧咸鱼自动发货链路。 +后端负责订单入库、履约任务编排、快手 Cloud / 91 卡券 / 快手小店核销配置、后台管理接口和数据库迁移。默认入口就是轻量后端,不再包含浏览器会话或 OCR 链路。 ## 快速启动 diff --git a/apps/backend/scripts/cleanup-dev-data.ts b/apps/backend/scripts/cleanup-dev-data.ts index 0a9cf007..559e2d48 100644 --- a/apps/backend/scripts/cleanup-dev-data.ts +++ b/apps/backend/scripts/cleanup-dev-data.ts @@ -6,8 +6,7 @@ import { closeDb, query } from '../src/db/client.js' import { runtimeConfig } from '../src/config/runtime.js' const RESET_TABLES = [ - 'message_deliveries', - 'tencent_browser_contexts', + 'task_runtime_contexts', 'task_events', 'claim_tokens', 'task_inventory_bindings', diff --git a/apps/backend/src/db/migrations/001_fulfillment_rebuild.sql b/apps/backend/src/db/migrations/001_fulfillment_rebuild.sql index 64bba773..65539ceb 100644 --- a/apps/backend/src/db/migrations/001_fulfillment_rebuild.sql +++ b/apps/backend/src/db/migrations/001_fulfillment_rebuild.sql @@ -206,7 +206,7 @@ CREATE TABLE IF NOT EXISTS task_events ( CREATE INDEX IF NOT EXISTS idx_task_events_task_created_at ON task_events(task_id, created_at DESC); -CREATE TABLE IF NOT EXISTS tencent_browser_contexts ( +CREATE TABLE IF NOT EXISTS task_runtime_contexts ( id BIGSERIAL PRIMARY KEY, task_id BIGINT NOT NULL UNIQUE REFERENCES fulfillment_tasks(id) ON DELETE CASCADE, browser_session_id TEXT NOT NULL DEFAULT '', @@ -223,33 +223,6 @@ CREATE TABLE IF NOT EXISTS tencent_browser_contexts ( updated_at TIMESTAMPTZ NOT NULL ); -CREATE TABLE IF NOT EXISTS message_deliveries ( - id BIGSERIAL PRIMARY KEY, - provider TEXT NOT NULL DEFAULT '', - platform TEXT NOT NULL DEFAULT '', - shop_id TEXT NOT NULL DEFAULT '', - shop_name TEXT NOT NULL DEFAULT '', - channel TEXT NOT NULL, - order_id BIGINT REFERENCES orders(id) ON DELETE SET NULL, - task_id BIGINT REFERENCES fulfillment_tasks(id) ON DELETE SET NULL, - platform_order_id TEXT NOT NULL DEFAULT '', - recipient_key TEXT NOT NULL DEFAULT '', - message_content TEXT NOT NULL DEFAULT '', - claim_url TEXT NOT NULL DEFAULT '', - status TEXT NOT NULL DEFAULT 'pending', - request_url TEXT NOT NULL DEFAULT '', - request_headers_json JSONB NOT NULL DEFAULT '{}'::jsonb, - request_body_json JSONB NOT NULL DEFAULT '{}'::jsonb, - response_status INTEGER NOT NULL DEFAULT 0, - response_json JSONB NOT NULL DEFAULT '{}'::jsonb, - error_message TEXT NOT NULL DEFAULT '', - sent_at TIMESTAMPTZ, - created_at TIMESTAMPTZ NOT NULL, - updated_at TIMESTAMPTZ NOT NULL -); - -CREATE INDEX IF NOT EXISTS idx_message_deliveries_task_channel ON message_deliveries(task_id, channel, created_at DESC); - CREATE TABLE IF NOT EXISTS webhook_events ( id BIGSERIAL PRIMARY KEY, provider TEXT NOT NULL DEFAULT '', diff --git a/apps/backend/src/db/migrations/003_message_delivery_dedupe_index.sql b/apps/backend/src/db/migrations/003_message_delivery_dedupe_index.sql deleted file mode 100644 index a201d627..00000000 --- a/apps/backend/src/db/migrations/003_message_delivery_dedupe_index.sql +++ /dev/null @@ -1,3 +0,0 @@ -CREATE INDEX IF NOT EXISTS idx_message_deliveries_dedupe_success -ON message_deliveries(provider, platform, shop_id, platform_order_id, channel, claim_url, created_at DESC) -WHERE status = 'success'; diff --git a/apps/backend/src/db/migrations/005_drop_message_deliveries.sql b/apps/backend/src/db/migrations/005_drop_message_deliveries.sql new file mode 100644 index 00000000..1a53bdae --- /dev/null +++ b/apps/backend/src/db/migrations/005_drop_message_deliveries.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS message_deliveries; diff --git a/apps/backend/src/db/migrations/006_rename_task_runtime_contexts.sql b/apps/backend/src/db/migrations/006_rename_task_runtime_contexts.sql new file mode 100644 index 00000000..77290219 --- /dev/null +++ b/apps/backend/src/db/migrations/006_rename_task_runtime_contexts.sql @@ -0,0 +1,7 @@ +DO $$ +BEGIN + IF to_regclass('public.tencent_browser_contexts') IS NOT NULL + AND to_regclass('public.task_runtime_contexts') IS NULL THEN + ALTER TABLE tencent_browser_contexts RENAME TO task_runtime_contexts; + END IF; +END $$; diff --git a/apps/backend/src/db/migrations/007_drop_legacy_tencent_profiles.sql b/apps/backend/src/db/migrations/007_drop_legacy_tencent_profiles.sql new file mode 100644 index 00000000..cdefd4d7 --- /dev/null +++ b/apps/backend/src/db/migrations/007_drop_legacy_tencent_profiles.sql @@ -0,0 +1,7 @@ +DELETE FROM fulfillment_profiles +WHERE profile_key IN ('tencent_claim_redeem', 'tencent_claim_assisted') + AND NOT EXISTS ( + SELECT 1 + FROM fulfillment_tasks + WHERE fulfillment_tasks.profile_id = fulfillment_profiles.id + ); diff --git a/apps/backend/src/repositories/message-delivery-repo.ts b/apps/backend/src/repositories/message-delivery-repo.ts deleted file mode 100644 index 0dcd5525..00000000 --- a/apps/backend/src/repositories/message-delivery-repo.ts +++ /dev/null @@ -1,314 +0,0 @@ -import { query } from '../db/client.js' -import type { - MessageDeliveryListQueryResult, - MessageDeliveryRow, -} from '../types/repository-rows.js' - -type MessageDeliveryCreateInput = { - provider: string - platform: string - shopId?: string - shopName?: string - channel: string - orderId?: number | string | null - taskId?: number | string | null - platformOrderId?: string - recipientKey?: string - messageContent?: string - claimUrl?: string - status?: string - requestUrl?: string - requestHeadersJson?: string | Record - requestBodyJson?: string | Record - responseStatus?: number | string - responseJson?: string | Record - errorMessage?: string - sentAt?: string | null - createdAt: string - updatedAt: string -} - -type MessageDeliveryPatch = Partial> - -type LatestSuccessfulMessageDeliveryQuery = { - provider?: string - platform?: string - shopId?: string - platformOrderId?: string - channel?: string - claimUrl?: string -} - -type MessageDeliveryListQuery = { - page?: number - pageSize?: number - provider?: string - platform?: string - status?: string - shopId?: string - platformOrderId?: string - taskNo?: string - dateFrom?: string - dateTo?: string -} - -export async function createMessageDelivery( - input: MessageDeliveryCreateInput, -): Promise { - const result = await query( - ` - INSERT INTO message_deliveries ( - provider, - platform, - shop_id, - shop_name, - channel, - order_id, - task_id, - platform_order_id, - recipient_key, - message_content, - claim_url, - status, - request_url, - request_headers_json, - request_body_json, - response_status, - response_json, - error_message, - sent_at, - created_at, - updated_at - ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14::jsonb, $15::jsonb, $16, $17::jsonb, $18, $19, $20, $21) - RETURNING * - `, - [ - input.provider, - input.platform, - input.shopId || '', - input.shopName || '', - input.channel, - input.orderId || null, - input.taskId || null, - input.platformOrderId || '', - input.recipientKey || '', - input.messageContent || '', - input.claimUrl || '', - input.status || 'pending', - input.requestUrl || '', - input.requestHeadersJson || '{}', - input.requestBodyJson || '{}', - Number(input.responseStatus || 0), - input.responseJson || '{}', - input.errorMessage || '', - input.sentAt || null, - input.createdAt, - input.updatedAt, - ], - ) - - return result.rows[0] || null -} - -export async function getMessageDeliveryById(deliveryId: number | string): Promise { - const result = await query('SELECT * FROM message_deliveries WHERE id = $1 LIMIT 1', [ - Number(deliveryId), - ]) - return result.rows[0] || null -} - -export async function updateMessageDelivery( - deliveryId: number | string, - patch: MessageDeliveryPatch = {}, -): Promise { - const current = await getMessageDeliveryById(deliveryId) - if (!current) { - return null - } - - const next = { ...current, ...patch } - const result = await query( - ` - UPDATE message_deliveries - SET - status = $1, - request_url = $2, - request_headers_json = $3::jsonb, - request_body_json = $4::jsonb, - response_status = $5, - response_json = $6::jsonb, - error_message = $7, - sent_at = $8, - updated_at = $9 - WHERE id = $10 - RETURNING * - `, - [ - next.status, - next.request_url || '', - next.request_headers_json || '{}', - next.request_body_json || '{}', - Number(next.response_status || 0), - next.response_json || '{}', - next.error_message || '', - next.sent_at || null, - next.updated_at, - Number(deliveryId), - ], - ) - - return result.rows[0] || null -} - -export async function findLatestSuccessfulMessageDeliveryByTask( - taskId: number | string, - channel: string, -): Promise { - const result = await query( - ` - SELECT * - FROM message_deliveries - WHERE task_id = $1 AND channel = $2 AND status = 'success' - ORDER BY id DESC - LIMIT 1 - `, - [Number(taskId), channel], - ) - - return result.rows[0] || null -} - -export async function findLatestSuccessfulMessageDelivery({ - provider = '', - platform = '', - shopId = '', - platformOrderId = '', - channel = '', - claimUrl = '', -}: LatestSuccessfulMessageDeliveryQuery = {}): Promise { - const result = await query( - ` - SELECT * - FROM message_deliveries - WHERE provider = $1 - AND platform = $2 - AND shop_id = $3 - AND platform_order_id = $4 - AND channel = $5 - AND claim_url = $6 - AND status = 'success' - ORDER BY id DESC - LIMIT 1 - `, - [ - provider, - platform, - shopId, - platformOrderId, - channel, - claimUrl, - ], - ) - - return result.rows[0] || null -} - -export async function listMessageDeliveries({ - page = 1, - pageSize = 20, - provider = '', - platform = '', - status = '', - shopId = '', - platformOrderId = '', - taskNo = '', - dateFrom = '', - dateTo = '', -}: MessageDeliveryListQuery = {}): Promise { - const offset = (page - 1) * pageSize - const filters: string[] = [] - const params: unknown[] = [] - - if (provider) { - params.push(provider) - filters.push(`md.provider = $${params.length}`) - } - - if (platform) { - params.push(platform) - filters.push(`md.platform = $${params.length}`) - } - - if (status) { - params.push(status) - filters.push(`md.status = $${params.length}`) - } - - if (shopId) { - params.push(shopId) - filters.push(`md.shop_id = $${params.length}`) - } - - if (platformOrderId) { - params.push(`%${platformOrderId}%`) - filters.push(`md.platform_order_id ILIKE $${params.length}`) - } - - if (taskNo) { - params.push(`%${taskNo}%`) - filters.push(`ft.task_no ILIKE $${params.length}`) - } - - if (dateFrom) { - params.push(dateFrom) - filters.push(`md.created_at >= $${params.length}`) - } - - if (dateTo) { - params.push(dateTo) - filters.push(`md.created_at <= $${params.length}`) - } - - const whereClause = filters.length > 0 ? `WHERE ${filters.join(' AND ')}` : '' - const fromClause = ` - FROM message_deliveries md - LEFT JOIN fulfillment_tasks ft ON ft.id = md.task_id - ` - - const totalResult = await query<{ [column: string]: unknown, total: number }>( - `SELECT COUNT(*)::int AS total ${fromClause} ${whereClause}`, - params, - ) - - params.push(pageSize) - params.push(offset) - const itemsResult = await query( - ` - SELECT - md.*, - ft.task_no, - ft.task_status - ${fromClause} - ${whereClause} - ORDER BY md.id DESC - LIMIT $${params.length - 1} OFFSET $${params.length} - `, - params, - ) - - return { - items: itemsResult.rows, - total: Number(totalResult.rows[0]?.total || 0), - } -} diff --git a/apps/backend/src/repositories/order-repo.ts b/apps/backend/src/repositories/order-repo.ts index 66d8944e..d520ec60 100644 --- a/apps/backend/src/repositories/order-repo.ts +++ b/apps/backend/src/repositories/order-repo.ts @@ -25,7 +25,7 @@ type OrderPlatformCandidateLookupInput = { } export async function findOrderByPlatformOrderId({ - provider = 'agiso', + provider = '91kaquan', platform, shopId = '', platformOrderId, @@ -44,7 +44,7 @@ export async function findOrderByPlatformOrderId({ } export async function findOrderByPlatformOrderIdCandidates({ - provider = 'agiso', + provider = '91kaquan', platform, shopIds = [], platformOrderId, diff --git a/apps/backend/src/repositories/task-repo.ts b/apps/backend/src/repositories/task-repo.ts index 687d2e18..2d9ebe5a 100644 --- a/apps/backend/src/repositories/task-repo.ts +++ b/apps/backend/src/repositories/task-repo.ts @@ -4,7 +4,7 @@ import type { PoolClient } from 'pg' import type { TaskCreateInput, TaskListQueryInput, - TaskTencentContextPatch, + TaskRuntimeContextPatch, TaskUpdatePatch, } from '../types/repository-inputs.js' import type { @@ -14,7 +14,7 @@ import type { type TaskQueryExecutor = (text: string, params?: unknown[]) => Promise<{ rows: unknown[] }> -type TencentBrowserContextRow = { +type TaskRuntimeContextRow = { browser_session_id: string login_type: string nickname: string @@ -52,7 +52,7 @@ const TASK_FIELDS = ` const TASK_JOINS = ` FROM fulfillment_tasks ft LEFT JOIN claim_tokens ct ON ct.task_id = ft.id - LEFT JOIN tencent_browser_contexts ctx ON ctx.task_id = ft.id + LEFT JOIN task_runtime_contexts ctx ON ctx.task_id = ft.id LEFT JOIN LATERAL ( SELECT tib.inventory_item_id, @@ -158,8 +158,8 @@ export async function createTask(input: TaskCreateInput): Promise { - const currentResult = await client.query( - 'SELECT * FROM tencent_browser_contexts WHERE task_id = $1 LIMIT 1', + const currentResult = await client.query( + 'SELECT * FROM task_runtime_contexts WHERE task_id = $1 LIMIT 1', [Number(taskId)], ) const current = currentResult.rows[0] || null @@ -362,7 +362,7 @@ async function upsertTencentBrowserContextWithClient( if (!current) { await client.query( ` - INSERT INTO tencent_browser_contexts ( + INSERT INTO task_runtime_contexts ( task_id, browser_session_id, login_type, @@ -399,7 +399,7 @@ async function upsertTencentBrowserContextWithClient( await client.query( ` - UPDATE tencent_browser_contexts + UPDATE task_runtime_contexts SET browser_session_id = $1, login_type = $2, @@ -444,7 +444,7 @@ async function getTaskByIdWithExecutor( return (result.rows[0] as TaskRow | undefined) || null } -function containsTencentPatch(patch: TaskUpdatePatch = {}): boolean { +function containsRuntimeContextPatch(patch: TaskUpdatePatch = {}): boolean { return [ 'browser_session_id', 'login_type', diff --git a/apps/backend/src/routes/admin.ts b/apps/backend/src/routes/admin.ts index 6a8e3e2c..ef34e404 100644 --- a/apps/backend/src/routes/admin.ts +++ b/apps/backend/src/routes/admin.ts @@ -4,7 +4,6 @@ import authRouter from "./admin/auth.js"; import auditLogsRouter from "./admin/audit-logs.js"; import dashboardRouter from "./admin/dashboard.js"; import inventoryRouter from "./admin/inventory.js"; -import messageDeliveriesRouter from "./admin/message-deliveries.js"; import ordersRouter from "./admin/orders.js"; import platformConfigRouter from "./admin/platform-config.js"; import { requireAdminSession } from "./admin/shared.js"; @@ -24,7 +23,6 @@ router.use(platformConfigRouter); router.use(ordersRouter); router.use(tasksRouter); router.use(inventoryRouter); -router.use(messageDeliveriesRouter); router.use(webhookEventsRouter); router.use((req, res) => { diff --git a/apps/backend/src/routes/admin/message-deliveries.ts b/apps/backend/src/routes/admin/message-deliveries.ts deleted file mode 100644 index e818e906..00000000 --- a/apps/backend/src/routes/admin/message-deliveries.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { Router } from 'express' - -import { getAdminMessageDeliveries } from '../../services/admin/admin-message-delivery-service.js' -import { createJsonHandler } from './shared.js' -import type { AdminMessageDeliveryRouteQuery } from '../../types/admin-route-inputs.js' - -const router = Router() - -router.get('/message-deliveries', createJsonHandler( - (req) => getAdminMessageDeliveries(req.query as AdminMessageDeliveryRouteQuery), - { - successMessage: 'ok', - errorMessage: '读取消息发送记录失败', - scope: '[admin/message-deliveries]', - }, -)) - -export default router diff --git a/apps/backend/src/services/admin/admin-message-delivery-service.ts b/apps/backend/src/services/admin/admin-message-delivery-service.ts deleted file mode 100644 index 09013950..00000000 --- a/apps/backend/src/services/admin/admin-message-delivery-service.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { listMessageDeliveries } from '../../repositories/message-delivery-repo.js' -import { normalizeDateQuery, normalizePage, normalizePageSize, safeParseJson } from './admin-query-utils.js' - -import type { AdminMessageDeliveryListQueryInput } from '../../types/admin-read-inputs.js' - -type JsonObject = Record - -export async function getAdminMessageDeliveries(query: AdminMessageDeliveryListQueryInput = {}) { - const page = normalizePage(query.page) - const pageSize = normalizePageSize(query.pageSize) - const { items, total } = await listMessageDeliveries({ - page, - pageSize, - provider: String(query.provider || '').trim(), - platform: String(query.platform || '').trim(), - status: String(query.status || '').trim(), - shopId: String(query.shopId || '').trim(), - platformOrderId: String(query.platformOrderId || '').trim(), - taskNo: String(query.taskNo || '').trim(), - dateFrom: normalizeDateQuery(query.dateFrom), - dateTo: normalizeDateQuery(query.dateTo, true), - }) - - return { - items: items.map(mapAdminMessageDeliveryListItem), - pagination: { page, pageSize, total }, - } -} - -function mapAdminMessageDeliveryListItem(item: JsonObject) { - return { - deliveryId: Number(item.id), - provider: String(item.provider || '').trim(), - platform: String(item.platform || '').trim(), - shopId: String(item.shop_id || '').trim(), - shopName: String(item.shop_name || '').trim(), - channel: String(item.channel || '').trim(), - orderId: item.order_id ? Number(item.order_id) : null, - taskId: item.task_id ? Number(item.task_id) : null, - taskNo: String(item.task_no || '').trim(), - taskStatus: String(item.task_status || '').trim(), - platformOrderId: String(item.platform_order_id || '').trim(), - recipientKey: String(item.recipient_key || '').trim(), - messageContent: String(item.message_content || '').trim(), - claimUrl: String(item.claim_url || '').trim(), - status: String(item.status || '').trim(), - requestUrl: String(item.request_url || '').trim(), - responseStatus: Number(item.response_status || 0), - response: safeParseJson(item.response_json), - errorMessage: String(item.error_message || '').trim(), - sentAt: item.sent_at || null, - createdAt: item.created_at, - updatedAt: item.updated_at, - } -} diff --git a/apps/backend/src/services/admin/admin-order-read-helpers.ts b/apps/backend/src/services/admin/admin-order-read-helpers.ts index f1db9eb9..00d1f055 100644 --- a/apps/backend/src/services/admin/admin-order-read-helpers.ts +++ b/apps/backend/src/services/admin/admin-order-read-helpers.ts @@ -18,7 +18,7 @@ export async function mapAdminOrderListItem(item: OrderListRow): Promise mapAdminTaskSummary(task, getTaskBindingSummary(taskBindingSummaryMap, task.id))), webhookEvents: webhookEvents.map((event) => ({ eventId: event.id, - provider: event.provider || 'agiso', + provider: event.provider || '', platform: event.platform, shopId: event.shop_id || '', shopName: resolveDisplayShopName(event.provider, event.shop_id, event.shop_name), @@ -230,7 +227,7 @@ export async function getAdminTaskDetail( order: order ? { orderId: order.id, - provider: order.provider || 'agiso', + provider: order.provider || '', platform: order.platform, shopId: order.shop_id || '', shopName: order.shop_name || '', @@ -270,7 +267,7 @@ export async function getAdminTaskDetail( artifacts: viewerContext.canViewSensitiveTaskData ? safeParseJson(task.artifacts_json) : {}, screenshotUrl, review: { - required: isAssistedClaimTask(task), + required: false, screenshotCapturedAt: String(taskState.reviewCapturedAt || '').trim() || null, roleId: String(taskState.reviewRoleId || '').trim() || '', roleName: String(taskState.reviewRoleName || '').trim() || '', @@ -302,8 +299,6 @@ export async function getAdminTaskDetail( canReturnKuaishouCloudFulfillment: viewerContext.canManageTaskLifecycle && String(task.executor_key || '').trim() === 'kuaishou_ct_assisted' && ['dispatched_pending_return'].includes(String(task.task_status || '').trim()), - canSupportConfirmRole: canViewerConfirmAssistedRole(task, viewerContext), - canSupportRedeem: canViewerRedeemAssistedTask(task, viewerContext), canViewSensitiveTaskData: viewerContext.canViewSensitiveTaskData, }, } diff --git a/apps/backend/src/services/admin/admin-read-shared-helpers.test.ts b/apps/backend/src/services/admin/admin-read-shared-helpers.test.ts index 1f3047e8..94a82325 100644 --- a/apps/backend/src/services/admin/admin-read-shared-helpers.test.ts +++ b/apps/backend/src/services/admin/admin-read-shared-helpers.test.ts @@ -2,7 +2,6 @@ import test from 'node:test' import assert from 'node:assert/strict' import { - canViewerConfirmAssistedRole, canViewerCloseTask, createAdminViewerContext, resolveAdminTaskScreenshotUrl, @@ -38,18 +37,3 @@ test('canViewerCloseTask allows support to close active task but not redeemed ta assert.equal(canViewerCloseTask({ task_status: 'link_generated' }, viewerContext), true) assert.equal(canViewerCloseTask({ task_status: 'redeemed' }, viewerContext), false) }) - -test('canViewerConfirmAssistedRole blocks support when task inventory group is not bound', () => { - const viewerContext = createAdminViewerContext({ - role: 'support', - allowedInventoryGroups: ['A组'], - }) - - assert.equal(canViewerConfirmAssistedRole({ - task_status: 'claimed', - executor_key: 'tencent_claim_assisted', - context_json: { - inventoryGroupCode: 'B组', - }, - }, viewerContext), false) -}) diff --git a/apps/backend/src/services/admin/admin-read-shared-helpers.ts b/apps/backend/src/services/admin/admin-read-shared-helpers.ts index 6dc5db72..16a67c02 100644 --- a/apps/backend/src/services/admin/admin-read-shared-helpers.ts +++ b/apps/backend/src/services/admin/admin-read-shared-helpers.ts @@ -279,10 +279,6 @@ 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 @@ -298,25 +294,7 @@ export function canRegenerateClaimLinkForViewer(task: TaskLike, viewerContext: A 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()) + return false } export function canViewerAccessTaskInventoryGroup( diff --git a/apps/backend/src/services/admin/platform-config/observed-products.test.ts b/apps/backend/src/services/admin/platform-config/observed-products.test.ts index 8f1a61ce..346ff065 100644 --- a/apps/backend/src/services/admin/platform-config/observed-products.test.ts +++ b/apps/backend/src/services/admin/platform-config/observed-products.test.ts @@ -10,8 +10,8 @@ import { test('mapAdminObservedProductRow normalizes query row fields', () => { assert.deepEqual( mapAdminObservedProductRow({ - provider: ' agiso ', - platform: ' xianyu ', + provider: ' 91kaquan ', + platform: ' kuaishou ', shop_id: ' shop-1 ', shop_name: ' 店铺A ', external_item_id: ' item-1 ', @@ -21,8 +21,8 @@ test('mapAdminObservedProductRow normalizes query row fields', () => { order_item_count: '3', }), { - provider: 'agiso', - platform: 'xianyu', + provider: '91kaquan', + platform: 'kuaishou', shopId: 'shop-1', shopName: '店铺A', externalItemId: 'item-1', @@ -40,8 +40,8 @@ test('listAdminObservedProducts queries rows and maps matched binding summary', const result = await listAdminObservedProducts( [ { - provider: 'agiso', - platform: 'xianyu', + provider: '91kaquan', + platform: 'kuaishou', shopId: 'shop-1', skuCode: 'inner-1', skuName: '内部商品A', @@ -57,8 +57,8 @@ test('listAdminObservedProducts queries rows and maps matched binding summary', return { rows: [ { - provider: 'agiso', - platform: 'xianyu', + provider: '91kaquan', + platform: 'kuaishou', shop_id: 'shop-1', shop_name: '店铺A', external_item_id: '', @@ -77,8 +77,8 @@ test('listAdminObservedProducts queries rows and maps matched binding summary', assert.equal(sqlCalls[0], ADMIN_OBSERVED_PRODUCTS_QUERY) assert.deepEqual(result, [ { - provider: 'agiso', - platform: 'xianyu', + provider: '91kaquan', + platform: 'kuaishou', shopId: 'shop-1', shopName: '店铺A', externalItemId: '', diff --git a/apps/backend/src/services/admin/write/shared.ts b/apps/backend/src/services/admin/write/shared.ts index 281ee355..acf143b5 100644 --- a/apps/backend/src/services/admin/write/shared.ts +++ b/apps/backend/src/services/admin/write/shared.ts @@ -1,21 +1,12 @@ import { buildClaimUrl, createTaskClaimToken } from '../../claim/claim-service.js' -import { createHttpError } from '../../../utils/http.js' import { maskCode as maskCodeValue, maskPhone as maskPhoneValue, } from '../../../utils/masking.js' -import { - canViewerConfirmAssistedRole, - canViewerRedeemAssistedTask, - isAssistedClaimTask, - parseTaskContext, -} from '../admin-read-shared-helpers.js' +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 TaskClaimLink = { token: string expiredAt: string | null @@ -41,33 +32,6 @@ export function resolveTaskInventoryGroupCodes(task: TaskRow): string[] | null { return inventoryGroupCode ? [inventoryGroupCode] : null } -export function ensureViewerCanOperateAssistedTask( - task: TaskRow, - viewerContext: AdminViewerContext, - action: AssistedTaskAction, -): void { - if (!viewerContext.canOperateAssistedTask || !isAssistedClaimTask(task)) { - throw createHttpError('当前账号没有此操作权限', { - statusCode: 403, - errorCode: 'admin_task_assisted_permission_denied', - }) - } - - if (action === 'confirm' && !canViewerConfirmAssistedRole(task, viewerContext)) { - throw createHttpError('当前任务状态还不能确认角色', { - statusCode: 409, - errorCode: 'admin_task_assisted_confirm_not_allowed', - }) - } - - if (action === 'redeem' && !canViewerRedeemAssistedTask(task, viewerContext)) { - throw createHttpError('当前任务状态还不能开始兑换', { - statusCode: 409, - errorCode: 'admin_task_assisted_redeem_not_allowed', - }) - } -} - export function isRecoverableTaskSessionCloseError(error: ErrorLike | null | undefined): boolean { const errorCode = String(error?.errorCode || error?.code || '').trim() return errorCode === 'session_not_found' || errorCode === 'session_closed' diff --git a/apps/backend/src/services/bootstrap/fulfillment-bootstrap-service.ts b/apps/backend/src/services/bootstrap/fulfillment-bootstrap-service.ts index 92f1c83f..3ea42216 100644 --- a/apps/backend/src/services/bootstrap/fulfillment-bootstrap-service.ts +++ b/apps/backend/src/services/bootstrap/fulfillment-bootstrap-service.ts @@ -24,38 +24,6 @@ const CORE_PROFILES = [ inventoryStrategy: 'static_pool', requirements: [], }, - { - profileKey: 'tencent_claim_redeem', - name: '腾讯领取兑换', - executorKey: 'tencent_claim_redeem', - requiresClaim: true, - autoDispatch: false, - inventoryStrategy: 'static_pool', - requirements: [ - { - roleKey: 'primary_code', - credentialType: 'tencent_code', - quantityPerUnit: 1, - isRequired: true, - }, - ], - }, - { - profileKey: 'tencent_claim_assisted', - name: '腾讯领取兑换(半自动+人工)', - executorKey: 'tencent_claim_assisted', - requiresClaim: true, - autoDispatch: false, - inventoryStrategy: 'static_pool', - requirements: [ - { - roleKey: 'primary_code', - credentialType: 'tencent_code', - quantityPerUnit: 1, - isRequired: true, - }, - ], - }, { profileKey: 'kuaishou_ct_assisted', name: '快手 cloud 履约', diff --git a/apps/backend/src/services/order/delivery-task-service.test.ts b/apps/backend/src/services/order/delivery-task-service.test.ts index 5935aeff..5416c5b7 100644 --- a/apps/backend/src/services/order/delivery-task-service.test.ts +++ b/apps/backend/src/services/order/delivery-task-service.test.ts @@ -5,8 +5,8 @@ import { syncDeliveryTasksForOrderWithDeps } from './delivery-task-service.js' const paidOrder = { id: 10, - provider: 'agiso', - platform: 'xianyu', + provider: '91kaquan', + platform: 'kuaishou', shop_id: 'shop-1', shop_name: '测试店铺', platform_order_id: 'P10001', @@ -27,7 +27,7 @@ function createTaskFixture(patch = {}) { id: 30, order_item_id: 20, task_status: 'paid', - executor_key: 'tencent_claim_assisted', + executor_key: 'claim_link_dispatch', requires_claim: true, claim_token: '', primary_claim_token_id: null, diff --git a/apps/backend/src/services/order/order-service.ts b/apps/backend/src/services/order/order-service.ts index 8fc3624c..d8749dab 100644 --- a/apps/backend/src/services/order/order-service.ts +++ b/apps/backend/src/services/order/order-service.ts @@ -48,16 +48,6 @@ type SourceOrderEvent = { type UpsertOrderSourceOptions = { sourceLabel?: string - handleClaimLinkTask?: (input: { - event: SourceOrderEvent - order: OrderRow - task: TaskRow - }) => Promise | null | undefined> -} - -type MessageDeliveryResult = { - taskId: number - [key: string]: unknown } type UpsertOrderIgnoredResult = { @@ -66,7 +56,6 @@ type UpsertOrderIgnoredResult = { order: null orderItems: [] tasks: [] - messageDeliveries: [] } type UpsertOrderResult = { @@ -75,7 +64,6 @@ type UpsertOrderResult = { order: OrderRow orderItems: OrderItemRow[] tasks: TaskRow[] - messageDeliveries: MessageDeliveryResult[] } | UpsertOrderIgnoredResult type OrderStateInput = { @@ -101,7 +89,7 @@ export async function upsertOrderFromWebhook(event: SourceOrderEvent): Promise { const now = nowIso() const exactExisting = await findOrderByPlatformOrderId({ @@ -154,7 +142,6 @@ export async function upsertOrderFromSource( order: null, orderItems: [], tasks: [], - messageDeliveries: [], } } @@ -207,7 +194,6 @@ export async function upsertOrderFromSource( ) const tasks = await syncDeliveryTasksForOrder(order, orderItems) - const messageDeliveries: MessageDeliveryResult[] = [] logWebhook('[order-service]', `${sourceLabel} 订单 upsert 完成`, { orderId: order.id, @@ -221,35 +207,10 @@ export async function upsertOrderFromSource( resolvedSkuCodes: configuredItems.map((item) => item.skuCode), }) - if (handleClaimLinkTask) { - for (const task of tasks) { - const isClaimLinkTask = String(task.task_status || '') === 'link_generated' - || ( - String(task.executor_key || '').trim() === 'kuaishou_ct_assisted' - && ['pending_binding_prepare', 'waiting_binding', 'role_confirmed', 'dispatched_pending_return'].includes(String(task.task_status || '').trim()) - ) - - if (!isClaimLinkTask || !task.primary_claim_token_id) { - continue - } - - const result = await handleClaimLinkTask({ event, order, task }) - if (!result) { - continue - } - - messageDeliveries.push({ - taskId: task.id, - ...result, - }) - } - } - return { order, orderItems, tasks, - messageDeliveries, } } diff --git a/apps/backend/src/types/admin-read-inputs.ts b/apps/backend/src/types/admin-read-inputs.ts index 3914eef9..41e5b3fd 100644 --- a/apps/backend/src/types/admin-read-inputs.ts +++ b/apps/backend/src/types/admin-read-inputs.ts @@ -57,17 +57,4 @@ export type AdminWebhookEventListQueryInput = { dateTo?: string } -export type AdminMessageDeliveryListQueryInput = { - page?: number | string - pageSize?: number | string - provider?: string - platform?: string - status?: string - shopId?: string - platformOrderId?: string - taskNo?: string - dateFrom?: string - dateTo?: string -} - export type AdminEntityIdInput = number | string diff --git a/apps/backend/src/types/admin-route-inputs.ts b/apps/backend/src/types/admin-route-inputs.ts index 114b55e0..a3773f50 100644 --- a/apps/backend/src/types/admin-route-inputs.ts +++ b/apps/backend/src/types/admin-route-inputs.ts @@ -1,7 +1,6 @@ import type { AdminInventoryListQueryInput, AdminInventorySkuSuggestionQueryInput, - AdminMessageDeliveryListQueryInput, AdminOrderListQueryInput, AdminTaskListQueryInput, AdminViewerSessionInput, @@ -35,7 +34,6 @@ import type { export type AdminInventoryRouteQuery = AdminInventoryListQueryInput export type AdminInventorySkuSuggestionRouteQuery = AdminInventorySkuSuggestionQueryInput -export type AdminMessageDeliveryRouteQuery = AdminMessageDeliveryListQueryInput export type AdminOrderRouteQuery = AdminOrderListQueryInput export type AdminTaskRouteQuery = AdminTaskListQueryInput export type AdminRouteAdminSession = AdminViewerSessionInput diff --git a/apps/backend/src/types/repository-inputs.ts b/apps/backend/src/types/repository-inputs.ts index 0cec63d6..24de5735 100644 --- a/apps/backend/src/types/repository-inputs.ts +++ b/apps/backend/src/types/repository-inputs.ts @@ -57,7 +57,7 @@ export type TaskListQueryInput = { dateTo?: string } -export type TaskTencentContextPatch = { +export type TaskRuntimeContextPatch = { browserSessionId?: string loginType?: string nickname?: string @@ -97,7 +97,7 @@ export type TaskCreateInput = { contextJson?: string | Record createdAt: string updatedAt: string - tencentContext?: TaskTencentContextPatch + runtimeContext?: TaskRuntimeContextPatch } export type TaskUpdatePatch = { diff --git a/apps/backend/src/types/repository-rows.ts b/apps/backend/src/types/repository-rows.ts index 9b8ab5ea..136197b0 100644 --- a/apps/backend/src/types/repository-rows.ts +++ b/apps/backend/src/types/repository-rows.ts @@ -128,33 +128,6 @@ export type WebhookEventRow = { created_at: string } -export type MessageDeliveryRow = { - id: number - provider: string - platform: string - shop_id: string - shop_name: string - channel: string - order_id: number | null - task_id: number | null - platform_order_id: string - recipient_key: string - message_content: string - claim_url: string - status: string - request_url: string - request_headers_json: string | Record - request_body_json: string | Record - response_status: number - response_json: string | Record - error_message: string - sent_at: string | null - created_at: string - updated_at: string - task_no?: string - task_status?: string -} - export type TaskInventoryBindingRow = { id: number task_id: number @@ -220,8 +193,3 @@ export type WebhookEventListQueryResult = { items: WebhookEventRow[] total: number } - -export type MessageDeliveryListQueryResult = { - items: MessageDeliveryRow[] - total: number -} diff --git a/apps/frontend/src/router/index.ts b/apps/frontend/src/router/index.ts index 1eead01f..1dd36e31 100644 --- a/apps/frontend/src/router/index.ts +++ b/apps/frontend/src/router/index.ts @@ -62,11 +62,6 @@ const router = createRouter({ meta: { allowedRoles: ['admin', 'operator'] }, component: () => import('@/views/admin/inventory/AdminInventoryView.vue'), }, - { - path: 'message-deliveries', - component: () => - import('@/views/admin/message-deliveries/AdminMessageDeliveriesView.vue'), - }, { path: 'webhook-events', meta: { allowedRoles: ['admin', 'operator'] }, diff --git a/apps/frontend/src/services/admin/index.ts b/apps/frontend/src/services/admin/index.ts index 11902e5f..403adc0f 100644 --- a/apps/frontend/src/services/admin/index.ts +++ b/apps/frontend/src/services/admin/index.ts @@ -6,5 +6,4 @@ export * from './platform-config' export * from './orders' export * from './tasks' export * from './inventory' -export * from './message-deliveries' export * from './webhook-events' diff --git a/apps/frontend/src/services/admin/message-deliveries.ts b/apps/frontend/src/services/admin/message-deliveries.ts deleted file mode 100644 index 60fe8697..00000000 --- a/apps/frontend/src/services/admin/message-deliveries.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { apiGet } from '@/lib/http' -import type { AdminMessageDeliveryListItem, AdminPagination } from '@/types/admin' - -export function fetchAdminMessageDeliveries(params?: Record) { - return apiGet<{ items: AdminMessageDeliveryListItem[]; pagination: AdminPagination }>( - '/api/v1/admin/message-deliveries', - params, - ) -} diff --git a/apps/frontend/src/services/admin/tasks.ts b/apps/frontend/src/services/admin/tasks.ts index 4f56557a..4b22b2f2 100644 --- a/apps/frontend/src/services/admin/tasks.ts +++ b/apps/frontend/src/services/admin/tasks.ts @@ -43,14 +43,6 @@ export function regenerateAdminTaskClaimLink(taskId: number | string) { return apiPost(`/api/v1/admin/tasks/${taskId}/regenerate-claim-link`, {}) } -export function confirmAdminTaskAssistedRole(taskId: number | string) { - return apiPost(`/api/v1/admin/tasks/${taskId}/support-confirm-role`, {}) -} - -export function redeemAdminTaskAssisted(taskId: number | string) { - return apiPost(`/api/v1/admin/tasks/${taskId}/support-redeem`, {}) -} - export function closeAdminTask(taskId: number | string) { return apiPost(`/api/v1/admin/tasks/${taskId}/close`, {}) } diff --git a/apps/frontend/src/styles/admin-list-pages.css b/apps/frontend/src/styles/admin-list-pages.css index 1f61827c..7ae70814 100644 --- a/apps/frontend/src/styles/admin-list-pages.css +++ b/apps/frontend/src/styles/admin-list-pages.css @@ -1,7 +1,7 @@ /* ============================================================ 管理员列表页共享样式 - 引入: AdminOrdersView, AdminTasksView, AdminMessageDeliveriesView, - AdminWebhookEventsView, AdminUsersView, AdminInventoryView + 引入: AdminOrdersView, AdminTasksView, AdminWebhookEventsView, + AdminUsersView, AdminInventoryView 各页面自行覆盖 grid-template-columns 等布局差异 ============================================================ */ diff --git a/apps/frontend/src/types/admin/index.ts b/apps/frontend/src/types/admin/index.ts index 31952edb..ceb818b1 100644 --- a/apps/frontend/src/types/admin/index.ts +++ b/apps/frontend/src/types/admin/index.ts @@ -39,9 +39,6 @@ export type { AdminWebhookReplayResponse, } from './webhook-events' -// Message deliveries types -export type { AdminMessageDeliveryListItem } from './message-deliveries' - // Platform config types export type { AdminNotificationBarkRecipient, diff --git a/apps/frontend/src/types/admin/message-deliveries.ts b/apps/frontend/src/types/admin/message-deliveries.ts deleted file mode 100644 index 893f2441..00000000 --- a/apps/frontend/src/types/admin/message-deliveries.ts +++ /dev/null @@ -1,24 +0,0 @@ -export interface AdminMessageDeliveryListItem { - deliveryId: number - provider: string - platform: string - shopId: string - shopName: string - channel: string - orderId: number | null - taskId: number | null - taskNo: string - taskStatus: string - platformOrderId: string - recipientKey: string - messageContent: string - claimUrl: string - status: string - requestUrl: string - responseStatus: number - response: Record - errorMessage: string - sentAt: string | null - createdAt: string - updatedAt: string -} diff --git a/apps/frontend/src/types/admin/tasks.ts b/apps/frontend/src/types/admin/tasks.ts index a5cb0d56..e041439f 100644 --- a/apps/frontend/src/types/admin/tasks.ts +++ b/apps/frontend/src/types/admin/tasks.ts @@ -48,8 +48,6 @@ export interface AdminTaskOperations { canRefreshKuaishouCloudRoleInfo: boolean canDispatchKuaishouCloudFulfillment: boolean canReturnKuaishouCloudFulfillment: boolean - canSupportConfirmRole: boolean - canSupportRedeem: boolean canViewSensitiveTaskData: boolean } diff --git a/apps/frontend/src/utils/admin-options.ts b/apps/frontend/src/utils/admin-options.ts index 4da0f0a9..42f21da5 100644 --- a/apps/frontend/src/utils/admin-options.ts +++ b/apps/frontend/src/utils/admin-options.ts @@ -41,13 +41,6 @@ export const adminInventoryCredentialTypeOptions = [ { label: '纯文本凭据', value: 'text_credential' }, ] -export const adminMessageDeliveryStatusOptions = [ - { label: '全部状态', value: '' }, - { label: '发送成功', value: 'success' }, - { label: '发送失败', value: 'failed' }, - { label: '等待发送', value: 'pending' }, -] - export const adminWebhookProcessedOptions = [ { label: '全部', value: '' }, { label: '处理成功', value: '1' }, diff --git a/apps/frontend/src/views/admin/inventory/composables/useAdminInventory.ts b/apps/frontend/src/views/admin/inventory/composables/useAdminInventory.ts index 63a2f699..f126cd60 100644 --- a/apps/frontend/src/views/admin/inventory/composables/useAdminInventory.ts +++ b/apps/frontend/src/views/admin/inventory/composables/useAdminInventory.ts @@ -148,7 +148,7 @@ export function useAdminInventory() { function getCredentialTypeHint(value: string) { const normalized = String(value || '').trim() if (normalized === 'tencent_code') - return '当前腾讯自动领取链路会优先预占这一类兑换码,三角洲行动这类场景就使用这个类型。内部 SKU 支持中文、英文和数字混用。' + return '当前领取链路会优先预占这一类兑换码,快手云履约等场景可按内部 SKU 统一匹配。' if (normalized === 'card_password') return '适合卡号 / 卡密一体的库存项。' if (normalized === 'account_password') return '适合账号密码、账号令牌等组合凭据。' if (normalized === 'activation_link') return '适合直接发链接的激活类库存。' diff --git a/apps/frontend/src/views/admin/layout/AdminLayout.vue b/apps/frontend/src/views/admin/layout/AdminLayout.vue index 211db269..aac897da 100644 --- a/apps/frontend/src/views/admin/layout/AdminLayout.vue +++ b/apps/frontend/src/views/admin/layout/AdminLayout.vue @@ -10,7 +10,6 @@ import { Expand, Fold, List, - Message, Setting, SwitchButton, Tickets, @@ -62,8 +61,6 @@ const navItems = computed(() => { items.push({ to: '/admin/inventory', label: '库存', icon: Box }) } - items.push({ to: '/admin/message-deliveries', label: '消息发送', icon: Message }) - if (isAdmin.value || isOperator.value) { items.push({ to: '/admin/webhook-events', label: 'Webhook', icon: Connection }) } diff --git a/apps/frontend/src/views/admin/message-deliveries/AdminMessageDeliveriesView.vue b/apps/frontend/src/views/admin/message-deliveries/AdminMessageDeliveriesView.vue deleted file mode 100644 index 5fab8e50..00000000 --- a/apps/frontend/src/views/admin/message-deliveries/AdminMessageDeliveriesView.vue +++ /dev/null @@ -1,281 +0,0 @@ - - - - - diff --git a/apps/frontend/src/views/admin/tasks/AdminTaskDetailView.vue b/apps/frontend/src/views/admin/tasks/AdminTaskDetailView.vue index 52777acf..ac5d65b6 100644 --- a/apps/frontend/src/views/admin/tasks/AdminTaskDetailView.vue +++ b/apps/frontend/src/views/admin/tasks/AdminTaskDetailView.vue @@ -3,9 +3,7 @@ import { onMounted } from 'vue' import { closeAdminTask, - confirmAdminTaskAssistedRole, markAdminTaskManualReview, - redeemAdminTaskAssisted, regenerateAdminTaskClaimLink, releaseAdminTaskInventoryBinding, retryAdminTask, @@ -94,20 +92,6 @@ onMounted(loadDetail) `确认重新生成任务 ${detail!.task.taskNo} 的领取链接吗?旧链接会失效。`, ) " - @confirm-role=" - runAction( - () => confirmAdminTaskAssistedRole(detail!.task.taskId), - '角色已确认', - `确认以客服身份锁定任务 ${detail!.task.taskNo} 当前识别到的角色信息吗?`, - ) - " - @support-redeem=" - runAction( - () => redeemAdminTaskAssisted(detail!.task.taskId), - '兑换任务已启动', - `确认开始执行任务 ${detail!.task.taskNo} 的自动兑换吗?`, - ) - " @prepare-kuaishou-cloud="submitKuaishouCloudPrepare" @dispatch-kuaishou-cloud="submitKuaishouCloudDispatch" @return-kuaishou-cloud="submitKuaishouCloudReturnNumber" diff --git a/apps/frontend/src/views/admin/tasks/components/AdminTaskActionBar.vue b/apps/frontend/src/views/admin/tasks/components/AdminTaskActionBar.vue index 14cc81a9..9fe017bf 100644 --- a/apps/frontend/src/views/admin/tasks/components/AdminTaskActionBar.vue +++ b/apps/frontend/src/views/admin/tasks/components/AdminTaskActionBar.vue @@ -13,8 +13,6 @@ defineProps() const emit = defineEmits<{ retry: [] regenerateClaimLink: [] - confirmRole: [] - supportRedeem: [] prepareKuaishouCloud: [] dispatchKuaishouCloud: [] returnKuaishouCloud: [] @@ -45,24 +43,6 @@ const emit = defineEmits<{ > 重发链接 - - 客服确认角色 - - - 客服开始兑换 - {