修复订单发布与核销并发状态覆盖
This commit is contained in:
@@ -0,0 +1,262 @@
|
||||
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<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: {
|
||||
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: (
|
||||
<div>
|
||||
<Typography.Paragraph>
|
||||
- 如订单关联电子凭证,系统会先自动核销,成功后再发布到大厅。核销后即使下架也不会自动恢复凭证。
|
||||
+ 如订单关联电子凭证,系统会在发布后顺带尝试核销;核销失败不影响订单发布。
|
||||
</Typography.Paragraph>
|
||||
<Checkbox
|
||||
onChange={(event) => {
|
||||
@@ -597,7 +599,7 @@ export default function WorkOrdersPanel() {
|
||||
</Checkbox>
|
||||
</div>
|
||||
),
|
||||
- okText: '发布并核销',
|
||||
+ okText: '发布订单',
|
||||
cancelText: '取消',
|
||||
onOk: () => {
|
||||
if (skipFutureConfirm) setSkipPublishConfirm()
|
||||
Reference in New Issue
Block a user