fix(claim): 修复后台手动关闭任务后领取链接未失效的缺陷
1. 鉴权层激活 CLAIM_TERMINAL_STATUSES:在 getClaimContext 中增加 task 状态校验,对 closed 与 expired 终态任务实施 410 拦截并自愈更新关联 Token; 2. 仓储层新增 closeClaimTokensByTaskId():支持按 taskId 批量更新所有非 closed 状态的 Token 为 closed,彻底杜绝多 Token 或重发链接导致的历史活跃残留; 3. 后台关闭任务流程闭环:closeAdminTask 显式触发 closeClaimTokensByTaskId,确保任务关闭时关联领取链接立即失效; 4. 补充 CLAIM_TERMINAL_STATUSES 单元测试与完整排查分析方案文档。
This commit is contained in:
@@ -127,3 +127,22 @@ export async function updateClaimToken(
|
||||
|
||||
return result.rows[0] || null
|
||||
}
|
||||
|
||||
export async function closeClaimTokensByTaskId(
|
||||
taskId: number | string,
|
||||
closedAt: string = new Date().toISOString(),
|
||||
): Promise<ClaimTokenRow[]> {
|
||||
const result = await query<ClaimTokenRow>(
|
||||
`
|
||||
UPDATE claim_tokens
|
||||
SET
|
||||
status = 'closed',
|
||||
expired_at = $1,
|
||||
updated_at = $1
|
||||
WHERE task_id = $2 AND status != 'closed'
|
||||
RETURNING *
|
||||
`,
|
||||
[closedAt, Number(taskId)],
|
||||
)
|
||||
return result.rows
|
||||
}
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import { updateClaimToken } from '../../../repositories/claim-token-repo.js'
|
||||
import {
|
||||
closeClaimTokensByTaskId,
|
||||
updateClaimToken,
|
||||
} from '../../../repositories/claim-token-repo.js'
|
||||
import { updateTask } from '../../../repositories/task-repo.js'
|
||||
import { createTaskEvent } from '../../../repositories/task-event-repo.js'
|
||||
import { createTaskClaimToken, buildClaimUrl } from '../../claim/claim-service.js'
|
||||
@@ -57,6 +60,8 @@ export async function closeAdminTask(
|
||||
})
|
||||
}
|
||||
|
||||
await closeClaimTokensByTaskId(task.id, now)
|
||||
|
||||
const updatedTask = await updateTask(task.id, {
|
||||
task_status: TASK_STATUS.CLOSED,
|
||||
delivery_status: 'closed',
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import test from 'node:test'
|
||||
|
||||
import { CLAIM_TERMINAL_STATUSES } from './kuaishou-cloud-claim-context.js'
|
||||
import { TASK_STATUS } from '../../domain/task-status.js'
|
||||
|
||||
test('CLAIM_TERMINAL_STATUSES 包含 closed 与 expired 状态', () => {
|
||||
assert.equal(CLAIM_TERMINAL_STATUSES.has(TASK_STATUS.CLOSED), true)
|
||||
assert.equal(CLAIM_TERMINAL_STATUSES.has(TASK_STATUS.EXPIRED), true)
|
||||
assert.equal(CLAIM_TERMINAL_STATUSES.has(TASK_STATUS.PAID), false)
|
||||
assert.equal(CLAIM_TERMINAL_STATUSES.has(TASK_STATUS.LINK_GENERATED), false)
|
||||
})
|
||||
@@ -13,7 +13,7 @@ import { normalizeAffiliateDashFlow } from '../fulfillment/affiliate-dash/index.
|
||||
import type { ClaimTokenRow, OrderItemRow, OrderRow, TaskRow } from '../../types/repository/rows.js'
|
||||
import type { JsonObject } from '../../types/json.js'
|
||||
|
||||
export const CLAIM_TERMINAL_STATUSES = new Set([TASK_STATUS.EXPIRED, TASK_STATUS.CLOSED])
|
||||
export const CLAIM_TERMINAL_STATUSES = new Set<string>([TASK_STATUS.EXPIRED, TASK_STATUS.CLOSED])
|
||||
|
||||
const RETIRED_CHANNEL_MESSAGE: Record<string, string> = {
|
||||
kuaishou_feifei: '飞飞履约渠道已下线,本链接仅可查看历史信息,无法继续领取',
|
||||
@@ -55,13 +55,45 @@ export async function getClaimContext(token: unknown) {
|
||||
})
|
||||
}
|
||||
|
||||
if (claimToken.status !== 'active') {
|
||||
throw createHttpError('领取链接当前不可用', {
|
||||
if (CLAIM_TERMINAL_STATUSES.has(task.task_status)) {
|
||||
if (claimToken.status === 'active') {
|
||||
await updateClaimToken(claimToken.id, {
|
||||
status: task.task_status === TASK_STATUS.EXPIRED ? 'expired' : 'closed',
|
||||
expired_at: nowIso(),
|
||||
updated_at: nowIso(),
|
||||
}).catch(() => null)
|
||||
}
|
||||
|
||||
const isExpired = task.task_status === TASK_STATUS.EXPIRED
|
||||
throw createHttpError(isExpired ? '领取链接已过期' : '任务已关闭,领取链接已失效', {
|
||||
statusCode: 410,
|
||||
errorCode: 'claim_token_inactive',
|
||||
errorCode: isExpired ? 'claim_token_expired' : 'claim_task_closed',
|
||||
context: {
|
||||
claimToken: {
|
||||
...claimToken,
|
||||
status: isExpired ? 'expired' : 'closed',
|
||||
},
|
||||
task,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
if (claimToken.status !== 'active') {
|
||||
const isClosed = claimToken.status === 'closed'
|
||||
const isExpired = claimToken.status === 'expired'
|
||||
throw createHttpError(
|
||||
isClosed ? '任务已关闭,领取链接已失效' : isExpired ? '领取链接已过期' : '领取链接当前不可用',
|
||||
{
|
||||
statusCode: 410,
|
||||
errorCode: isClosed
|
||||
? 'claim_task_closed'
|
||||
: isExpired
|
||||
? 'claim_token_expired'
|
||||
: 'claim_token_inactive',
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
if (claimToken.expired_at && new Date(claimToken.expired_at).getTime() <= Date.now()) {
|
||||
const expiredContext = await expireClaimContext(claimToken, task)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user