fix(order): remove status bypass and wire pending_review lifecycle

This commit is contained in:
zetaloop
2026-02-23 11:05:38 +08:00
parent 4fce328ef1
commit da5574c5b3
2 changed files with 48 additions and 39 deletions
+47 -39
View File
@@ -1,5 +1,9 @@
import { create } from "zustand"
import { ORDER_ACCEPT_TIMEOUT_MS, ORDER_CLOSE_TIMEOUT_MS } from "@/lib/config/demo-timers"
import {
ORDER_ACCEPT_TIMEOUT_MS,
ORDER_CLOSE_TIMEOUT_MS,
ORDER_REVIEW_TIMEOUT_MS,
} from "@/lib/config/demo-timers"
import { evaluateOrderTransition, type OrderAction } from "@/lib/domain/order-machine"
import { generateId } from "@/lib/id"
import { allow, deny } from "@/lib/policy/assert"
@@ -11,6 +15,7 @@ import { useAuthStore } from "@/store/auth"
import { useChatStore } from "@/store/chat"
import { useNotificationStore } from "@/store/notifications"
import { useWalletStore } from "@/store/wallet"
import { useShopStore } from "@/store/shops"
interface CreateOrderInput {
consumerId: string
@@ -46,10 +51,12 @@ interface OrderState {
markDisputed: (orderId: string, actor: Actor) => OrderMutationResult
autoTimeoutPendingAccept: (orderId: string) => OrderMutationResult
autoTimeoutPendingClose: (orderId: string) => OrderMutationResult
updateOrderStatus: (orderId: string, status: OrderStatus) => void
autoTimeoutPendingReview: (orderId: string) => OrderMutationResult
resolveDispute: (orderId: string) => OrderMutationResult
}
const orderTimeouts = new Map<string, ReturnType<typeof setTimeout>>()
const pendingCreations = new Set<string>()
function clearOrderTimeout(orderId: string) {
const timer = orderTimeouts.get(orderId)
@@ -67,7 +74,7 @@ function isOrderOwnerActor(order: Order, actor: Actor) {
}
function validateActorForAction(order: Order, action: OrderAction, actor?: Actor): PolicyDecision {
if (action.startsWith("AUTO_TIMEOUT_")) {
if (action.startsWith("AUTO_TIMEOUT_") || (action === "RESOLVE_DISPUTE" && !actor?.userId)) {
return allow()
}
@@ -165,11 +172,16 @@ function syncChatSession(order: Order, previousStatus: OrderStatus) {
function scheduleOrderTimeout(orderId: string, status: OrderStatus) {
clearOrderTimeout(orderId)
if (status !== "pending_accept" && status !== "pending_close") {
if (status !== "pending_accept" && status !== "pending_close" && status !== "pending_review") {
return
}
const timeoutMs = status === "pending_accept" ? ORDER_ACCEPT_TIMEOUT_MS : ORDER_CLOSE_TIMEOUT_MS
const timeoutMap: Partial<Record<OrderStatus, number>> = {
pending_accept: ORDER_ACCEPT_TIMEOUT_MS,
pending_close: ORDER_CLOSE_TIMEOUT_MS,
pending_review: ORDER_REVIEW_TIMEOUT_MS,
}
const timeoutMs = timeoutMap[status]!
const timer = setTimeout(() => {
const state = useOrderStore.getState()
@@ -181,10 +193,10 @@ function scheduleOrderTimeout(orderId: string, status: OrderStatus) {
if (status === "pending_accept") {
state.autoTimeoutPendingAccept(orderId)
}
if (status === "pending_close") {
} else if (status === "pending_close") {
state.autoTimeoutPendingClose(orderId)
} else if (status === "pending_review") {
state.autoTimeoutPendingReview(orderId)
}
orderTimeouts.delete(orderId)
@@ -330,9 +342,15 @@ export const useOrderStore = create<OrderState>((set, get) => {
return { decision: deny("ROLE_FORBIDDEN", "仅客户可下单支付") }
}
const dedupeKey = `${input.consumerId}-${input.service.id}`
if (pendingCreations.has(dedupeKey)) {
return { decision: deny("DUPLICATE_REQUEST", "订单正在创建中,请勿重复提交") }
}
pendingCreations.add(dedupeKey)
const orderId = generateId("ord")
const paid = useWalletStore.getState().deductBalance(orderId, input.totalPrice)
if (!paid) {
pendingCreations.delete(dedupeKey)
return { decision: deny("PAYMENT_FAILED", "余额不足或订单已支付") }
}
@@ -357,9 +375,16 @@ export const useOrderStore = create<OrderState>((set, get) => {
useChatStore.getState().ensureOrderSession(order)
notifyOrderStatus(order)
setTimeout(() => pendingCreations.delete(dedupeKey), 2000)
return { decision: allow(), order }
},
payOrder: (orderId, actor) => applyTransition(orderId, "PAY", actor),
payOrder: (orderId, actor) => {
const order = get().orders.find((item) => item.id === orderId)
if (!order) return { decision: deny("NOT_FOUND", "订单不存在") }
const paid = useWalletStore.getState().deductBalance(orderId, order.totalPrice)
if (!paid) return { decision: deny("PAYMENT_FAILED", "余额不足或订单已支付") }
return applyTransition(orderId, "PAY", actor)
},
acceptOrder: (orderId, actor) => applyTransition(orderId, "ACCEPT", actor),
requestClose: (orderId, actor) => applyTransition(orderId, "REQUEST_CLOSE", actor),
confirmClose: (orderId, actor) => applyTransition(orderId, "CONFIRM_CLOSE", actor),
@@ -367,35 +392,8 @@ export const useOrderStore = create<OrderState>((set, get) => {
markDisputed: (orderId, actor) => applyTransition(orderId, "OPEN_DISPUTE", actor),
autoTimeoutPendingAccept: (orderId) => applyTransition(orderId, "AUTO_TIMEOUT_PENDING_ACCEPT"),
autoTimeoutPendingClose: (orderId) => applyTransition(orderId, "AUTO_TIMEOUT_PENDING_CLOSE"),
updateOrderStatus: (orderId, status) => {
set((state) => {
let previousOrder: Order | undefined
let updatedOrder: Order | undefined
const orders = state.orders.map((order) => {
if (order.id !== orderId) return order
previousOrder = order
updatedOrder = applyStatus(order, status)
return updatedOrder
})
if (previousOrder && updatedOrder) {
if (previousOrder.status !== "completed" && updatedOrder.status === "completed") {
useWalletStore
.getState()
.addIncome(updatedOrder.id, updatedOrder.totalPrice, updatedOrder.shopId)
}
if (previousOrder.status === "pending_accept" && updatedOrder.status === "cancelled") {
useWalletStore.getState().refundPayment(updatedOrder.id, updatedOrder.totalPrice)
}
syncChatSession(updatedOrder, previousOrder.status)
}
return { orders }
})
},
autoTimeoutPendingReview: (orderId) => applyTransition(orderId, "AUTO_TIMEOUT_PENDING_REVIEW"),
resolveDispute: (orderId) => applyTransition(orderId, "RESOLVE_DISPUTE"),
}
})
@@ -404,9 +402,19 @@ useOrderStore.subscribe((state, prevState) => {
const prevOrder = prevState.orders.find((item) => item.id === order.id)
if (!prevOrder || prevOrder.status !== order.status) {
scheduleOrderTimeout(order.id, order.status)
if (order.status === "pending_accept" && order.shopId) {
const shop = useShopStore.getState().shops.find((s) => s.id === order.shopId)
if (shop?.dispatchMode === "auto") {
setTimeout(() => {
const current = useOrderStore.getState().orders.find((o) => o.id === order.id)
if (!current || current.status !== "pending_accept") return
const actor: Actor = { userId: order.playerId, role: "player", shopId: order.shopId }
useOrderStore.getState().acceptOrder(order.id, actor)
}, 3000)
}
}
}
})
prevState.orders.forEach((order) => {
if (!state.orders.some((item) => item.id === order.id)) {
clearOrderTimeout(order.id)
+1
View File
@@ -105,6 +105,7 @@ export const useReviewStore = create<ReviewState>((set, get) => ({
item.orderId === input.orderId ? { ...item, sealed: false } : item,
),
}))
useOrderStore.getState().autoTimeoutPendingReview(input.orderId)
}
return allow()