import type { Actor } from "@/lib/actor" import { ORDER_ACCEPT_TIMEOUT_MS, ORDER_CLOSE_TIMEOUT_MS, ORDER_REVIEW_TIMEOUT_MS, } from "@/lib/config/demo-timers" 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 { 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 } const orderTimeouts = new Map>() const pendingCreations = new Set() function clearOrderTimeout(orderId: string) { const timer = orderTimeouts.get(orderId) if (!timer) return clearTimeout(timer) orderTimeouts.delete(orderId) } 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 scheduleOrderTimeout(orderId: string, status: OrderStatus) { clearOrderTimeout(orderId) if (status !== "pending_accept" && status !== "pending_close" && status !== "pending_review") { return } const timeoutMap: Record<"pending_accept" | "pending_close" | "pending_review", 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() const order = state.orders.find((item) => item.id === orderId) if (!order || order.status !== status) { orderTimeouts.delete(orderId) return } if (status === "pending_accept") { state.autoTimeoutPendingAccept(orderId) } else if (status === "pending_close") { state.autoTimeoutPendingClose(orderId) } else if (status === "pending_review") { state.autoTimeoutPendingReview(orderId) } orderTimeouts.delete(orderId) }, timeoutMs) orderTimeouts.set(orderId, timer) } 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 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], })) scheduleOrderTimeout(order.id, order.status) 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"), } }) useOrderStore.subscribe((state, prevState) => { state.orders.forEach((order) => { const prevOrder = prevState.orders.find((item) => item.id === order.id) if (!prevOrder || prevOrder.status !== order.status) { scheduleOrderTimeout(order.id, order.status) } }) prevState.orders.forEach((order) => { if (!state.orders.some((item) => item.id === order.id)) { clearOrderTimeout(order.id) } }) })