From cf0fea9926aa317c2469cdb153713c8fe071456f Mon Sep 17 00:00:00 2001 From: zetaloop Date: Fri, 1 May 2026 04:13:55 +0800 Subject: [PATCH] refactor(orders): replace local state machine with minimal cache MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Strip store/orders.ts to a thin local cache with setOrders and updateOrder only. Remove all client-side state transition logic, actor validation, chat sync, notification generation, and wallet integration — these are now handled by the backend API. Fix components/order-actions.tsx and stores that depended on the removed order store methods (markDisputed, autoTimeout*). --- app/(order)/order/[id]/page.tsx | 69 ++++-- app/(order)/orders/page.tsx | 40 ++-- components/order-actions.tsx | 117 +++++----- store/disputes.ts | 5 - store/orders.ts | 392 +------------------------------- store/reviews.ts | 17 -- 6 files changed, 135 insertions(+), 505 deletions(-) diff --git a/app/(order)/order/[id]/page.tsx b/app/(order)/order/[id]/page.tsx index 71a1f3e..fb79550 100644 --- a/app/(order)/order/[id]/page.tsx +++ b/app/(order)/order/[id]/page.tsx @@ -5,9 +5,10 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" import { EmptyState } from "@/components/ui/empty-state" import { Separator } from "@/components/ui/separator" import { StatusBadge } from "@/components/ui/status-badge" -import { getOrderById, listChatSessions, listReviewsByOrder } from "@/lib/api" +import { getOrderById, getPlayerById, getUserById, listReviewsByOrder } from "@/lib/api" import { statusLabels } from "@/lib/constants" -import type { OrderStatus } from "@/lib/types" +import type { OrderStatus, Player, User } from "@/lib/types" +import { useAuthStore } from "@/store/auth" import { ArrowLeft, CheckCircle, Clock, Star } from "lucide-react" import Link from "next/link" import { use, useEffect, useState } from "react" @@ -46,32 +47,15 @@ const statusVariants: Record = { export default function OrderDetailPage({ params }: { params: Promise<{ id: string }> }) { const { id } = use(params) - const [sessions, setSessions] = useState>>([]) + const currentUserId = useAuthStore((state) => state.user?.id) const [reviews, setReviews] = useState>>([]) + const [chatTarget, setChatTarget] = useState(null) + const [player, setPlayer] = useState(null) const [order, setOrder] = useState> | undefined>( undefined, ) const [loading, setLoading] = useState(true) - useEffect(() => { - let cancelled = false - - void (async () => { - try { - const sessions = await Promise.resolve(listChatSessions()) - if (cancelled) return - setSessions(sessions) - } catch { - if (cancelled) return - setSessions([]) - } - })() - - return () => { - cancelled = true - } - }, []) - useEffect(() => { let cancelled = false @@ -94,6 +78,42 @@ export default function OrderDetailPage({ params }: { params: Promise<{ id: stri } }, [id]) + useEffect(() => { + let cancelled = false + + void (async () => { + if (!order || !currentUserId) { + setPlayer(null) + setChatTarget(null) + return + } + + try { + setPlayer(null) + setChatTarget(null) + const player = await getPlayerById(String(order.playerId)) + if (!cancelled) setPlayer(player ?? null) + + if (String(order.consumerId) === currentUserId) { + if (!cancelled) setChatTarget(player?.user ?? null) + return + } + + if (player?.user.id === currentUserId) { + const consumer = await getUserById(String(order.consumerId)) + if (!cancelled) setChatTarget(consumer ?? null) + } + } catch { + if (!cancelled) setPlayer(null) + if (!cancelled) setChatTarget(null) + } + })() + + return () => { + cancelled = true + } + }, [currentUserId, order]) + useEffect(() => { let cancelled = false @@ -129,7 +149,6 @@ export default function OrderDetailPage({ params }: { params: Promise<{ id: stri ) } - const chatSession = sessions.find((session) => session.type === "order" && session.orderId === id) const statusSteps = order.status === "disputed" ? disputedStatusSteps @@ -137,6 +156,7 @@ export default function OrderDetailPage({ params }: { params: Promise<{ id: stri ? cancelledStatusSteps : normalStatusSteps const currentStepIndex = statusSteps.indexOf(order.status) + const isPlayerParticipant = player?.user.id === currentUserId return (
@@ -291,7 +311,8 @@ export default function OrderDetailPage({ params }: { params: Promise<{ id: stri order={order} onOrderChange={(next) => setOrder(next)} initialStatus={order.status} - chatSessionId={chatSession?.id} + chatTargetId={chatTarget?.id} + isPlayerParticipant={isPlayerParticipant} serviceId={order.service.id} />
diff --git a/app/(order)/orders/page.tsx b/app/(order)/orders/page.tsx index c1cabbf..ae18279 100644 --- a/app/(order)/orders/page.tsx +++ b/app/(order)/orders/page.tsx @@ -6,7 +6,7 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" import { EmptyState } from "@/components/ui/empty-state" import { StatusBadge } from "@/components/ui/status-badge" import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" -import { listChatSessions, listOrders } from "@/lib/api" +import { getPlayerById, listOrders } from "@/lib/api" import { statusLabels } from "@/lib/constants" import { isActiveOrder, @@ -111,7 +111,7 @@ function OrderListContent({ const orderRole = getOrderRole(currentRole) const [tab, setTab] = useState("all") const [orders, setOrders] = useState>>([]) - const [sessions, setSessions] = useState>>([]) + const [chatTargets, setChatTargets] = useState>({}) useEffect(() => { let cancelled = false @@ -143,27 +143,41 @@ function OrderListContent({ void (async () => { try { - const items = await Promise.resolve(listChatSessions()) + const entries = await Promise.all( + orders.map(async (order) => { + if (currentRole === "consumer") { + const player = await getPlayerById(String(order.playerId)) + return [String(order.id), player?.user.id] as const + } + if (currentRole === "player") + return [String(order.id), String(order.consumerId)] as const + return [String(order.id), undefined] as const + }), + ) if (cancelled) return - setSessions(items) + setChatTargets( + Object.fromEntries( + entries.filter((entry): entry is readonly [string, string] => Boolean(entry[1])), + ), + ) } catch { if (cancelled) return - setSessions([]) + setChatTargets({}) } })() return () => { cancelled = true } - }, []) + }, [currentRole, orders]) const tabs = currentRole === "consumer" ? consumerTabs : currentRole === "player" ? playerTabs : ownerTabs const roleFiltered = orders.filter((order) => { - if (currentRole === "consumer") return userId ? order.consumerId === userId : false - if (currentRole === "player") return userId ? order.playerId === userId : false - return ownerShopId ? order.shopId === ownerShopId : false + if (currentRole === "consumer") return userId ? String(order.consumerId) === userId : false + if (currentRole === "player") return true + return ownerShopId ? String(order.shopId) === ownerShopId : false }) const filtered = roleFiltered.filter((order) => { @@ -241,13 +255,11 @@ function OrderListContent({ if (order.status !== "in_progress" && order.status !== "pending_close") { return null } - const session = sessions.find( - (item) => item.type === "order" && item.orderId === order.id, - ) - if (!session) return null + const chatTargetId = chatTargets[String(order.id)] + if (!chatTargetId) return null return ( - - + )} {status === "pending_accept" && ( @@ -181,9 +166,9 @@ export default function OrderActions({ )} - {(status === "in_progress" || status === "pending_close") && resolvedChatSessionId && ( + {(status === "in_progress" || status === "pending_close") && resolvedChatTargetId && ( - + runAction("已发起结单", requestClose(orderId)) + }} + > + 发起结单 + + )} + {isParticipant && ( + + )} )} @@ -228,16 +217,18 @@ export default function OrderActions({ 确认结单 )} - + {isParticipant && ( + + )} )} - {status === "pending_review" && ( + {status === "pending_review" && isParticipant && ( )} - {status === "disputed" && ( + {status === "disputed" && isParticipant && ( diff --git a/store/disputes.ts b/store/disputes.ts index df92c2a..2be260d 100644 --- a/store/disputes.ts +++ b/store/disputes.ts @@ -108,11 +108,6 @@ export const useDisputeStore = create((set, get) => { return { decision: deny(403, "仅订单参与方可发起争议") } } - const markResult = useOrderStore.getState().markDisputed(input.orderId, actor) - if (!markResult.decision.ok) { - return { decision: markResult.decision } - } - const createdAt = new Date().toISOString() const dispute: DisputeRecord = { id: generateId("disp"), diff --git a/store/orders.ts b/store/orders.ts index 821fc08..ac44777 100644 --- a/store/orders.ts +++ b/store/orders.ts @@ -1,389 +1,17 @@ -import type { Actor } from "@/lib/actor" -import { allow, deny } from "@/lib/decision" -import { evaluateOrderTransition, type OrderAction } from "@/lib/domain/order-machine" -import type { ApiDecision } from "@/lib/errors" -import { generateId } from "@/lib/id" -import type { Order, OrderStatus } from "@/lib/types" -import { useAuthStore } from "@/store/auth" -import { useChatStore } from "@/store/chat" -import { useNotificationStore } from "@/store/notifications" -import { usePlayerStore } from "@/store/players" -import { useServiceStore } from "@/store/services" -import { useWalletStore } from "@/store/wallet" +import type { Order } from "@/lib/types" import { create } from "zustand" -interface CreateOrderInput { - playerId: string - serviceId: string - shopId?: string - quantity: number - note?: string -} - -type CreatePaidOrderInput = CreateOrderInput - -interface OrderMutationResult { - decision: ApiDecision - order?: Order -} - interface OrderState { orders: Order[] - createOrder: (input: CreateOrderInput, actor: Actor) => OrderMutationResult - createPaidOrder: (input: CreatePaidOrderInput, actor: Actor) => OrderMutationResult - payOrder: (orderId: string, actor: Actor) => OrderMutationResult - acceptOrder: (orderId: string, actor: Actor) => OrderMutationResult - requestClose: (orderId: string, actor: Actor) => OrderMutationResult - confirmClose: (orderId: string, actor: Actor) => OrderMutationResult - cancelPreAccept: (orderId: string, actor: Actor) => OrderMutationResult - markDisputed: (orderId: string, actor: Actor) => OrderMutationResult - autoTimeoutPendingAccept: (orderId: string) => OrderMutationResult - autoTimeoutPendingClose: (orderId: string) => OrderMutationResult - autoTimeoutPendingReview: (orderId: string) => OrderMutationResult - resolveDispute: (orderId: string) => OrderMutationResult + setOrders: (orders: Order[]) => void + updateOrder: (orderId: string, patch: Partial) => void } -const pendingCreations = new Set() - -function isParticipant(order: Order, userId: string) { - return order.consumerId === userId || order.playerId === userId -} - -function isOrderOwnerActor(order: Order, actor: Actor) { - return actor.role === "owner" && Boolean(order.shopId) && actor.shopId === order.shopId -} - -function validateActorForAction(order: Order, action: OrderAction, actor?: Actor): ApiDecision { - if (action.startsWith("AUTO_TIMEOUT_") || (action === "RESOLVE_DISPUTE" && !actor?.userId)) { - return allow() - } - - if (!actor?.userId) { - return deny(401, "请先登录") - } - - if (order.status === "disputed" && action !== "RESOLVE_DISPUTE") { - return deny(400, "争议处理中,暂不可执行此操作") - } - - if (action === "PAY" || action === "CANCEL_PRE_ACCEPT" || action === "CONFIRM_CLOSE") { - return order.consumerId === actor.userId ? allow() : deny(403, "仅下单客户可执行该操作") - } - - if (action === "ACCEPT") { - return order.playerId === actor.userId || isOrderOwnerActor(order, actor) - ? allow() - : deny(403, "仅该订单打手或所属店主可执行接单") - } - - if (action === "REQUEST_CLOSE" || action === "OPEN_DISPUTE" || action === "SUBMIT_REVIEW") { - return isParticipant(order, actor.userId) ? allow() : deny(403, "仅订单参与方可执行该操作") - } - - if (action === "RESOLVE_DISPUTE") { - return isOrderOwnerActor(order, actor) ? allow() : deny(403, "仅订单所属店主可执行该操作") - } - - return allow() -} - -function applyStatus(order: Order, status: OrderStatus): Order { - const now = new Date().toISOString() - - switch (status) { - case "in_progress": - return { - ...order, - status, - acceptedAt: order.acceptedAt ?? now, - } - case "pending_close": - case "pending_review": - return { - ...order, - status, - } - case "completed": - return { - ...order, - status, - completedAt: order.completedAt ?? now, - } - default: - return { - ...order, - status, - } - } -} - -function syncChatSession(order: Order, previousStatus: OrderStatus) { - const chatStore = useChatStore.getState() - if (order.status === "pending_payment") return - - if (order.status === "cancelled") { - const sessionExists = chatStore.sessions.some( - (session) => session.type === "order" && session.orderId === order.id, - ) - if (sessionExists) { - chatStore.ensureOrderSession(order) - } - return - } - - if (previousStatus !== order.status) { - chatStore.ensureOrderSession(order) - } -} - -function notifyOrderStatus(order: Order) { - if (!useAuthStore.getState().notificationPrefs.order) { - return - } - - const mapping: Partial> = { - pending_accept: { - title: "订单待接单", - content: `${order.service.title} 已支付,等待接单`, - }, - in_progress: { - title: "订单已接单", - content: `订单 ${order.service.title} 已开始服务`, - }, - pending_close: { - title: "订单发起结单", - content: `订单 ${order.id} 等待确认结单`, - }, - pending_review: { - title: "订单待评价", - content: "服务已结束,可提交双向评价", - }, - completed: { - title: "订单已完成", - content: `订单 ${order.id} 已完成`, - }, - cancelled: { - title: "订单已取消", - content: `订单 ${order.id} 已取消`, - }, - disputed: { - title: "订单进入争议", - content: "已发起争议,等待平台处理", - }, - } - - const payload = mapping[order.status] - if (!payload) { - return - } - - useNotificationStore.getState().addNotification({ - type: "order", - title: payload.title, - content: payload.content, - link: `/order/${order.id}`, - }) -} - -export const useOrderStore = create((set, get) => { - const applyTransition = ( - orderId: string, - action: OrderAction, - actor?: Actor, - ): OrderMutationResult => { - const order = get().orders.find((item) => item.id === orderId) - if (!order) return { decision: deny(404, "订单不存在") } - - const actorDecision = validateActorForAction(order, action, actor) - if (!actorDecision.ok) return { decision: actorDecision } - - const transition = evaluateOrderTransition({ actor, order, action }) - if (!transition.decision.ok || !transition.nextStatus) { - return { decision: transition.decision } - } - const nextStatus = transition.nextStatus - - let previousOrder: Order | undefined - let updatedOrder: Order | undefined - +export const useOrderStore = create((set) => ({ + orders: [], + setOrders: (orders) => set({ orders }), + updateOrder: (orderId, patch) => set((state) => ({ - orders: state.orders.map((item) => { - if (item.id !== orderId) return item - previousOrder = item - updatedOrder = applyStatus(item, nextStatus) - return updatedOrder - }), - })) - - if (!updatedOrder || !previousOrder) { - return { decision: deny(404, "订单不存在") } - } - - if (previousOrder.status !== "completed" && updatedOrder.status === "completed") { - useWalletStore - .getState() - .addIncome(updatedOrder.id, updatedOrder.totalPrice, updatedOrder.shopId) - } - - if (previousOrder.status !== updatedOrder.status) { - notifyOrderStatus(updatedOrder) - } - - const shouldRefund = - previousOrder.status === "pending_accept" && - updatedOrder.status === "cancelled" && - (action === "CANCEL_PRE_ACCEPT" || action === "AUTO_TIMEOUT_PENDING_ACCEPT") - - if (shouldRefund) { - useWalletStore.getState().refundPayment(updatedOrder.id, updatedOrder.totalPrice) - } - - syncChatSession(updatedOrder, previousOrder.status) - return { decision: allow(), order: updatedOrder } - } - - return { - orders: [], - createOrder: (input, actor) => { - if (actor.role !== "consumer") { - return { decision: deny(403, "仅客户可下单") } - } - - const consumer = useAuthStore.getState().user - if (!consumer || consumer.id !== actor.userId) { - return { decision: deny(403, "仅本人可下单") } - } - - const service = useServiceStore - .getState() - .services.find((item) => item.id === input.serviceId) - if (!service) { - return { decision: deny(404, "服务不存在") } - } - - const player = usePlayerStore.getState().players.find((item) => item.id === input.playerId) - if (!player) { - return { decision: deny(404, "打手不存在") } - } - - if (service.playerId !== player.id) { - return { decision: deny(400, "服务与打手不匹配") } - } - - const resolvedShopId = input.shopId ?? player.shopId - if (input.shopId && player.shopId && input.shopId !== player.shopId) { - return { decision: deny(400, "店铺信息与打手不匹配") } - } - - const quantity = Number.isFinite(input.quantity) ? Math.floor(input.quantity) : Number.NaN - if (!quantity || quantity < 1) { - return { decision: deny(400, "数量不合法") } - } - - const totalPrice = service.price * quantity - const order: Order = { - id: generateId("ord"), - consumerId: consumer.id, - playerId: player.id, - shopId: resolvedShopId, - service, - status: "pending_payment", - totalPrice, - note: input.note?.trim() ? input.note.trim() : undefined, - createdAt: new Date().toISOString(), - } - - set((state) => ({ - orders: [order, ...state.orders], - })) - - return { decision: allow(), order } - }, - createPaidOrder: (input, actor) => { - if (actor.role !== "consumer") { - return { decision: deny(403, "仅客户可下单支付") } - } - - const consumer = useAuthStore.getState().user - if (!consumer || consumer.id !== actor.userId) { - return { decision: deny(403, "仅本人可下单支付") } - } - - const service = useServiceStore - .getState() - .services.find((item) => item.id === input.serviceId) - if (!service) { - return { decision: deny(404, "服务不存在") } - } - - const player = usePlayerStore.getState().players.find((item) => item.id === input.playerId) - if (!player) { - return { decision: deny(404, "打手不存在") } - } - - if (service.playerId !== player.id) { - return { decision: deny(400, "服务与打手不匹配") } - } - - const resolvedShopId = input.shopId ?? player.shopId - if (input.shopId && player.shopId && input.shopId !== player.shopId) { - return { decision: deny(400, "店铺信息与打手不匹配") } - } - - const quantity = Number.isFinite(input.quantity) ? Math.floor(input.quantity) : Number.NaN - if (!quantity || quantity < 1) { - return { decision: deny(400, "数量不合法") } - } - - const totalPrice = service.price * quantity - const dedupeKey = `${consumer.id}-${service.id}` - if (pendingCreations.has(dedupeKey)) { - return { decision: deny(400, "订单正在创建中,请勿重复提交") } - } - pendingCreations.add(dedupeKey) - const orderId = generateId("ord") - const paid = useWalletStore.getState().deductBalance(orderId, totalPrice) - if (!paid) { - pendingCreations.delete(dedupeKey) - return { decision: deny(400, "余额不足或订单已支付") } - } - - const order: Order = { - id: orderId, - consumerId: consumer.id, - playerId: player.id, - shopId: resolvedShopId, - service, - status: "pending_accept", - totalPrice, - note: input.note?.trim() ? input.note.trim() : undefined, - createdAt: new Date().toISOString(), - } - - set((state) => ({ - orders: [order, ...state.orders], - })) - - useChatStore.getState().ensureOrderSession(order) - notifyOrderStatus(order) - setTimeout(() => pendingCreations.delete(dedupeKey), 2000) - return { decision: allow(), order } - }, - payOrder: (orderId, actor) => { - const order = get().orders.find((item) => item.id === orderId) - if (!order) return { decision: deny(404, "订单不存在") } - const paid = useWalletStore.getState().deductBalance(orderId, order.totalPrice) - if (!paid) return { decision: deny(400, "余额不足或订单已支付") } - 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), - cancelPreAccept: (orderId, actor) => applyTransition(orderId, "CANCEL_PRE_ACCEPT", actor), - markDisputed: (orderId, actor) => applyTransition(orderId, "OPEN_DISPUTE", actor), - autoTimeoutPendingAccept: (orderId) => applyTransition(orderId, "AUTO_TIMEOUT_PENDING_ACCEPT"), - autoTimeoutPendingClose: (orderId) => applyTransition(orderId, "AUTO_TIMEOUT_PENDING_CLOSE"), - autoTimeoutPendingReview: (orderId) => applyTransition(orderId, "AUTO_TIMEOUT_PENDING_REVIEW"), - resolveDispute: (orderId) => applyTransition(orderId, "RESOLVE_DISPUTE"), - } -}) + orders: state.orders.map((o) => (o.id === orderId ? { ...o, ...patch } : o)), + })), +})) diff --git a/store/reviews.ts b/store/reviews.ts index 30b2233..7c4766f 100644 --- a/store/reviews.ts +++ b/store/reviews.ts @@ -2,7 +2,6 @@ import { allow, deny } from "@/lib/decision" import type { ApiDecision } from "@/lib/errors" import { generateId } from "@/lib/id" import type { Review } from "@/lib/types" -import { useOrderStore } from "@/store/orders" import { create } from "zustand" interface SubmitReviewInput { @@ -33,21 +32,6 @@ export const useReviewStore = create((set, get) => ({ return deny(400, "评分范围应为 1-5") } - const order = useOrderStore.getState().orders.find((item) => item.id === input.orderId) - if (!order) { - return deny(404, "订单不存在") - } - - if (order.status !== "pending_review") { - return deny(400, "仅待评价订单可提交评价") - } - - const isParticipant = - order.consumerId === input.fromUserId || order.playerId === input.fromUserId - if (!isParticipant) { - return deny(403, "仅订单参与方可评价") - } - const exists = get().hasUserReviewed(input.orderId, input.fromUserId) if (exists) { return deny(400, "该订单已提交过评价") @@ -75,7 +59,6 @@ export const useReviewStore = create((set, get) => ({ item.orderId === input.orderId ? { ...item, sealed: false } : item, ), })) - useOrderStore.getState().autoTimeoutPendingReview(input.orderId) } return allow()