清理旧消息和历史命名
This commit is contained in:
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
快手轻量后端服务。
|
快手轻量后端服务。
|
||||||
|
|
||||||
后端负责订单入库、履约任务编排、快手 Cloud / 91 卡券 / 快手小店核销配置、后台管理接口和数据库迁移。默认入口就是轻量后端,不再包含浏览器会话、OCR 或旧咸鱼自动发货链路。
|
后端负责订单入库、履约任务编排、快手 Cloud / 91 卡券 / 快手小店核销配置、后台管理接口和数据库迁移。默认入口就是轻量后端,不再包含浏览器会话或 OCR 链路。
|
||||||
|
|
||||||
## 快速启动
|
## 快速启动
|
||||||
|
|
||||||
|
|||||||
@@ -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),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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,
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 = {
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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 等布局差异
|
||||||
============================================================ */
|
============================================================ */
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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)"
|
||||||
/>
|
/>
|
||||||
|
|||||||
Reference in New Issue
Block a user