清理旧消息和历史命名

This commit is contained in:
yml
2026-05-25 22:03:07 +08:00
parent e7aa194dde
commit 0caa38e690
41 changed files with 63 additions and 1044 deletions
+1 -1
View File
@@ -2,7 +2,7 @@
快手轻量后端服务。 快手轻量后端服务。
后端负责订单入库、履约任务编排、快手 Cloud / 91 卡券 / 快手小店核销配置、后台管理接口和数据库迁移。默认入口就是轻量后端,不再包含浏览器会话OCR 或旧咸鱼自动发货链路。 后端负责订单入库、履约任务编排、快手 Cloud / 91 卡券 / 快手小店核销配置、后台管理接口和数据库迁移。默认入口就是轻量后端,不再包含浏览器会话OCR 链路。
## 快速启动 ## 快速启动
+1 -2
View File
@@ -6,8 +6,7 @@ import { closeDb, query } from '../src/db/client.js'
import { runtimeConfig } from '../src/config/runtime.js' import { runtimeConfig } from '../src/config/runtime.js'
const RESET_TABLES = [ const RESET_TABLES = [
'message_deliveries', 'task_runtime_contexts',
'tencent_browser_contexts',
'task_events', 'task_events',
'claim_tokens', 'claim_tokens',
'task_inventory_bindings', 'task_inventory_bindings',
@@ -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 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, id BIGSERIAL PRIMARY KEY,
task_id BIGINT NOT NULL UNIQUE REFERENCES fulfillment_tasks(id) ON DELETE CASCADE, task_id BIGINT NOT NULL UNIQUE REFERENCES fulfillment_tasks(id) ON DELETE CASCADE,
browser_session_id TEXT NOT NULL DEFAULT '', browser_session_id TEXT NOT NULL DEFAULT '',
@@ -223,33 +223,6 @@ CREATE TABLE IF NOT EXISTS tencent_browser_contexts (
updated_at TIMESTAMPTZ NOT NULL 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 ( CREATE TABLE IF NOT EXISTS webhook_events (
id BIGSERIAL PRIMARY KEY, id BIGSERIAL PRIMARY KEY,
provider TEXT NOT NULL DEFAULT '', provider TEXT NOT NULL DEFAULT '',
@@ -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';
@@ -0,0 +1 @@
DROP TABLE IF EXISTS message_deliveries;
@@ -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 $$;
@@ -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
);
@@ -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<string, unknown>
requestBodyJson?: string | Record<string, unknown>
responseStatus?: number | string
responseJson?: string | Record<string, unknown>
errorMessage?: string
sentAt?: string | null
createdAt: string
updatedAt: string
}
type MessageDeliveryPatch = Partial<Pick<
MessageDeliveryRow,
| 'status'
| 'request_url'
| 'request_headers_json'
| 'request_body_json'
| 'response_status'
| 'response_json'
| 'error_message'
| 'sent_at'
| 'updated_at'
>>
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<MessageDeliveryRow | null> {
const result = await query<MessageDeliveryRow>(
`
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<MessageDeliveryRow | null> {
const result = await query<MessageDeliveryRow>('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<MessageDeliveryRow | null> {
const current = await getMessageDeliveryById(deliveryId)
if (!current) {
return null
}
const next = { ...current, ...patch }
const result = await query<MessageDeliveryRow>(
`
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<MessageDeliveryRow | null> {
const result = await query<MessageDeliveryRow>(
`
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<MessageDeliveryRow | null> {
const result = await query<MessageDeliveryRow>(
`
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<MessageDeliveryListQueryResult> {
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<MessageDeliveryRow>(
`
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),
}
}
+2 -2
View File
@@ -25,7 +25,7 @@ type OrderPlatformCandidateLookupInput = {
} }
export async function findOrderByPlatformOrderId({ export async function findOrderByPlatformOrderId({
provider = 'agiso', provider = '91kaquan',
platform, platform,
shopId = '', shopId = '',
platformOrderId, platformOrderId,
@@ -44,7 +44,7 @@ export async function findOrderByPlatformOrderId({
} }
export async function findOrderByPlatformOrderIdCandidates({ export async function findOrderByPlatformOrderIdCandidates({
provider = 'agiso', provider = '91kaquan',
platform, platform,
shopIds = [], shopIds = [],
platformOrderId, platformOrderId,
+15 -15
View File
@@ -4,7 +4,7 @@ import type { PoolClient } from 'pg'
import type { import type {
TaskCreateInput, TaskCreateInput,
TaskListQueryInput, TaskListQueryInput,
TaskTencentContextPatch, TaskRuntimeContextPatch,
TaskUpdatePatch, TaskUpdatePatch,
} from '../types/repository-inputs.js' } from '../types/repository-inputs.js'
import type { import type {
@@ -14,7 +14,7 @@ import type {
type TaskQueryExecutor = (text: string, params?: unknown[]) => Promise<{ rows: unknown[] }> type TaskQueryExecutor = (text: string, params?: unknown[]) => Promise<{ rows: unknown[] }>
type TencentBrowserContextRow = { type TaskRuntimeContextRow = {
browser_session_id: string browser_session_id: string
login_type: string login_type: string
nickname: string nickname: string
@@ -52,7 +52,7 @@ const TASK_FIELDS = `
const TASK_JOINS = ` const TASK_JOINS = `
FROM fulfillment_tasks ft FROM fulfillment_tasks ft
LEFT JOIN claim_tokens ct ON ct.task_id = ft.id 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 ( LEFT JOIN LATERAL (
SELECT SELECT
tib.inventory_item_id, tib.inventory_item_id,
@@ -158,8 +158,8 @@ export async function createTask(input: TaskCreateInput): Promise<TaskRow | null
) )
const taskId = Number(taskResult.rows[0]?.id || 0) const taskId = Number(taskResult.rows[0]?.id || 0)
if (input.tencentContext) { if (input.runtimeContext) {
await upsertTencentBrowserContextWithClient(client, taskId, input.tencentContext, input.createdAt) await upsertTaskRuntimeContextWithClient(client, taskId, input.runtimeContext, input.createdAt)
} }
return getTaskByIdWithExecutor(client.query.bind(client) as TaskQueryExecutor, taskId) return getTaskByIdWithExecutor(client.query.bind(client) as TaskQueryExecutor, taskId)
@@ -220,8 +220,8 @@ export async function updateTask(taskId: number | string, patch: TaskUpdatePatch
], ],
) )
if (containsTencentPatch(patch)) { if (containsRuntimeContextPatch(patch)) {
await upsertTencentBrowserContextWithClient(client, Number(taskId), { await upsertTaskRuntimeContextWithClient(client, Number(taskId), {
browserSessionId: patch.browser_session_id, browserSessionId: patch.browser_session_id,
loginType: patch.login_type, loginType: patch.login_type,
nickname: patch.nickname, nickname: patch.nickname,
@@ -310,7 +310,7 @@ export async function listTasks({
SELECT COUNT(*)::int AS total SELECT COUNT(*)::int AS total
FROM fulfillment_tasks ft FROM fulfillment_tasks ft
LEFT JOIN order_items oi ON oi.id = ft.order_item_id LEFT JOIN order_items oi ON oi.id = ft.order_item_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
${whereClause} ${whereClause}
`, `,
params, params,
@@ -333,14 +333,14 @@ export async function listTasks({
} }
} }
async function upsertTencentBrowserContextWithClient( async function upsertTaskRuntimeContextWithClient(
client: PoolClient, client: PoolClient,
taskId: number | string, taskId: number | string,
patch: TaskTencentContextPatch = {}, patch: TaskRuntimeContextPatch = {},
timestamp: string | undefined, timestamp: string | undefined,
): Promise<void> { ): Promise<void> {
const currentResult = await client.query<TencentBrowserContextRow>( const currentResult = await client.query<TaskRuntimeContextRow>(
'SELECT * FROM tencent_browser_contexts WHERE task_id = $1 LIMIT 1', 'SELECT * FROM task_runtime_contexts WHERE task_id = $1 LIMIT 1',
[Number(taskId)], [Number(taskId)],
) )
const current = currentResult.rows[0] || null const current = currentResult.rows[0] || null
@@ -362,7 +362,7 @@ async function upsertTencentBrowserContextWithClient(
if (!current) { if (!current) {
await client.query( await client.query(
` `
INSERT INTO tencent_browser_contexts ( INSERT INTO task_runtime_contexts (
task_id, task_id,
browser_session_id, browser_session_id,
login_type, login_type,
@@ -399,7 +399,7 @@ async function upsertTencentBrowserContextWithClient(
await client.query( await client.query(
` `
UPDATE tencent_browser_contexts UPDATE task_runtime_contexts
SET SET
browser_session_id = $1, browser_session_id = $1,
login_type = $2, login_type = $2,
@@ -444,7 +444,7 @@ async function getTaskByIdWithExecutor(
return (result.rows[0] as TaskRow | undefined) || null return (result.rows[0] as TaskRow | undefined) || null
} }
function containsTencentPatch(patch: TaskUpdatePatch = {}): boolean { function containsRuntimeContextPatch(patch: TaskUpdatePatch = {}): boolean {
return [ return [
'browser_session_id', 'browser_session_id',
'login_type', 'login_type',
-2
View File
@@ -4,7 +4,6 @@ import authRouter from "./admin/auth.js";
import auditLogsRouter from "./admin/audit-logs.js"; import auditLogsRouter from "./admin/audit-logs.js";
import dashboardRouter from "./admin/dashboard.js"; import dashboardRouter from "./admin/dashboard.js";
import inventoryRouter from "./admin/inventory.js"; import inventoryRouter from "./admin/inventory.js";
import messageDeliveriesRouter from "./admin/message-deliveries.js";
import ordersRouter from "./admin/orders.js"; import ordersRouter from "./admin/orders.js";
import platformConfigRouter from "./admin/platform-config.js"; import platformConfigRouter from "./admin/platform-config.js";
import { requireAdminSession } from "./admin/shared.js"; import { requireAdminSession } from "./admin/shared.js";
@@ -24,7 +23,6 @@ router.use(platformConfigRouter);
router.use(ordersRouter); router.use(ordersRouter);
router.use(tasksRouter); router.use(tasksRouter);
router.use(inventoryRouter); router.use(inventoryRouter);
router.use(messageDeliveriesRouter);
router.use(webhookEventsRouter); router.use(webhookEventsRouter);
router.use((req, res) => { router.use((req, res) => {
@@ -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
@@ -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<string, any>
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,
}
}
@@ -18,7 +18,7 @@ export async function mapAdminOrderListItem(item: OrderListRow): Promise<AdminOr
return { return {
orderId: item.id, orderId: item.id,
provider: item.provider || 'agiso', provider: item.provider || '',
platform: item.platform, platform: item.platform,
shopId: item.shop_id || '', shopId: item.shop_id || '',
shopName: resolveDisplayShopName(item.provider, item.shop_id, item.shop_name), shopName: resolveDisplayShopName(item.provider, item.shop_id, item.shop_name),
@@ -17,12 +17,9 @@ import { normalizeDateQuery, normalizePage, normalizePageSize, safeParseJson } f
import { import {
canViewerCloseTask, canViewerCloseTask,
canRegenerateClaimLinkForViewer, canRegenerateClaimLinkForViewer,
canViewerConfirmAssistedRole,
canViewerRedeemAssistedTask,
createAdminViewerContext, createAdminViewerContext,
getTaskPrimaryClaimTokenId, getTaskPrimaryClaimTokenId,
getTaskPrimaryInventoryItemId, getTaskPrimaryInventoryItemId,
isAssistedClaimTask,
isManualDispatchTask, isManualDispatchTask,
mapKuaishouCloudFulfillmentContext, mapKuaishouCloudFulfillmentContext,
mapManualDispatchContext, mapManualDispatchContext,
@@ -108,7 +105,7 @@ export async function getAdminOrderDetail(orderId: AdminEntityIdInput): Promise<
return { return {
order: { order: {
orderId: order.id, orderId: order.id,
provider: order.provider || 'agiso', provider: order.provider || '',
platform: order.platform, platform: order.platform,
shopId: order.shop_id || '', shopId: order.shop_id || '',
shopName: resolveDisplayShopName(order.provider, order.shop_id, order.shop_name), shopName: resolveDisplayShopName(order.provider, order.shop_id, order.shop_name),
@@ -144,7 +141,7 @@ export async function getAdminOrderDetail(orderId: AdminEntityIdInput): Promise<
tasks: tasks.map((task) => mapAdminTaskSummary(task, getTaskBindingSummary(taskBindingSummaryMap, task.id))), tasks: tasks.map((task) => mapAdminTaskSummary(task, getTaskBindingSummary(taskBindingSummaryMap, task.id))),
webhookEvents: webhookEvents.map((event) => ({ webhookEvents: webhookEvents.map((event) => ({
eventId: event.id, eventId: event.id,
provider: event.provider || 'agiso', provider: event.provider || '',
platform: event.platform, platform: event.platform,
shopId: event.shop_id || '', shopId: event.shop_id || '',
shopName: resolveDisplayShopName(event.provider, event.shop_id, event.shop_name), shopName: resolveDisplayShopName(event.provider, event.shop_id, event.shop_name),
@@ -230,7 +227,7 @@ export async function getAdminTaskDetail(
order: order order: order
? { ? {
orderId: order.id, orderId: order.id,
provider: order.provider || 'agiso', provider: order.provider || '',
platform: order.platform, platform: order.platform,
shopId: order.shop_id || '', shopId: order.shop_id || '',
shopName: order.shop_name || '', shopName: order.shop_name || '',
@@ -270,7 +267,7 @@ export async function getAdminTaskDetail(
artifacts: viewerContext.canViewSensitiveTaskData ? safeParseJson(task.artifacts_json) : {}, artifacts: viewerContext.canViewSensitiveTaskData ? safeParseJson(task.artifacts_json) : {},
screenshotUrl, screenshotUrl,
review: { review: {
required: isAssistedClaimTask(task), required: false,
screenshotCapturedAt: String(taskState.reviewCapturedAt || '').trim() || null, screenshotCapturedAt: String(taskState.reviewCapturedAt || '').trim() || null,
roleId: String(taskState.reviewRoleId || '').trim() || '', roleId: String(taskState.reviewRoleId || '').trim() || '',
roleName: String(taskState.reviewRoleName || '').trim() || '', roleName: String(taskState.reviewRoleName || '').trim() || '',
@@ -302,8 +299,6 @@ export async function getAdminTaskDetail(
canReturnKuaishouCloudFulfillment: viewerContext.canManageTaskLifecycle canReturnKuaishouCloudFulfillment: viewerContext.canManageTaskLifecycle
&& String(task.executor_key || '').trim() === 'kuaishou_ct_assisted' && String(task.executor_key || '').trim() === 'kuaishou_ct_assisted'
&& ['dispatched_pending_return'].includes(String(task.task_status || '').trim()), && ['dispatched_pending_return'].includes(String(task.task_status || '').trim()),
canSupportConfirmRole: canViewerConfirmAssistedRole(task, viewerContext),
canSupportRedeem: canViewerRedeemAssistedTask(task, viewerContext),
canViewSensitiveTaskData: viewerContext.canViewSensitiveTaskData, canViewSensitiveTaskData: viewerContext.canViewSensitiveTaskData,
}, },
} }
@@ -2,7 +2,6 @@ import test from 'node:test'
import assert from 'node:assert/strict' import assert from 'node:assert/strict'
import { import {
canViewerConfirmAssistedRole,
canViewerCloseTask, canViewerCloseTask,
createAdminViewerContext, createAdminViewerContext,
resolveAdminTaskScreenshotUrl, 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: 'link_generated' }, viewerContext), true)
assert.equal(canViewerCloseTask({ task_status: 'redeemed' }, viewerContext), false) 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)
})
@@ -279,10 +279,6 @@ 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: TaskLike | null | undefined): boolean {
return String(task?.executor_key || '').trim() === 'tencent_claim_assisted'
}
export function canRegenerateClaimLinkForViewer(task: TaskLike, viewerContext: AdminViewerContext): boolean { export function canRegenerateClaimLinkForViewer(task: TaskLike, viewerContext: AdminViewerContext): boolean {
if (isManualDispatchTask(task)) { if (isManualDispatchTask(task)) {
return false return false
@@ -298,25 +294,7 @@ export function canRegenerateClaimLinkForViewer(task: TaskLike, viewerContext: A
return true return true
} }
return viewerContext.canOperateAssistedTask return false
&& 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( export function canViewerAccessTaskInventoryGroup(
@@ -10,8 +10,8 @@ import {
test('mapAdminObservedProductRow normalizes query row fields', () => { test('mapAdminObservedProductRow normalizes query row fields', () => {
assert.deepEqual( assert.deepEqual(
mapAdminObservedProductRow({ mapAdminObservedProductRow({
provider: ' agiso ', provider: ' 91kaquan ',
platform: ' xianyu ', platform: ' kuaishou ',
shop_id: ' shop-1 ', shop_id: ' shop-1 ',
shop_name: ' 店铺A ', shop_name: ' 店铺A ',
external_item_id: ' item-1 ', external_item_id: ' item-1 ',
@@ -21,8 +21,8 @@ test('mapAdminObservedProductRow normalizes query row fields', () => {
order_item_count: '3', order_item_count: '3',
}), }),
{ {
provider: 'agiso', provider: '91kaquan',
platform: 'xianyu', platform: 'kuaishou',
shopId: 'shop-1', shopId: 'shop-1',
shopName: '店铺A', shopName: '店铺A',
externalItemId: 'item-1', externalItemId: 'item-1',
@@ -40,8 +40,8 @@ test('listAdminObservedProducts queries rows and maps matched binding summary',
const result = await listAdminObservedProducts( const result = await listAdminObservedProducts(
[ [
{ {
provider: 'agiso', provider: '91kaquan',
platform: 'xianyu', platform: 'kuaishou',
shopId: 'shop-1', shopId: 'shop-1',
skuCode: 'inner-1', skuCode: 'inner-1',
skuName: '内部商品A', skuName: '内部商品A',
@@ -57,8 +57,8 @@ test('listAdminObservedProducts queries rows and maps matched binding summary',
return { return {
rows: [ rows: [
{ {
provider: 'agiso', provider: '91kaquan',
platform: 'xianyu', platform: 'kuaishou',
shop_id: 'shop-1', shop_id: 'shop-1',
shop_name: '店铺A', shop_name: '店铺A',
external_item_id: '', 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.equal(sqlCalls[0], ADMIN_OBSERVED_PRODUCTS_QUERY)
assert.deepEqual(result, [ assert.deepEqual(result, [
{ {
provider: 'agiso', provider: '91kaquan',
platform: 'xianyu', platform: 'kuaishou',
shopId: 'shop-1', shopId: 'shop-1',
shopName: '店铺A', shopName: '店铺A',
externalItemId: '', externalItemId: '',
@@ -1,21 +1,12 @@
import { buildClaimUrl, createTaskClaimToken } from '../../claim/claim-service.js' import { buildClaimUrl, createTaskClaimToken } from '../../claim/claim-service.js'
import { createHttpError } from '../../../utils/http.js'
import { import {
maskCode as maskCodeValue, maskCode as maskCodeValue,
maskPhone as maskPhoneValue, maskPhone as maskPhoneValue,
} from '../../../utils/masking.js' } from '../../../utils/masking.js'
import { import { parseTaskContext } from '../admin-read-shared-helpers.js'
canViewerConfirmAssistedRole,
canViewerRedeemAssistedTask,
isAssistedClaimTask,
parseTaskContext,
} 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 TaskClaimLink = { type TaskClaimLink = {
token: string token: string
expiredAt: string | null expiredAt: string | null
@@ -41,33 +32,6 @@ export function resolveTaskInventoryGroupCodes(task: TaskRow): string[] | null {
return inventoryGroupCode ? [inventoryGroupCode] : 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 { export function isRecoverableTaskSessionCloseError(error: ErrorLike | null | undefined): boolean {
const errorCode = String(error?.errorCode || error?.code || '').trim() const errorCode = String(error?.errorCode || error?.code || '').trim()
return errorCode === 'session_not_found' || errorCode === 'session_closed' return errorCode === 'session_not_found' || errorCode === 'session_closed'
@@ -24,38 +24,6 @@ const CORE_PROFILES = [
inventoryStrategy: 'static_pool', inventoryStrategy: 'static_pool',
requirements: [], 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', profileKey: 'kuaishou_ct_assisted',
name: '快手 cloud 履约', name: '快手 cloud 履约',
@@ -5,8 +5,8 @@ import { syncDeliveryTasksForOrderWithDeps } from './delivery-task-service.js'
const paidOrder = { const paidOrder = {
id: 10, id: 10,
provider: 'agiso', provider: '91kaquan',
platform: 'xianyu', platform: 'kuaishou',
shop_id: 'shop-1', shop_id: 'shop-1',
shop_name: '测试店铺', shop_name: '测试店铺',
platform_order_id: 'P10001', platform_order_id: 'P10001',
@@ -27,7 +27,7 @@ function createTaskFixture(patch = {}) {
id: 30, id: 30,
order_item_id: 20, order_item_id: 20,
task_status: 'paid', task_status: 'paid',
executor_key: 'tencent_claim_assisted', executor_key: 'claim_link_dispatch',
requires_claim: true, requires_claim: true,
claim_token: '', claim_token: '',
primary_claim_token_id: null, primary_claim_token_id: null,
@@ -48,16 +48,6 @@ type SourceOrderEvent = {
type UpsertOrderSourceOptions = { type UpsertOrderSourceOptions = {
sourceLabel?: string sourceLabel?: string
handleClaimLinkTask?: (input: {
event: SourceOrderEvent
order: OrderRow
task: TaskRow
}) => Promise<Record<string, unknown> | null | undefined>
}
type MessageDeliveryResult = {
taskId: number
[key: string]: unknown
} }
type UpsertOrderIgnoredResult = { type UpsertOrderIgnoredResult = {
@@ -66,7 +56,6 @@ type UpsertOrderIgnoredResult = {
order: null order: null
orderItems: [] orderItems: []
tasks: [] tasks: []
messageDeliveries: []
} }
type UpsertOrderResult = { type UpsertOrderResult = {
@@ -75,7 +64,6 @@ type UpsertOrderResult = {
order: OrderRow order: OrderRow
orderItems: OrderItemRow[] orderItems: OrderItemRow[]
tasks: TaskRow[] tasks: TaskRow[]
messageDeliveries: MessageDeliveryResult[]
} | UpsertOrderIgnoredResult } | UpsertOrderIgnoredResult
type OrderStateInput = { type OrderStateInput = {
@@ -101,7 +89,7 @@ export async function upsertOrderFromWebhook(event: SourceOrderEvent): Promise<U
export async function upsertOrderFromSource( export async function upsertOrderFromSource(
event: SourceOrderEvent, event: SourceOrderEvent,
{ sourceLabel = 'source', handleClaimLinkTask }: UpsertOrderSourceOptions = {}, { sourceLabel = 'source' }: UpsertOrderSourceOptions = {},
): Promise<UpsertOrderResult> { ): Promise<UpsertOrderResult> {
const now = nowIso() const now = nowIso()
const exactExisting = await findOrderByPlatformOrderId({ const exactExisting = await findOrderByPlatformOrderId({
@@ -154,7 +142,6 @@ export async function upsertOrderFromSource(
order: null, order: null,
orderItems: [], orderItems: [],
tasks: [], tasks: [],
messageDeliveries: [],
} }
} }
@@ -207,7 +194,6 @@ export async function upsertOrderFromSource(
) )
const tasks = await syncDeliveryTasksForOrder(order, orderItems) const tasks = await syncDeliveryTasksForOrder(order, orderItems)
const messageDeliveries: MessageDeliveryResult[] = []
logWebhook('[order-service]', `${sourceLabel} 订单 upsert 完成`, { logWebhook('[order-service]', `${sourceLabel} 订单 upsert 完成`, {
orderId: order.id, orderId: order.id,
@@ -221,35 +207,10 @@ export async function upsertOrderFromSource(
resolvedSkuCodes: configuredItems.map((item) => item.skuCode), 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 { return {
order, order,
orderItems, orderItems,
tasks, tasks,
messageDeliveries,
} }
} }
@@ -57,17 +57,4 @@ export type AdminWebhookEventListQueryInput = {
dateTo?: string 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 export type AdminEntityIdInput = number | string
@@ -1,7 +1,6 @@
import type { import type {
AdminInventoryListQueryInput, AdminInventoryListQueryInput,
AdminInventorySkuSuggestionQueryInput, AdminInventorySkuSuggestionQueryInput,
AdminMessageDeliveryListQueryInput,
AdminOrderListQueryInput, AdminOrderListQueryInput,
AdminTaskListQueryInput, AdminTaskListQueryInput,
AdminViewerSessionInput, AdminViewerSessionInput,
@@ -35,7 +34,6 @@ import type {
export type AdminInventoryRouteQuery = AdminInventoryListQueryInput export type AdminInventoryRouteQuery = AdminInventoryListQueryInput
export type AdminInventorySkuSuggestionRouteQuery = AdminInventorySkuSuggestionQueryInput export type AdminInventorySkuSuggestionRouteQuery = AdminInventorySkuSuggestionQueryInput
export type AdminMessageDeliveryRouteQuery = AdminMessageDeliveryListQueryInput
export type AdminOrderRouteQuery = AdminOrderListQueryInput export type AdminOrderRouteQuery = AdminOrderListQueryInput
export type AdminTaskRouteQuery = AdminTaskListQueryInput export type AdminTaskRouteQuery = AdminTaskListQueryInput
export type AdminRouteAdminSession = AdminViewerSessionInput export type AdminRouteAdminSession = AdminViewerSessionInput
+2 -2
View File
@@ -57,7 +57,7 @@ export type TaskListQueryInput = {
dateTo?: string dateTo?: string
} }
export type TaskTencentContextPatch = { export type TaskRuntimeContextPatch = {
browserSessionId?: string browserSessionId?: string
loginType?: string loginType?: string
nickname?: string nickname?: string
@@ -97,7 +97,7 @@ export type TaskCreateInput = {
contextJson?: string | Record<string, unknown> contextJson?: string | Record<string, unknown>
createdAt: string createdAt: string
updatedAt: string updatedAt: string
tencentContext?: TaskTencentContextPatch runtimeContext?: TaskRuntimeContextPatch
} }
export type TaskUpdatePatch = { export type TaskUpdatePatch = {
-32
View File
@@ -128,33 +128,6 @@ export type WebhookEventRow = {
created_at: string 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<string, unknown>
request_body_json: string | Record<string, unknown>
response_status: number
response_json: string | Record<string, unknown>
error_message: string
sent_at: string | null
created_at: string
updated_at: string
task_no?: string
task_status?: string
}
export type TaskInventoryBindingRow = { export type TaskInventoryBindingRow = {
id: number id: number
task_id: number task_id: number
@@ -220,8 +193,3 @@ export type WebhookEventListQueryResult = {
items: WebhookEventRow[] items: WebhookEventRow[]
total: number total: number
} }
export type MessageDeliveryListQueryResult = {
items: MessageDeliveryRow[]
total: number
}
-5
View File
@@ -62,11 +62,6 @@ const router = createRouter({
meta: { allowedRoles: ['admin', 'operator'] }, meta: { allowedRoles: ['admin', 'operator'] },
component: () => import('@/views/admin/inventory/AdminInventoryView.vue'), component: () => import('@/views/admin/inventory/AdminInventoryView.vue'),
}, },
{
path: 'message-deliveries',
component: () =>
import('@/views/admin/message-deliveries/AdminMessageDeliveriesView.vue'),
},
{ {
path: 'webhook-events', path: 'webhook-events',
meta: { allowedRoles: ['admin', 'operator'] }, meta: { allowedRoles: ['admin', 'operator'] },
@@ -6,5 +6,4 @@ export * from './platform-config'
export * from './orders' export * from './orders'
export * from './tasks' export * from './tasks'
export * from './inventory' export * from './inventory'
export * from './message-deliveries'
export * from './webhook-events' export * from './webhook-events'
@@ -1,9 +0,0 @@
import { apiGet } from '@/lib/http'
import type { AdminMessageDeliveryListItem, AdminPagination } from '@/types/admin'
export function fetchAdminMessageDeliveries(params?: Record<string, unknown>) {
return apiGet<{ items: AdminMessageDeliveryListItem[]; pagination: AdminPagination }>(
'/api/v1/admin/message-deliveries',
params,
)
}
@@ -43,14 +43,6 @@ export function regenerateAdminTaskClaimLink(taskId: number | string) {
return apiPost<AdminTaskActionResponse>(`/api/v1/admin/tasks/${taskId}/regenerate-claim-link`, {}) return apiPost<AdminTaskActionResponse>(`/api/v1/admin/tasks/${taskId}/regenerate-claim-link`, {})
} }
export function confirmAdminTaskAssistedRole(taskId: number | string) {
return apiPost<AdminTaskActionResponse>(`/api/v1/admin/tasks/${taskId}/support-confirm-role`, {})
}
export function redeemAdminTaskAssisted(taskId: number | string) {
return apiPost<AdminTaskActionResponse>(`/api/v1/admin/tasks/${taskId}/support-redeem`, {})
}
export function closeAdminTask(taskId: number | string) { export function closeAdminTask(taskId: number | string) {
return apiPost<AdminTaskActionResponse>(`/api/v1/admin/tasks/${taskId}/close`, {}) return apiPost<AdminTaskActionResponse>(`/api/v1/admin/tasks/${taskId}/close`, {})
} }
@@ -1,7 +1,7 @@
/* ============================================================ /* ============================================================
管理员列表页共享样式 管理员列表页共享样式
引入: AdminOrdersView, AdminTasksView, AdminMessageDeliveriesView, 引入: AdminOrdersView, AdminTasksView, AdminWebhookEventsView,
AdminWebhookEventsView, AdminUsersView, AdminInventoryView AdminUsersView, AdminInventoryView
各页面自行覆盖 grid-template-columns 等布局差异 各页面自行覆盖 grid-template-columns 等布局差异
============================================================ */ ============================================================ */
-3
View File
@@ -39,9 +39,6 @@ export type {
AdminWebhookReplayResponse, AdminWebhookReplayResponse,
} from './webhook-events' } from './webhook-events'
// Message deliveries types
export type { AdminMessageDeliveryListItem } from './message-deliveries'
// Platform config types // Platform config types
export type { export type {
AdminNotificationBarkRecipient, AdminNotificationBarkRecipient,
@@ -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<string, unknown>
errorMessage: string
sentAt: string | null
createdAt: string
updatedAt: string
}
-2
View File
@@ -48,8 +48,6 @@ export interface AdminTaskOperations {
canRefreshKuaishouCloudRoleInfo: boolean canRefreshKuaishouCloudRoleInfo: boolean
canDispatchKuaishouCloudFulfillment: boolean canDispatchKuaishouCloudFulfillment: boolean
canReturnKuaishouCloudFulfillment: boolean canReturnKuaishouCloudFulfillment: boolean
canSupportConfirmRole: boolean
canSupportRedeem: boolean
canViewSensitiveTaskData: boolean canViewSensitiveTaskData: boolean
} }
-7
View File
@@ -41,13 +41,6 @@ export const adminInventoryCredentialTypeOptions = [
{ label: '纯文本凭据', value: 'text_credential' }, { label: '纯文本凭据', value: 'text_credential' },
] ]
export const adminMessageDeliveryStatusOptions = [
{ label: '全部状态', value: '' },
{ label: '发送成功', value: 'success' },
{ label: '发送失败', value: 'failed' },
{ label: '等待发送', value: 'pending' },
]
export const adminWebhookProcessedOptions = [ export const adminWebhookProcessedOptions = [
{ label: '全部', value: '' }, { label: '全部', value: '' },
{ label: '处理成功', value: '1' }, { label: '处理成功', value: '1' },
@@ -148,7 +148,7 @@ export function useAdminInventory() {
function getCredentialTypeHint(value: string) { function getCredentialTypeHint(value: string) {
const normalized = String(value || '').trim() const normalized = String(value || '').trim()
if (normalized === 'tencent_code') if (normalized === 'tencent_code')
return '当前腾讯自动领取链路会优先预占这一类兑换码,三角洲行动这类场景就使用这个类型。内部 SKU 支持中文、英文和数字混用。' return '当前领取链路会优先预占这一类兑换码,快手云履约等场景可按内部 SKU 统一匹配。'
if (normalized === 'card_password') return '适合卡号 / 卡密一体的库存项。' if (normalized === 'card_password') return '适合卡号 / 卡密一体的库存项。'
if (normalized === 'account_password') return '适合账号密码、账号令牌等组合凭据。' if (normalized === 'account_password') return '适合账号密码、账号令牌等组合凭据。'
if (normalized === 'activation_link') return '适合直接发链接的激活类库存。' if (normalized === 'activation_link') return '适合直接发链接的激活类库存。'
@@ -10,7 +10,6 @@ import {
Expand, Expand,
Fold, Fold,
List, List,
Message,
Setting, Setting,
SwitchButton, SwitchButton,
Tickets, Tickets,
@@ -62,8 +61,6 @@ const navItems = computed(() => {
items.push({ to: '/admin/inventory', label: '库存', icon: Box }) items.push({ to: '/admin/inventory', label: '库存', icon: Box })
} }
items.push({ to: '/admin/message-deliveries', label: '消息发送', icon: Message })
if (isAdmin.value || isOperator.value) { if (isAdmin.value || isOperator.value) {
items.push({ to: '/admin/webhook-events', label: 'Webhook', icon: Connection }) items.push({ to: '/admin/webhook-events', label: 'Webhook', icon: Connection })
} }
@@ -1,281 +0,0 @@
<script setup lang="ts">
import { computed, onMounted, ref } from 'vue'
import { useAdminListPage } from '@/composables/useAdminListPage'
import AdminPageHeader from '@/components/admin/AdminPageHeader.vue'
import AdminStatusTag from '@/components/admin/AdminStatusTag.vue'
import { fetchAdminMessageDeliveries } from '@/services/admin'
import type { AdminMessageDeliveryListItem } from '@/types/admin'
import { formatAdminDateTime } from '@/utils/admin-time'
import { adminMessageDeliveryStatusOptions } from '@/utils/admin-options'
const provider = ref('agiso')
const platform = ref('xianyu')
const status = ref('')
const shopId = ref('')
const platformOrderId = ref('')
const taskNo = ref('')
const dateFrom = ref('')
const dateTo = ref('')
const {
loading,
errorMessage,
items,
pagination,
loadPage: loadMessageDeliveries,
} = useAdminListPage<AdminMessageDeliveryListItem>({
defaultErrorMessage: '读取消息发送记录失败',
fetchPage: (page, pageSize) =>
fetchAdminMessageDeliveries({
page,
pageSize,
provider: provider.value.trim(),
platform: platform.value.trim(),
status: status.value.trim(),
shopId: shopId.value.trim(),
platformOrderId: platformOrderId.value.trim(),
taskNo: taskNo.value.trim(),
dateFrom: dateFrom.value.trim(),
dateTo: dateTo.value.trim(),
}),
})
const summary = computed(() => ({
successCount: items.value.filter((item) => item.status === 'success').length,
failedCount: items.value.filter((item) => item.status === 'failed').length,
pendingCount: items.value.filter((item) => item.status === 'pending').length,
}))
function resetFilters() {
provider.value = 'agiso'
platform.value = 'xianyu'
status.value = ''
shopId.value = ''
platformOrderId.value = ''
taskNo.value = ''
dateFrom.value = ''
dateTo.value = ''
void loadMessageDeliveries(1)
}
onMounted(() => {
void loadMessageDeliveries(1)
})
</script>
<template>
<div class="deliveries-page list-page">
<AdminPageHeader
title="消息发送记录"
description="查看领取链接消息是否真正发出、平台返回了什么、失败原因是什么。"
>
<template #extra>
<span class="total-badge"> {{ pagination.total }} 条记录</span>
</template>
</AdminPageHeader>
<!-- 筛选 -->
<el-card shadow="never" class="section-card">
<template #header>
<div class="card-header">
<span class="card-title">筛选消息</span>
<span class="card-desc">按服务商平台发送状态店铺等条件筛选</span>
</div>
</template>
<div class="filter-grid">
<el-input
v-model="provider"
class="filter-control"
placeholder="服务商,如 agiso"
clearable
@keyup.enter="loadMessageDeliveries(1)"
/>
<el-input
v-model="platform"
class="filter-control"
placeholder="业务平台,如 xianyu"
clearable
@keyup.enter="loadMessageDeliveries(1)"
/>
<el-select v-model="status" class="filter-control" placeholder="发送状态" clearable>
<el-option
v-for="opt in adminMessageDeliveryStatusOptions"
:key="opt.value"
:label="opt.label"
:value="opt.value"
/>
</el-select>
<el-input
v-model="shopId"
class="filter-control"
placeholder="店铺 ID(精确匹配)"
clearable
@keyup.enter="loadMessageDeliveries(1)"
/>
<el-input
v-model="platformOrderId"
class="filter-control"
placeholder="平台订单号"
clearable
@keyup.enter="loadMessageDeliveries(1)"
/>
<el-input
v-model="taskNo"
class="filter-control"
placeholder="任务编号"
clearable
@keyup.enter="loadMessageDeliveries(1)"
/>
<el-date-picker
v-model="dateFrom"
class="filter-control"
type="date"
placeholder="开始日期"
value-format="YYYY-MM-DD"
/>
<el-date-picker
v-model="dateTo"
class="filter-control"
type="date"
placeholder="结束日期"
value-format="YYYY-MM-DD"
/>
<div class="filter-buttons">
<el-button round @click="resetFilters">重置</el-button>
<el-button round type="primary" @click="loadMessageDeliveries(1)">查询</el-button>
</div>
</div>
</el-card>
<!-- 摘要 -->
<div v-if="!loading && items.length > 0" class="summary-strip">
<span class="summary-title">当前页</span>
<span class="summary-metric" data-tone="success"
>发送成功 <strong>{{ summary.successCount }}</strong></span
>
<span class="summary-metric" data-tone="danger"
>发送失败 <strong>{{ summary.failedCount }}</strong></span
>
<span class="summary-metric" data-tone="warning"
>等待中 <strong>{{ summary.pendingCount }}</strong></span
>
</div>
<el-alert
v-if="errorMessage"
:title="errorMessage"
type="error"
show-icon
:closable="false"
class="mt-4"
/>
<!-- 列表 -->
<el-card
shadow="never"
class="section-card"
v-loading="loading"
element-loading-text="消息发送记录加载中"
>
<template v-if="items.length === 0 && !loading">
<el-empty description="当前筛选下还没有消息发送记录。" :image-size="60" />
</template>
<template v-else>
<div class="orders-toolbar table-toolbar">
<strong>发送明细</strong>
<span> {{ pagination.page }} 当前展示 {{ items.length }} </span>
</div>
<el-table :data="items" stripe size="small" class="orders-table data-table">
<el-table-column label="消息ID" width="100">
<template #default="{ row }">
<span class="order-no id-link">#{{ row.deliveryId }}</span>
</template>
</el-table-column>
<el-table-column label="接收者" min-width="160">
<template #default="{ row }">
<div class="cell-stack">
<span class="cell-title">{{
row.recipientKey || row.shopName || row.shopId || '-'
}}</span>
<span class="cell-subline">{{ row.shopName || row.shopId || '-' }}</span>
</div>
</template>
</el-table-column>
<el-table-column label="消息类型" min-width="140">
<template #default="{ row }">
<div class="cell-stack">
<span class="cell-title">{{ row.channel || '-' }}</span>
<div class="cell-meta-row">
<span class="compact-chip">{{ row.provider || '-' }}</span>
<span class="compact-chip">{{ row.platform || '-' }}</span>
</div>
</div>
</template>
</el-table-column>
<el-table-column label="状态" width="100">
<template #default="{ row }">
<AdminStatusTag :status="row.status" />
</template>
</el-table-column>
<el-table-column label="创建时间" min-width="160">
<template #default="{ row }">
<div class="cell-stack">
<span class="cell-title">{{ formatAdminDateTime(row.createdAt) }}</span>
<span v-if="row.sentAt" class="cell-subline"
>发送 {{ formatAdminDateTime(row.sentAt) }}</span
>
</div>
</template>
</el-table-column>
<el-table-column label="操作" min-width="140">
<template #default="{ row }">
<div class="action-stack">
<a
v-if="row.claimUrl"
class="detail-link"
:href="row.claimUrl"
target="_blank"
rel="noreferrer"
>Claim 链接</a
>
<RouterLink
v-if="row.orderId"
class="detail-link"
:to="`/admin/orders/${row.orderId}`"
>订单 #{{ row.orderId }}</RouterLink
>
<RouterLink v-if="row.taskId" class="detail-link" :to="`/admin/tasks/${row.taskId}`"
>任务 {{ row.taskNo || row.taskId }}</RouterLink
>
<span v-if="!row.claimUrl && !row.orderId && !row.taskId" class="cell-subline"
>-</span
>
</div>
</template>
</el-table-column>
</el-table>
<el-pagination
v-if="pagination.total > 0"
v-model:current-page="pagination.page"
:page-size="pagination.pageSize"
:total="pagination.total"
layout="total, prev, pager, next"
class="pagination-center"
@current-change="loadMessageDeliveries"
/>
</template>
</el-card>
</div>
</template>
<style scoped>
@import '../../../styles/admin-list-pages.css';
.filter-grid {
grid-template-columns: repeat(4, minmax(0, 1fr));
}
</style>
@@ -3,9 +3,7 @@ import { onMounted } from 'vue'
import { import {
closeAdminTask, closeAdminTask,
confirmAdminTaskAssistedRole,
markAdminTaskManualReview, markAdminTaskManualReview,
redeemAdminTaskAssisted,
regenerateAdminTaskClaimLink, regenerateAdminTaskClaimLink,
releaseAdminTaskInventoryBinding, releaseAdminTaskInventoryBinding,
retryAdminTask, retryAdminTask,
@@ -94,20 +92,6 @@ onMounted(loadDetail)
`确认重新生成任务 ${detail!.task.taskNo} 的领取链接吗?旧链接会失效。`, `确认重新生成任务 ${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" @prepare-kuaishou-cloud="submitKuaishouCloudPrepare"
@dispatch-kuaishou-cloud="submitKuaishouCloudDispatch" @dispatch-kuaishou-cloud="submitKuaishouCloudDispatch"
@return-kuaishou-cloud="submitKuaishouCloudReturnNumber" @return-kuaishou-cloud="submitKuaishouCloudReturnNumber"
@@ -13,8 +13,6 @@ defineProps<Props>()
const emit = defineEmits<{ const emit = defineEmits<{
retry: [] retry: []
regenerateClaimLink: [] regenerateClaimLink: []
confirmRole: []
supportRedeem: []
prepareKuaishouCloud: [] prepareKuaishouCloud: []
dispatchKuaishouCloud: [] dispatchKuaishouCloud: []
returnKuaishouCloud: [] returnKuaishouCloud: []
@@ -45,24 +43,6 @@ const emit = defineEmits<{
> >
重发链接 重发链接
</el-button> </el-button>
<el-button
v-if="detail.operations.canSupportConfirmRole"
:loading="actionLoading"
round
type="warning"
@click="emit('confirmRole')"
>
客服确认角色
</el-button>
<el-button
v-if="detail.operations.canSupportRedeem"
:loading="actionLoading"
round
type="success"
@click="emit('supportRedeem')"
>
客服开始兑换
</el-button>
<el-button <el-button
v-if="canManageTaskLifecycle && detail.operations.canPrepareKuaishouCloudFulfillment" v-if="canManageTaskLifecycle && detail.operations.canPrepareKuaishouCloudFulfillment"
:loading="actionLoading" :loading="actionLoading"
@@ -152,14 +152,14 @@ onMounted(() => {
<el-input <el-input
v-model="provider" v-model="provider"
class="filter-control" class="filter-control"
placeholder="服务商,如 agiso" placeholder="服务商,如 91kaquan"
clearable clearable
@keyup.enter="applyFilters(1)" @keyup.enter="applyFilters(1)"
/> />
<el-input <el-input
v-model="platform" v-model="platform"
class="filter-control" class="filter-control"
placeholder="业务平台,如 xianyu / taobao" placeholder="业务平台,如 kuaishou"
clearable clearable
@keyup.enter="applyFilters(1)" @keyup.enter="applyFilters(1)"
/> />