refactor(fulfillment): 移除飞飞/乐玩通道,仅保留 affiliate_dash

新单只路由 affiliate_dash(未命中走 manual_dispatch);历史飞飞/乐玩任务保留只读 stub,避免订单重同步失败。同步清理 cloudtentacles 调度、Admin 写路径与相关 env。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
yml2213
2026-09-14 17:29:47 +08:00
co-authored by Cursor
parent 0cbcb89dd8
commit b3aad0044c
155 changed files with 661 additions and 23351 deletions
@@ -1,46 +0,0 @@
import assert from 'node:assert/strict'
import test from 'node:test'
import { syncKuaishouCloudTaskStateForTask } from './kuaishou-cloud-task-state-repo.js'
test('syncKuaishouCloudTaskStateForTask 将 Date 更新时间规范化为 ISO 字符串', async () => {
const calls: Array<{ text: string; params?: unknown[] }> = []
const executor = async (text: string, params?: unknown[]) => {
calls.push({ text, params })
return { rows: [] }
}
await syncKuaishouCloudTaskStateForTask(
{
taskId: 9,
executorKey: 'kuaishou_ct_assisted',
contextJson: {
kuaishouCloudFulfillment: {
ticket: {
status: 'verified',
code: '597172E91CF18417',
},
binding: {
bindUrl: 'https://example.test/bind',
vnPhone: '14062706492',
roleName: '测试角色',
roleId: '4212063544',
resolvedSourceKey: 'account_test',
},
dispatch: {
status: 'pending',
},
consume: {
status: 'pending',
},
},
},
updatedAt: new Date('2026-05-29T09:05:46.658Z'),
},
executor,
)
assert.equal(calls.length, 1)
assert.match(calls[0].text, /INSERT INTO kuaishou_cloud_task_states/)
assert.equal(calls[0].params?.[10], '2026-05-29T09:05:46.658Z')
})
@@ -1,180 +0,0 @@
import { query } from '../db/client.js'
import { TASK_STATUS } from '../domain/task-status.js'
import { maskCode } from '../utils/masking.js'
import { parseTaskContext } from '../utils/task-json.js'
import { normalizeTimestampIso } from '../utils/time.js'
import type { JsonRecord } from '../types/json.js'
type QueryExecutor = (text: string, params?: unknown[]) => Promise<{ rows: unknown[] }>
export type KuaishouCloudSourceLoadStat = {
sourceKey: string
activeCount: number
redeemingCount: number
}
export type KuaishouCloudTaskStateSyncInput = {
taskId: number | string
executorKey?: unknown
contextJson?: unknown
updatedAt?: unknown
}
export async function syncKuaishouCloudTaskStateForTask(
input: KuaishouCloudTaskStateSyncInput,
executor: QueryExecutor = query,
): Promise<void> {
const taskId = Number(input.taskId || 0)
if (!Number.isFinite(taskId) || taskId <= 0) {
return
}
if (String(input.executorKey || '').trim() !== 'kuaishou_ct_assisted') {
await executor('DELETE FROM kuaishou_cloud_task_states WHERE task_id = $1', [taskId])
return
}
const flow = resolveKuaishouCloudFlow(input.contextJson)
if (!flow) {
await executor('DELETE FROM kuaishou_cloud_task_states WHERE task_id = $1', [taskId])
return
}
const ticket = asRecord(flow.ticket)
const binding = asRecord(flow.binding)
const role = asRecord(flow.role)
const dispatch = asRecord(flow.dispatch)
const consume = asRecord(flow.consume)
const roleName = pickFirstNonEmpty([role.name, binding.roleName])
const roleId = pickFirstNonEmpty([role.rid, binding.roleId])
const updatedAt = normalizeTimestampIso(input.updatedAt)
await executor(
`
INSERT INTO kuaishou_cloud_task_states (
task_id,
ticket_status,
ticket_code_masked,
bind_url,
vn_phone,
role_name,
role_id,
dispatch_status,
consume_status,
source_key,
updated_at
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
ON CONFLICT (task_id) DO UPDATE
SET
ticket_status = EXCLUDED.ticket_status,
ticket_code_masked = EXCLUDED.ticket_code_masked,
bind_url = EXCLUDED.bind_url,
vn_phone = EXCLUDED.vn_phone,
role_name = EXCLUDED.role_name,
role_id = EXCLUDED.role_id,
dispatch_status = EXCLUDED.dispatch_status,
consume_status = EXCLUDED.consume_status,
source_key = EXCLUDED.source_key,
updated_at = EXCLUDED.updated_at
`,
[
taskId,
normalizeStatus(ticket.status),
maskCode(ticket.code),
String(binding.bindUrl || '').trim(),
String(binding.vnPhone || '').trim(),
roleName,
roleId,
normalizeStatus(dispatch.status),
normalizeStatus(consume.status),
String(binding.resolvedSourceKey || '').trim(),
updatedAt,
],
)
}
export async function listKuaishouCloudSourceLoadStats(
sourceKeys: unknown[] = [],
executor: QueryExecutor = query,
): Promise<KuaishouCloudSourceLoadStat[]> {
const candidates = normalizeStringArray(sourceKeys)
if (candidates.length === 0) {
return []
}
const activeStatuses = [
TASK_STATUS.WAITING_BINDING,
TASK_STATUS.ROLE_CONFIRMED,
TASK_STATUS.REDEEMING,
TASK_STATUS.DISPATCHED_PENDING_RETURN,
]
const result = await executor(
`
SELECT
kcts.source_key,
COUNT(*) FILTER (WHERE ft.task_status = ANY($2::text[])) AS active_count,
COUNT(*) FILTER (WHERE ft.task_status = $3) AS redeeming_count
FROM fulfillment_tasks ft
JOIN kuaishou_cloud_task_states kcts ON kcts.task_id = ft.id
WHERE ft.executor_key = 'kuaishou_ct_assisted'
AND kcts.source_key = ANY($1::text[])
AND ft.task_status = ANY($2::text[])
GROUP BY kcts.source_key
`,
[candidates, activeStatuses, TASK_STATUS.REDEEMING],
)
return result.rows
.map((row) => {
const source = asRecord(row)
return {
sourceKey: String(source.source_key || '').trim(),
activeCount: Number(source.active_count || 0) || 0,
redeemingCount: Number(source.redeeming_count || 0) || 0,
}
})
.filter((row) => row.sourceKey)
}
function resolveKuaishouCloudFlow(contextJson: unknown): JsonRecord | null {
const context = parseTaskContext({ context_json: contextJson })
const flow = asRecord(context.kuaishouCloudFulfillment)
return Object.keys(flow).length > 0 ? flow : null
}
function asRecord(value: unknown): JsonRecord {
return value && typeof value === 'object' && !Array.isArray(value) ? (value as JsonRecord) : {}
}
function normalizeStatus(value: unknown): string {
return String(value || 'pending').trim() || 'pending'
}
function normalizeStringArray(value: unknown): string[] {
if (Array.isArray(value)) {
return Array.from(new Set(value.map((item) => String(item || '').trim()).filter(Boolean)))
}
if (typeof value === 'string') {
return Array.from(
new Set(
value
.split(',')
.map((item) => item.trim())
.filter(Boolean),
),
)
}
return []
}
function pickFirstNonEmpty(values: unknown[]): string {
for (const value of values) {
const normalized = String(value || '').trim()
if (normalized) {
return normalized
}
}
return ''
}
@@ -12,7 +12,7 @@ test('订单列表综合筛选下推到总数和分页查询', async () => {
pageSize: 10,
keyword: '皮肤店',
provider: '91kaquan',
executorKey: 'kuaishou_ct_assisted',
executorKey: 'affiliate_dash',
payStatus: 'paid',
taskStatus: 'manual_review',
dateFrom: '2026-09-01T00:00:00.000Z',
@@ -35,7 +35,7 @@ test('订单列表综合筛选下推到总数和分页查询', async () => {
'91kaquan',
'paid',
'manual_review',
'kuaishou_ct_assisted',
'affiliate_dash',
'2026-09-01T00:00:00.000Z',
'2026-09-02T23:59:59.999Z',
])
@@ -6,7 +6,7 @@ import { buildTaskListFilters } from './task-repo.js'
test('任务列表综合关键词覆盖任务订单商品与角色字段', () => {
const result = buildTaskListFilters({
keyword: '指挥官密钥',
executorKey: 'kuaishou_ct_assisted',
executorKey: 'affiliate_dash',
status: 'manual_review',
dateFrom: '2026-09-01T00:00:00.000Z',
dateTo: '2026-09-02T23:59:59.999Z',
@@ -25,7 +25,7 @@ test('任务列表综合关键词覆盖任务订单商品与角色字段', () =>
assert.match(result.whereClause, /ft\.created_at <= \$5/)
assert.deepEqual(result.params, [
'%指挥官密钥%',
'kuaishou_ct_assisted',
'affiliate_dash',
'manual_review',
'2026-09-01T00:00:00.000Z',
'2026-09-02T23:59:59.999Z',
+2 -32
View File
@@ -1,7 +1,6 @@
import { query, withTransaction } from '../db/client.js'
import type { PoolClient } from 'pg'
import { syncKuaishouCloudTaskStateForTask } from './kuaishou-cloud-task-state-repo.js'
import { normalizeTimestampIso } from '../utils/time.js'
import type {
TaskCreateInput,
@@ -12,7 +11,6 @@ import type {
import type { TaskListQueryResult, TaskRow } from '../types/repository/rows.js'
type TaskQueryExecutor = (text: string, params?: unknown[]) => Promise<{ rows: unknown[] }>
const KUAISHOU_FEIFEI_EXECUTOR_KEY = 'kuaishou_feifei'
const AFFILIATE_DASH_EXECUTOR_KEY = 'affiliate_dash'
type TaskRuntimeContextRow = {
@@ -168,16 +166,6 @@ export async function createTask(input: TaskCreateInput): Promise<TaskRow | null
)
}
await syncKuaishouCloudTaskStateForTask(
{
taskId,
executorKey: input.executorKey,
contextJson: input.contextJson || '{}',
updatedAt: input.updatedAt,
},
client.query.bind(client) as TaskQueryExecutor,
)
return getTaskByIdWithExecutor(client.query.bind(client) as TaskQueryExecutor, taskId)
})
}
@@ -262,16 +250,6 @@ export async function updateTask(
)
}
await syncKuaishouCloudTaskStateForTask(
{
taskId,
executorKey: next.executor_key,
contextJson: next.context_json,
updatedAt: next.updated_at,
},
client.query.bind(client) as TaskQueryExecutor,
)
return getTaskByIdWithExecutor(client.query.bind(client) as TaskQueryExecutor, taskId)
})
}
@@ -329,16 +307,6 @@ export async function updateTaskStatusIfCurrent(
return null
}
await syncKuaishouCloudTaskStateForTask(
{
taskId: updated.id,
executorKey: updated.executor_key,
contextJson: updated.context_json,
updatedAt: updated.updated_at,
},
client.query.bind(client) as TaskQueryExecutor,
)
return getTaskByIdWithExecutor(client.query.bind(client) as TaskQueryExecutor, taskId)
})
}
@@ -360,6 +328,8 @@ export async function findTaskByClaimTokenId(
return result.rows[0] || null
}
const KUAISHOU_FEIFEI_EXECUTOR_KEY = 'kuaishou_feifei'
export async function findKuaishouFeifeiTaskByOrder(input: {
platformOrderNo?: unknown
orderNo?: unknown