点“关闭任务”,就会达到你要的效果:链接失效,预占释放。
This commit is contained in:
@@ -22,6 +22,7 @@ import { confirmClaimRoleForAdminTask, redeemClaimTaskForAdminTask } from '../cl
|
||||
import { reserveInventoryForTask } from '../order/inventory-service.js'
|
||||
import { replayAgisoTradeWebhookEvent } from '../order/webhook-service.js'
|
||||
import { ensureAgisoXianyuAutoDeliveryForDeliveredTask } from '../platforms/agiso/xianyu/auto-delivery-service.js'
|
||||
import { closeTencentBrowserSession } from '../session/session.js'
|
||||
import { createHttpError } from '../../utils/http.js'
|
||||
import { nowIso } from '../../utils/time.js'
|
||||
import {
|
||||
@@ -394,7 +395,25 @@ export async function redeemAdminTaskAssisted(taskId, session = null) {
|
||||
/** @returns {Promise<AdminTaskActionResponse>} */
|
||||
/** @param {AdminEntityIdInput} taskId */
|
||||
export async function closeAdminTask(taskId) {
|
||||
const task = await getRequiredTask(taskId)
|
||||
return closeAdminTaskWithDeps(taskId)
|
||||
}
|
||||
|
||||
/** @returns {Promise<AdminTaskActionResponse>} */
|
||||
/** @param {AdminEntityIdInput} taskId */
|
||||
export async function closeAdminTaskWithDeps(
|
||||
taskId,
|
||||
{
|
||||
getRequiredTask: getTask = getRequiredTask,
|
||||
listTaskInventoryBindingsByTaskId: listBindings = listTaskInventoryBindingsByTaskId,
|
||||
releaseReservedInventoryItem: releaseReserved = releaseReservedInventoryItem,
|
||||
updateClaimToken: updateToken = updateClaimToken,
|
||||
updateTask: updateTaskRecord = updateTask,
|
||||
createTaskEvent: createEvent = createTaskEvent,
|
||||
closeTencentBrowserSession: closeSession = closeTencentBrowserSession,
|
||||
nowIso: getNowIso = nowIso,
|
||||
} = {},
|
||||
) {
|
||||
const task = await getTask(taskId)
|
||||
|
||||
if (task.task_status === 'redeemed') {
|
||||
throw createHttpError('已兑换任务不能关闭', {
|
||||
@@ -403,13 +422,63 @@ export async function closeAdminTask(taskId) {
|
||||
})
|
||||
}
|
||||
|
||||
const updatedTask = await updateTask(task.id, {
|
||||
const now = getNowIso()
|
||||
const bindings = await listBindings(task.id)
|
||||
const reservedBindings = bindings.filter((binding) => String(binding.binding_status || '').trim() === 'reserved')
|
||||
const releasedInventoryItemIds = Array.from(new Set(reservedBindings.map((binding) => Number(binding.inventory_item_id)).filter((id) => id > 0)))
|
||||
const hasConsumedBindings = bindings.some((binding) => String(binding.binding_status || '').trim() === 'consumed')
|
||||
const primaryClaimTokenId = getTaskPrimaryClaimTokenId(task)
|
||||
let browserSessionClosed = false
|
||||
|
||||
if (primaryClaimTokenId) {
|
||||
await updateToken(primaryClaimTokenId, {
|
||||
status: 'revoked',
|
||||
expired_at: now,
|
||||
updated_at: now,
|
||||
})
|
||||
}
|
||||
|
||||
if (task.browser_session_id) {
|
||||
try {
|
||||
await closeSession(task.browser_session_id)
|
||||
browserSessionClosed = true
|
||||
} catch (error) {
|
||||
if (!isRecoverableTaskSessionCloseError(error)) {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const inventoryItemId of releasedInventoryItemIds) {
|
||||
await releaseReserved(inventoryItemId, now)
|
||||
}
|
||||
|
||||
const closeReasonParts = ['已手动关闭任务']
|
||||
if (primaryClaimTokenId) {
|
||||
closeReasonParts.push('领取链接已失效')
|
||||
}
|
||||
if (releasedInventoryItemIds.length > 0) {
|
||||
closeReasonParts.push('预占库存已释放')
|
||||
}
|
||||
|
||||
const updatedTask = await updateTaskRecord(task.id, {
|
||||
task_status: 'closed',
|
||||
inventory_status: hasConsumedBindings ? 'consumed' : 'pending',
|
||||
delivery_status: 'closed',
|
||||
last_error: task.last_error || '已手动关闭任务',
|
||||
updated_at: nowIso(),
|
||||
user_action_status: 'closed',
|
||||
claim_expires_at: primaryClaimTokenId ? now : task.claim_expires_at,
|
||||
browser_session_id: '',
|
||||
last_error: task.last_error || closeReasonParts.join(','),
|
||||
updated_at: now,
|
||||
})
|
||||
|
||||
await createEvent(task.id, 'task_closed', {
|
||||
claimTokenRevoked: Boolean(primaryClaimTokenId),
|
||||
releasedInventoryItemIds,
|
||||
releasedInventoryCount: releasedInventoryItemIds.length,
|
||||
browserSessionClosed,
|
||||
}, now)
|
||||
|
||||
return {
|
||||
task: mapTaskActionPayload(updatedTask),
|
||||
}
|
||||
@@ -697,6 +766,11 @@ function ensureViewerCanOperateAssistedTask(task, viewerContext, action) {
|
||||
}
|
||||
}
|
||||
|
||||
function isRecoverableTaskSessionCloseError(error) {
|
||||
const errorCode = String(error?.errorCode || error?.code || '').trim()
|
||||
return errorCode === 'session_not_found' || errorCode === 'session_closed'
|
||||
}
|
||||
|
||||
function normalizeManualDispatchOutcome(value) {
|
||||
const normalized = String(value || '').trim().toLowerCase()
|
||||
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
import test from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
|
||||
import { closeAdminTaskWithDeps } from './admin-write-service.js'
|
||||
|
||||
test('closeAdminTaskWithDeps revokes claim link, releases reserved inventory, and closes browser session', async () => {
|
||||
const calls = {
|
||||
updateToken: [],
|
||||
releaseReserved: [],
|
||||
closeSession: [],
|
||||
updateTask: [],
|
||||
createEvent: [],
|
||||
}
|
||||
const task = {
|
||||
id: 30,
|
||||
task_status: 'link_generated',
|
||||
delivery_status: 'pending',
|
||||
inventory_status: 'reserved',
|
||||
user_action_status: 'pending_claim',
|
||||
browser_session_id: 'browser-session-30',
|
||||
primary_claim_token_id: 9,
|
||||
claim_expires_at: '2026-04-15T10:00:00.000Z',
|
||||
last_error: '',
|
||||
updated_at: '2026-04-14T10:00:00.000Z',
|
||||
}
|
||||
const now = '2026-04-14T10:16:18.000Z'
|
||||
|
||||
const result = await closeAdminTaskWithDeps(task.id, {
|
||||
getRequiredTask: async () => task,
|
||||
listTaskInventoryBindingsByTaskId: async () => ([
|
||||
{ inventory_item_id: 17, binding_status: 'reserved' },
|
||||
{ inventory_item_id: 18, binding_status: 'released' },
|
||||
{ inventory_item_id: 17, binding_status: 'reserved' },
|
||||
]),
|
||||
releaseReservedInventoryItem: async (inventoryItemId, updatedAt) => {
|
||||
calls.releaseReserved.push({ inventoryItemId, updatedAt })
|
||||
return { id: inventoryItemId }
|
||||
},
|
||||
updateClaimToken: async (tokenId, patch) => {
|
||||
calls.updateToken.push({ tokenId, patch })
|
||||
return { id: tokenId, ...patch }
|
||||
},
|
||||
closeTencentBrowserSession: async (sessionId) => {
|
||||
calls.closeSession.push(sessionId)
|
||||
return { sessionId, closed: true }
|
||||
},
|
||||
updateTask: async (taskId, patch) => {
|
||||
calls.updateTask.push({ taskId, patch })
|
||||
return { ...task, ...patch }
|
||||
},
|
||||
createTaskEvent: async (taskId, eventType, payload, createdAt) => {
|
||||
calls.createEvent.push({ taskId, eventType, payload, createdAt })
|
||||
return null
|
||||
},
|
||||
nowIso: () => now,
|
||||
})
|
||||
|
||||
assert.equal(calls.updateToken.length, 1)
|
||||
assert.deepEqual(calls.updateToken[0], {
|
||||
tokenId: 9,
|
||||
patch: {
|
||||
status: 'revoked',
|
||||
expired_at: now,
|
||||
updated_at: now,
|
||||
},
|
||||
})
|
||||
assert.deepEqual(calls.releaseReserved, [
|
||||
{ inventoryItemId: 17, updatedAt: now },
|
||||
])
|
||||
assert.deepEqual(calls.closeSession, ['browser-session-30'])
|
||||
assert.equal(calls.updateTask.length, 1)
|
||||
assert.equal(calls.updateTask[0].patch.task_status, 'closed')
|
||||
assert.equal(calls.updateTask[0].patch.delivery_status, 'closed')
|
||||
assert.equal(calls.updateTask[0].patch.inventory_status, 'pending')
|
||||
assert.equal(calls.updateTask[0].patch.user_action_status, 'closed')
|
||||
assert.equal(calls.updateTask[0].patch.browser_session_id, '')
|
||||
assert.equal(calls.updateTask[0].patch.claim_expires_at, now)
|
||||
assert.match(calls.updateTask[0].patch.last_error, /领取链接已失效/)
|
||||
assert.match(calls.updateTask[0].patch.last_error, /预占库存已释放/)
|
||||
assert.equal(calls.createEvent.length, 1)
|
||||
assert.deepEqual(calls.createEvent[0], {
|
||||
taskId: 30,
|
||||
eventType: 'task_closed',
|
||||
payload: {
|
||||
claimTokenRevoked: true,
|
||||
releasedInventoryItemIds: [17],
|
||||
releasedInventoryCount: 1,
|
||||
browserSessionClosed: true,
|
||||
},
|
||||
createdAt: now,
|
||||
})
|
||||
assert.equal(result.task.status, 'closed')
|
||||
})
|
||||
|
||||
test('closeAdminTaskWithDeps rejects redeemed tasks', async () => {
|
||||
await assert.rejects(
|
||||
() => closeAdminTaskWithDeps(99, {
|
||||
getRequiredTask: async () => ({
|
||||
id: 99,
|
||||
task_status: 'redeemed',
|
||||
}),
|
||||
}),
|
||||
/已兑换任务不能关闭/,
|
||||
)
|
||||
})
|
||||
Reference in New Issue
Block a user