From da5574c5b37af9311c02d92001bf2a8570545ce5 Mon Sep 17 00:00:00 2001 From: zetaloop Date: Mon, 23 Feb 2026 11:05:38 +0800 Subject: [PATCH] fix(order): remove status bypass and wire pending_review lifecycle --- store/orders.ts | 86 ++++++++++++++++++++++++++---------------------- store/reviews.ts | 1 + 2 files changed, 48 insertions(+), 39 deletions(-) diff --git a/store/orders.ts b/store/orders.ts index 5b35d52..ed2d958 100644 --- a/store/orders.ts +++ b/store/orders.ts @@ -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>() +const pendingCreations = new Set() 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> = { + 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((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((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((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) diff --git a/store/reviews.ts b/store/reviews.ts index 64aedd0..f97efc0 100644 --- a/store/reviews.ts +++ b/store/reviews.ts @@ -105,6 +105,7 @@ export const useReviewStore = create((set, get) => ({ item.orderId === input.orderId ? { ...item, sealed: false } : item, ), })) + useOrderStore.getState().autoTimeoutPendingReview(input.orderId) } return allow()