diff --git a/apps/backend/src/repositories/worker-platform/work-order-management-repo.ts b/apps/backend/src/repositories/worker-platform/work-order-management-repo.ts index 40937f3a..88652ebb 100644 --- a/apps/backend/src/repositories/worker-platform/work-order-management-repo.ts +++ b/apps/backend/src/repositories/worker-platform/work-order-management-repo.ts @@ -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( + ` + 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: { diff --git a/apps/backend/src/services/worker-platform/admin-work-order-management-service.ts b/apps/backend/src/services/worker-platform/admin-work-order-management-service.ts index bd47017d..1e1b74c4 100644 --- a/apps/backend/src/services/worker-platform/admin-work-order-management-service.ts +++ b/apps/backend/src/services/worker-platform/admin-work-order-management-service.ts @@ -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) + const now = nowIso() + const published = await publishUnassignedWorkOrder({ + workOrderId: workOrder.id, + now, + actorName, + }) + if (!published.order) { + throw createHttpError('订单状态已变化,无法发布,请刷新后重试', { + statusCode: 409, + errorCode: 'work_order_publish_conflict', + }) + } + + 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 : '电子凭证核销失败', + } + } if (!voucherConsume.ok) { - const failedAt = nowIso() await createWorkOrderEvent({ workOrderId: workOrder.id, - actorType: 'admin', - actorId: actorName, + actorType: 'system', + actorId: 'kuaishou_send_code', eventType: 'publish_voucher_consume_failed', - fromStatus: workOrder.status, - toStatus: workOrder.status, + fromStatus: WORK_ORDER_STATUS.OPEN, + toStatus: WORK_ORDER_STATUS.OPEN, payloadJson: JSON.stringify(voucherConsume), - now: failedAt, - }) - throw createHttpError(voucherConsume.errorMessage || '电子凭证核销失败,订单未发布', { - statusCode: 409, - errorCode: 'work_order_publish_voucher_consume_failed', - context: voucherConsume, + now: nowIso(), }) } - 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, - }, - ) - } - await createWorkOrderEvent({ - workOrderId: workOrder.id, - actorType: 'admin', - actorId: actorName, - eventType: 'published', - fromStatus: workOrder.status, - toStatus: WORK_ORDER_STATUS.OPEN, - payloadJson: JSON.stringify({ voucherConsume }), - now, - }) + 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, } diff --git a/apps/frontend/src/components/WorkOrderEventTimeline.tsx b/apps/frontend/src/components/WorkOrderEventTimeline.tsx index f3489181..95f9da5c 100644 --- a/apps/frontend/src/components/WorkOrderEventTimeline.tsx +++ b/apps/frontend/src/components/WorkOrderEventTimeline.tsx @@ -351,8 +351,9 @@ export function formatWorkOrderEvent(event: WorkOrderEvent): EventNodeStyle { case 'voucher_backfilled': return { title: '回填了电子凭证信息', color: 'default' } case 'assign_voucher_consume_failed': - case 'publish_voucher_consume_failed': return { title: '电子凭证核销失败,本次操作未生效', color: 'red' } + case 'publish_voucher_consume_failed': + return { title: '订单已发布,但电子凭证核销未完成', color: 'orange' } default: // 通用状态流转兜底 diff --git a/apps/frontend/src/pages/admin/panels/WorkOrdersPanel.tsx b/apps/frontend/src/pages/admin/panels/WorkOrdersPanel.tsx index d31bd9b9..398c0d63 100644 --- a/apps/frontend/src/pages/admin/panels/WorkOrdersPanel.tsx +++ b/apps/frontend/src/pages/admin/panels/WorkOrdersPanel.tsx @@ -566,11 +566,13 @@ export default function WorkOrdersPanel() { return runAction( () => publishAdminWorkOrder(row.workOrderId), (response) => - response.data.hallVisible - ? response.data.voucherConsume.voucherCount > 0 - ? '电子凭证已自动核销,订单已展示在大厅' - : '订单已展示在大厅' - : '订单已进入大厅等待队列', + !response.data.voucherConsume.ok + ? '订单已发布,但电子凭证核销未完成' + : response.data.hallVisible + ? response.data.voucherConsume.voucherCount > 0 + ? '订单已发布,电子凭证已顺带核销' + : '订单已展示在大厅' + : '订单已进入大厅等待队列', ) } @@ -582,11 +584,11 @@ export default function WorkOrdersPanel() { let skipFutureConfirm = false modal.confirm({ - title: '确认发布并核销该订单?', + title: '确认发布该订单?', content: (
- 如订单关联电子凭证,系统会先自动核销,成功后再发布到大厅。核销后即使下架也不会自动恢复凭证。 + 如订单关联电子凭证,系统会在发布后顺带尝试核销;核销失败不影响订单发布。 { @@ -597,7 +599,7 @@ export default function WorkOrdersPanel() {
), - okText: '发布并核销', + okText: '发布订单', cancelText: '取消', onOk: () => { if (skipFutureConfirm) setSkipPublishConfirm()