修复订单发布与核销并发状态覆盖

This commit is contained in:
yml2213
2026-09-01 14:13:10 +08:00
parent d47360f484
commit c6dd6a8b7c
8 changed files with 667 additions and 57 deletions
@@ -1,6 +1,7 @@
import { query } from '../../db/client.js'
import { query, withTransaction } from '../../db/client.js'
import { toJsonString, toPositiveInteger } from './shared.js'
import { getWorkOrderById } from './work-order-query-repo.js'
import { getWorkOrderById, getWorkOrderByIdWithClient } from './work-order-query-repo.js'
import { createWorkOrderEventWithClient } from './work-order-event-repo.js'
import {
normalizeWorkOrderAcceptanceMode,
normalizeWorkOrderGiftCooldownHours,
@@ -125,6 +126,64 @@ export async function updateWorkOrder(
return getWorkOrderById(workOrderId)
}
/** 将未分配工单原子发布到大厅,并在事务内记录发布事件。 */
export async function publishUnassignedWorkOrder(input: {
workOrderId: number | string
now: string
actorName?: string
}): Promise<{
order: WorkOrderRow | null
failureReason: 'work_order_not_publishable' | null
}> {
return withTransaction(async (client) => {
const currentResult = await client.query<WorkOrderRow>(
`
SELECT *
FROM work_orders
WHERE id = $1
FOR UPDATE
`,
[Number(input.workOrderId)],
)
const current = currentResult.rows[0] || null
if (!current || current.status !== 'unassigned' || current.assigned_worker_id) {
return { order: null, failureReason: 'work_order_not_publishable' }
}
const updatedResult = await client.query<{ id: number }>(
`
UPDATE work_orders
SET status = 'open', published_at = $1, hall_queued_at = $1, updated_at = $1
WHERE id = $2
AND status = 'unassigned'
AND assigned_worker_id IS NULL
AND reward_amount > 0
RETURNING id
`,
[input.now, Number(input.workOrderId)],
)
if (!updatedResult.rows[0]) {
return { order: null, failureReason: 'work_order_not_publishable' }
}
await createWorkOrderEventWithClient(client, {
workOrderId: Number(input.workOrderId),
actorType: 'admin',
actorId: input.actorName || '',
eventType: 'published',
fromStatus: current.status,
toStatus: 'open',
payloadJson: JSON.stringify({ voucherConsumeDeferred: true }),
now: input.now,
})
return {
order: await getWorkOrderByIdWithClient(client, input.workOrderId),
failureReason: null,
}
})
}
export async function updateWorkOrderBasic(
workOrderId: number | string,
patch: {
@@ -4,6 +4,7 @@ import {
deleteWorkOrder,
listWorkOrderShares,
listWorkOrders,
publishUnassignedWorkOrder,
reopenCancelledWorkOrder,
resolveProblemWorkOrder,
updateWorkOrder,
@@ -15,7 +16,10 @@ import { createHttpError } from '../../utils/http.js'
import { nowIso } from '../../utils/time.js'
import { resolveAdminNotificationEntity } from '../admin/admin-notification-service.js'
import { publishWorkOrderRealtimeChange } from '../realtime/realtime-event-service.js'
import { consumeIndustryVouchersBeforeWorkOrderPublish } from './publish-voucher-service.js'
import {
consumeIndustryVouchersBeforeWorkOrderPublish,
type WorkOrderVoucherConsumeResult,
} from './publish-voucher-service.js'
import {
mapWorkOrderAdmin,
normalizeAmountFen,
@@ -228,55 +232,47 @@ export async function publishAdminWorkOrder(workOrderId: number | string, actorN
})
}
const voucherConsume = await consumeIndustryVouchersBeforeWorkOrderPublish(workOrder)
if (!voucherConsume.ok) {
const failedAt = nowIso()
await createWorkOrderEvent({
workOrderId: workOrder.id,
actorType: 'admin',
actorId: actorName,
eventType: 'publish_voucher_consume_failed',
fromStatus: workOrder.status,
toStatus: workOrder.status,
payloadJson: JSON.stringify(voucherConsume),
now: failedAt,
})
throw createHttpError(voucherConsume.errorMessage || '电子凭证核销失败,订单未发布', {
const now = nowIso()
const published = await publishUnassignedWorkOrder({
workOrderId: workOrder.id,
now,
actorName,
})
if (!published.order) {
throw createHttpError('订单状态已变化,无法发布,请刷新后重试', {
statusCode: 409,
errorCode: 'work_order_publish_voucher_consume_failed',
context: voucherConsume,
errorCode: 'work_order_publish_conflict',
})
}
const now = nowIso()
const updated = await updateWorkOrder(workOrder.id, {
status: WORK_ORDER_STATUS.OPEN,
published_at: now,
hall_queued_at: now,
updated_at: now,
})
if (!updated) {
throw createHttpError(
voucherConsume.voucherCount > 0
? '电子凭证已核销,但订单发布失败,请重试发布'
: '订单发布失败,请重试',
{
statusCode: 409,
errorCode: 'work_order_publish_update_failed',
context: voucherConsume,
},
)
let voucherConsume: WorkOrderVoucherConsumeResult
try {
voucherConsume = await consumeIndustryVouchersBeforeWorkOrderPublish(published.order)
} catch (error) {
voucherConsume = {
ok: false,
voucherCount: 0,
consumedCount: 0,
alreadyConsumedCount: 0,
failedCount: 1,
voucherCodes: [],
errorMessage: error instanceof Error ? error.message : '电子凭证核销失败',
}
}
await createWorkOrderEvent({
workOrderId: workOrder.id,
actorType: 'admin',
actorId: actorName,
eventType: 'published',
fromStatus: workOrder.status,
toStatus: WORK_ORDER_STATUS.OPEN,
payloadJson: JSON.stringify({ voucherConsume }),
now,
})
if (!voucherConsume.ok) {
await createWorkOrderEvent({
workOrderId: workOrder.id,
actorType: 'system',
actorId: 'kuaishou_send_code',
eventType: 'publish_voucher_consume_failed',
fromStatus: WORK_ORDER_STATUS.OPEN,
toStatus: WORK_ORDER_STATUS.OPEN,
payloadJson: JSON.stringify(voucherConsume),
now: nowIso(),
})
}
const currentOrder = await getRequiredWorkOrder(workOrder.id)
publishWorkOrderRealtimeChange({ workOrderId: Number(workOrder.id), hallChanged: true })
const hallConfig = getWorkerHallConfig()
const hallEntry = await listWorkOrders({
@@ -289,7 +285,7 @@ export async function publishAdminWorkOrder(workOrderId: number | string, actorN
excludeFilledSharing: true,
})
return {
order: mapWorkOrderAdmin(updated),
order: mapWorkOrderAdmin(currentOrder),
voucherConsume,
hallVisible: hallEntry.total > 0,
}