新增 affiliate_dash 回调路由与幂等去重(阶段 5)

- POST /api/v1/open/affiliate-dash:验签 → 查任务(404 重试) → webhook_events 幂等闸门 → 状态同步
- migration 013:webhook_events(provider+event_id 唯一)
- task-repo findAffiliateDashTaskByOrder(仿 feifei 同构 SQL)
- notify-service:去重闸门在找到任务之后,任务未匹配不被 eventId 永久拦截
- typecheck + 212 测试通过;前置错误分支脚本验证通过
This commit is contained in:
yml2213
2026-08-05 16:07:33 +08:00
parent 00d490a2cd
commit 7fc3d60588
7 changed files with 294 additions and 0 deletions
@@ -16,6 +16,7 @@ import type {
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 = {
runtime_session_id: string
@@ -355,6 +356,40 @@ export async function findKuaishouFeifeiTaskByOrder(input: {
return result.rows[0] || null
}
export async function findAffiliateDashTaskByOrder(input: {
orderNo?: unknown
clientOrderNo?: unknown
}): Promise<TaskRow | null> {
const orderNo = String(input.orderNo || '').trim()
const clientOrderNo = String(input.clientOrderNo || '').trim()
const params: unknown[] = [AFFILIATE_DASH_EXECUTOR_KEY]
const orderFilters: string[] = []
if (orderNo) {
params.push(orderNo)
orderFilters.push(`ft.context_json #>> '{affiliateDash,orderNo}' = $${params.length}`)
}
if (clientOrderNo) {
params.push(clientOrderNo)
orderFilters.push(`ft.context_json #>> '{affiliateDash,clientOrderNo}' = $${params.length}`)
}
if (orderFilters.length === 0) {
return null
}
const result = await query<TaskRow>(
`${buildTaskSelect()}
WHERE ft.executor_key = $1
AND (${orderFilters.join(' OR ')})
ORDER BY ft.id DESC
LIMIT 1`,
params,
)
return result.rows[0] || null
}
export async function listTasks({
page = 1,
pageSize = 20,
@@ -0,0 +1,43 @@
import { query } from '../db/client.js'
import type { JsonObject } from '../types/json.js'
import { nowIso } from '../utils/time.js'
/**
* 幂等写入一条 webhook 事件(provider + event_id 唯一)。
* 冲突时不做任何更新并返回 inserted=false —— 供回调去重闸门使用。
*/
export async function insertWebhookEventOnce(input: {
provider: string
eventId: string
eventType: string
taskId?: number | string | null
payload: JsonObject
processedAt?: string
}): Promise<{ inserted: boolean }> {
const processedAt = input.processedAt || nowIso()
const result = await query<{ id: number }>(
`
INSERT INTO webhook_events (
provider,
event_id,
event_type,
task_id,
payload,
processed_at,
created_at
) VALUES ($1, $2, $3, $4, $5::jsonb, $6, $6)
ON CONFLICT (provider, event_id) DO NOTHING
RETURNING id
`,
[
String(input.provider || '').trim(),
String(input.eventId || '').trim(),
String(input.eventType || '').trim(),
input.taskId == null ? null : Number(input.taskId),
JSON.stringify(input.payload || {}),
processedAt,
],
)
return { inserted: result.rows.length > 0 }
}