feat(orders): migrate orders to backend API

This commit is contained in:
zetaloop
2026-02-28 18:13:42 +08:00
parent e94a7e68ff
commit 9739c94bdc
9 changed files with 263 additions and 130 deletions
+21 -1
View File
@@ -19,7 +19,8 @@ export default function DashboardPage() {
const [player, setPlayer] = useState<Player | null>(null) const [player, setPlayer] = useState<Player | null>(null)
const [shop, setShop] = useState<Shop | null>(null) const [shop, setShop] = useState<Shop | null>(null)
const [services, setServices] = useState<PlayerService[]>([]) const [services, setServices] = useState<PlayerService[]>([])
const recentOrders = listOrders().slice(0, 3) const [orders, setOrders] = useState<Awaited<ReturnType<typeof listOrders>>>([])
const recentOrders = orders.slice(0, 3)
useEffect(() => { useEffect(() => {
let cancelled = false let cancelled = false
@@ -43,6 +44,25 @@ export default function DashboardPage() {
} }
}, []) }, [])
useEffect(() => {
let cancelled = false
;(async () => {
try {
const orders = await Promise.resolve(listOrders())
if (cancelled) return
setOrders(orders)
} catch {
if (cancelled) return
setOrders([])
}
})()
return () => {
cancelled = true
}
}, [])
const totalOrders = isOwner ? (shop?.totalOrders ?? 0) : (player?.totalOrders ?? 0) const totalOrders = isOwner ? (shop?.totalOrders ?? 0) : (player?.totalOrders ?? 0)
const rating = isOwner ? (shop?.rating ?? 0) : (player?.rating ?? 0) const rating = isOwner ? (shop?.rating ?? 0) : (player?.rating ?? 0)
const playerCount = shop?.playerCount ?? 0 const playerCount = shop?.playerCount ?? 0
+5 -3
View File
@@ -14,9 +14,9 @@ import { useEffect, useState } from "react"
export default function CommunityPage() { export default function CommunityPage() {
const [games, setGames] = useState<Game[]>([]) const [games, setGames] = useState<Game[]>([])
const [players, setPlayers] = useState<Player[]>([]) const [players, setPlayers] = useState<Player[]>([])
const [orders, setOrders] = useState<Awaited<ReturnType<typeof listOrders>>>([])
const [posts, setPosts] = useState<Post[]>([]) const [posts, setPosts] = useState<Post[]>([])
const [postsLoading, setPostsLoading] = useState(true) const [postsLoading, setPostsLoading] = useState(true)
const orders = listOrders()
const [sortMode, setSortMode] = useState<"latest" | "hot">("latest") const [sortMode, setSortMode] = useState<"latest" | "hot">("latest")
const [selectedGame, setSelectedGame] = useState<string | null>(null) const [selectedGame, setSelectedGame] = useState<string | null>(null)
@@ -26,11 +26,12 @@ export default function CommunityPage() {
setPostsLoading(true) setPostsLoading(true)
Promise.all([listGames(), listPlayers(), listPosts()]) Promise.all([listGames(), listPlayers(), Promise.resolve(listOrders()), listPosts()])
.then(([gamesItems, playersItems, postsItems]) => { .then(([gamesItems, playersItems, ordersItems, postsItems]) => {
if (cancelled) return if (cancelled) return
setGames(gamesItems) setGames(gamesItems)
setPlayers(playersItems) setPlayers(playersItems)
setOrders(ordersItems)
setPosts(postsItems) setPosts(postsItems)
setPostsLoading(false) setPostsLoading(false)
}) })
@@ -38,6 +39,7 @@ export default function CommunityPage() {
if (cancelled) return if (cancelled) return
setGames([]) setGames([])
setPlayers([]) setPlayers([])
setOrders([])
setPosts([]) setPosts([])
setPostsLoading(false) setPostsLoading(false)
}) })
+4 -10
View File
@@ -5,7 +5,7 @@ import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
import { Badge } from "@/components/ui/badge" import { Badge } from "@/components/ui/badge"
import { Card, CardContent, CardHeader } from "@/components/ui/card" import { Card, CardContent, CardHeader } from "@/components/ui/card"
import { Separator } from "@/components/ui/separator" import { Separator } from "@/components/ui/separator"
import { getOrderById, getPlayerById, getPostById } from "@/lib/api" import { getPostById } from "@/lib/api"
import { roleLabels } from "@/lib/constants" import { roleLabels } from "@/lib/constants"
import { ArrowLeft, Pin, Star } from "lucide-react" import { ArrowLeft, Pin, Star } from "lucide-react"
import Image from "next/image" import Image from "next/image"
@@ -17,9 +17,6 @@ export default async function PostDetailPage({ params }: { params: Promise<{ id:
const post = await getPostById(id) const post = await getPostById(id)
if (!post) notFound() if (!post) notFound()
const linkedOrder = post.linkedOrderId ? getOrderById(post.linkedOrderId) : null
const linkedPlayer = linkedOrder ? await getPlayerById(linkedOrder.playerId) : null
return ( return (
<div className="container mx-auto max-w-2xl px-4 py-8 space-y-6"> <div className="container mx-auto max-w-2xl px-4 py-8 space-y-6">
<Link <Link
@@ -65,17 +62,14 @@ export default async function PostDetailPage({ params }: { params: Promise<{ id:
</div> </div>
)} )}
{linkedOrder && ( {post.linkedOrderId && (
<Link href={`/order/${linkedOrder.id}`}> <Link href={`/order/${post.linkedOrderId}`}>
<div className="rounded-lg border bg-muted/30 p-3 text-sm hover:bg-muted/50 transition-colors"> <div className="rounded-lg border bg-muted/30 p-3 text-sm hover:bg-muted/50 transition-colors">
<div className="flex items-center gap-2 mb-1"> <div className="flex items-center gap-2 mb-1">
<Star className="h-3.5 w-3.5 text-yellow-500" /> <Star className="h-3.5 w-3.5 text-yellow-500" />
<span className="font-medium"></span> <span className="font-medium"></span>
</div> </div>
<p className="text-muted-foreground text-xs"> <p className="text-muted-foreground text-xs"></p>
{linkedOrder.service.gameName} · {linkedOrder.service.title} · {" "}
{linkedPlayer?.rating ?? "--"}
</p>
</div> </div>
</Link> </Link>
)} )}
+35 -2
View File
@@ -4,11 +4,11 @@ import OrderActions from "@/components/order-actions"
import { Badge } from "@/components/ui/badge" import { Badge } from "@/components/ui/badge"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Separator } from "@/components/ui/separator" import { Separator } from "@/components/ui/separator"
import { getOrderById } from "@/lib/api"
import { ORDER_ACCEPT_TIMEOUT_MS, ORDER_CLOSE_TIMEOUT_MS } from "@/lib/config/demo-timers" import { ORDER_ACCEPT_TIMEOUT_MS, ORDER_CLOSE_TIMEOUT_MS } from "@/lib/config/demo-timers"
import { statusLabels } from "@/lib/constants" import { statusLabels } from "@/lib/constants"
import type { OrderStatus } from "@/lib/types" import type { OrderStatus } from "@/lib/types"
import { useChatStore } from "@/store/chat" import { useChatStore } from "@/store/chat"
import { useOrderStore } from "@/store/orders"
import { useReviewStore } from "@/store/reviews" import { useReviewStore } from "@/store/reviews"
import { ArrowLeft, CheckCircle, Clock, Star } from "lucide-react" import { ArrowLeft, CheckCircle, Clock, Star } from "lucide-react"
import Link from "next/link" import Link from "next/link"
@@ -35,7 +35,6 @@ const cancelledStatusSteps: OrderStatus[] = ["pending_payment", "pending_accept"
export default function OrderDetailPage({ params }: { params: Promise<{ id: string }> }) { export default function OrderDetailPage({ params }: { params: Promise<{ id: string }> }) {
const { id } = use(params) const { id } = use(params)
const order = useOrderStore((state) => state.orders.find((item) => item.id === id))
const sessions = useChatStore((state) => state.sessions) const sessions = useChatStore((state) => state.sessions)
const allReviews = useReviewStore((state) => state.reviews) const allReviews = useReviewStore((state) => state.reviews)
// Filtering is deferred to useMemo after reading the raw store array. // Filtering is deferred to useMemo after reading the raw store array.
@@ -43,8 +42,34 @@ export default function OrderDetailPage({ params }: { params: Promise<{ id: stri
// Returning a fresh filtered array from the selector can re-trigger updates // Returning a fresh filtered array from the selector can re-trigger updates
// and loop under useSyncExternalStore (pmndrs/zustand#1936, #3155). // and loop under useSyncExternalStore (pmndrs/zustand#1936, #3155).
const reviews = useMemo(() => allReviews.filter((item) => item.orderId === id), [allReviews, id]) const reviews = useMemo(() => allReviews.filter((item) => item.orderId === id), [allReviews, id])
const [order, setOrder] = useState<Awaited<ReturnType<typeof getOrderById>> | undefined>(
undefined,
)
const [loading, setLoading] = useState(true)
const [nowTs, setNowTs] = useState(0) const [nowTs, setNowTs] = useState(0)
useEffect(() => {
let cancelled = false
setLoading(true)
;(async () => {
try {
const order = await Promise.resolve(getOrderById(id))
if (cancelled) return
setOrder(order)
setLoading(false)
} catch {
if (cancelled) return
setOrder(undefined)
setLoading(false)
}
})()
return () => {
cancelled = true
}
}, [id])
useEffect(() => { useEffect(() => {
if (!order) return if (!order) return
if (order.status !== "pending_accept" && order.status !== "pending_close") return if (order.status !== "pending_accept" && order.status !== "pending_close") return
@@ -56,6 +81,12 @@ export default function OrderDetailPage({ params }: { params: Promise<{ id: stri
return () => clearInterval(timer) return () => clearInterval(timer)
}, [order]) }, [order])
if (loading) {
return (
<div className="container mx-auto py-8 px-4 text-center text-muted-foreground">...</div>
)
}
if (!order) { if (!order) {
return ( return (
<div className="container mx-auto py-8 px-4 text-center text-muted-foreground"> <div className="container mx-auto py-8 px-4 text-center text-muted-foreground">
@@ -257,6 +288,8 @@ export default function OrderDetailPage({ params }: { params: Promise<{ id: stri
<OrderActions <OrderActions
orderId={order.id} orderId={order.id}
order={order}
onOrderChange={(next) => setOrder(next)}
initialStatus={order.status} initialStatus={order.status}
chatSessionId={chatSession?.id} chatSessionId={chatSession?.id}
serviceId={order.service.id} serviceId={order.service.id}
+15 -26
View File
@@ -7,13 +7,11 @@ import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label" import { Label } from "@/components/ui/label"
import { Separator } from "@/components/ui/separator" import { Separator } from "@/components/ui/separator"
import { Textarea } from "@/components/ui/textarea" import { Textarea } from "@/components/ui/textarea"
import type { Actor } from "@/lib/actor"
import { getPlayerById, getServiceById } from "@/lib/api" import { getPlayerById, getServiceById } from "@/lib/api"
import { createPaidOrder } from "@/lib/api/orders"
import { notifySuccess } from "@/lib/toast" import { notifySuccess } from "@/lib/toast"
import type { Player, PlayerService } from "@/lib/types" import type { Player, PlayerService } from "@/lib/types"
import { useRequireAuth } from "@/lib/use-require-auth" import { useRequireAuth } from "@/lib/use-require-auth"
import { useAuthStore } from "@/store/auth"
import { useOrderStore } from "@/store/orders"
import { useWalletStore } from "@/store/wallet" import { useWalletStore } from "@/store/wallet"
import { ArrowLeft, CheckCircle, CreditCard, ShieldCheck } from "lucide-react" import { ArrowLeft, CheckCircle, CreditCard, ShieldCheck } from "lucide-react"
import Link from "next/link" import Link from "next/link"
@@ -24,7 +22,6 @@ export default function NewOrderPage() {
const router = useRouter() const router = useRouter()
const searchParams = useSearchParams() const searchParams = useSearchParams()
const { requireAuth } = useRequireAuth() const { requireAuth } = useRequireAuth()
const createPaidOrder = useOrderStore((state) => state.createPaidOrder)
const balance = useWalletStore((state) => state.balance) const balance = useWalletStore((state) => state.balance)
const serviceId = searchParams.get("serviceId") const serviceId = searchParams.get("serviceId")
@@ -234,34 +231,26 @@ export default function NewOrderPage() {
disabled={balance < totalPrice} disabled={balance < totalPrice}
onClick={() => onClick={() =>
requireAuth(() => { requireAuth(() => {
const authUser = useAuthStore.getState().user Promise.resolve(
if (!authUser) return createPaidOrder({
const actor: Actor = {
userId: authUser.id,
role: "consumer",
}
const result = createPaidOrder(
{
playerId: player.id, playerId: player.id,
serviceId: service.id, serviceId: service.id,
shopId: player.shopId, shopId: player.shopId,
quantity, quantity,
note, note,
}, }),
actor, ).then((result) => {
) if (!result.decision.ok || !result.order) {
if (!result.decision.ok || !result.order) { return
return }
} const nextOrder = result.order
const nextOrder = result.order
setSubmitted(true) setSubmitted(true)
notifySuccess("下单成功") notifySuccess("下单成功")
setTimeout(() => { setTimeout(() => {
router.push(`/order/${nextOrder.id}`) router.push(`/order/${nextOrder.id}`)
}, 800) }, 800)
})
}) })
} }
> >
+22 -3
View File
@@ -4,6 +4,7 @@ import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import { listOrders } from "@/lib/api"
import { statusLabels } from "@/lib/constants" import { statusLabels } from "@/lib/constants"
import { import {
isActiveOrder, isActiveOrder,
@@ -16,11 +17,10 @@ import type { OrderStatus, UserRole } from "@/lib/types"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
import { useAuthStore } from "@/store/auth" import { useAuthStore } from "@/store/auth"
import { useChatStore } from "@/store/chat" import { useChatStore } from "@/store/chat"
import { useOrderStore } from "@/store/orders"
import { useShopStore } from "@/store/shops" import { useShopStore } from "@/store/shops"
import { Clock, MessageSquare, RefreshCw } from "lucide-react" import { Clock, MessageSquare, RefreshCw } from "lucide-react"
import Link from "next/link" import Link from "next/link"
import { useState } from "react" import { useEffect, useState } from "react"
const statusColors: Record<OrderStatus, string> = { const statusColors: Record<OrderStatus, string> = {
pending_payment: "bg-yellow-100 text-yellow-800", pending_payment: "bg-yellow-100 text-yellow-800",
@@ -82,9 +82,28 @@ function OrderListContent({
ownerShopId?: string ownerShopId?: string
}) { }) {
const [tab, setTab] = useState<TabFilter | "pending">("all") const [tab, setTab] = useState<TabFilter | "pending">("all")
const orders = useOrderStore((state) => state.orders) const [orders, setOrders] = useState<Awaited<ReturnType<typeof listOrders>>>([])
const sessions = useChatStore((state) => state.sessions) const sessions = useChatStore((state) => state.sessions)
useEffect(() => {
let cancelled = false
;(async () => {
try {
const items = await Promise.resolve(listOrders())
if (cancelled) return
setOrders(items)
} catch {
if (cancelled) return
setOrders([])
}
})()
return () => {
cancelled = true
}
}, [])
const tabs = const tabs =
currentRole === "consumer" ? consumerTabs : currentRole === "player" ? playerTabs : ownerTabs currentRole === "consumer" ? consumerTabs : currentRole === "player" ? playerTabs : ownerTabs
+30 -15
View File
@@ -10,7 +10,7 @@ import {
} from "@/lib/api/orders" } from "@/lib/api/orders"
import type { ApiDecision } from "@/lib/errors" import type { ApiDecision } from "@/lib/errors"
import { notifyInfo, notifySuccess } from "@/lib/toast" import { notifyInfo, notifySuccess } from "@/lib/toast"
import type { OrderStatus } from "@/lib/types" import type { Order, OrderStatus } from "@/lib/types"
import { useAuthStore } from "@/store/auth" import { useAuthStore } from "@/store/auth"
import { useChatStore } from "@/store/chat" import { useChatStore } from "@/store/chat"
import { useOrderStore } from "@/store/orders" import { useOrderStore } from "@/store/orders"
@@ -29,6 +29,8 @@ import { useCallback } from "react"
interface OrderActionsProps { interface OrderActionsProps {
orderId: string orderId: string
order?: Order
onOrderChange?: (order: Order) => void
initialStatus: OrderStatus initialStatus: OrderStatus
chatSessionId?: string chatSessionId?: string
serviceId: string serviceId: string
@@ -40,12 +42,15 @@ function showFeedback(message: string) {
export default function OrderActions({ export default function OrderActions({
orderId, orderId,
order: orderProp,
onOrderChange,
initialStatus, initialStatus,
chatSessionId, chatSessionId,
serviceId, serviceId,
}: OrderActionsProps) { }: OrderActionsProps) {
const currentUserId = useAuthStore((state) => state.user?.id) const currentUserId = useAuthStore((state) => state.user?.id)
const order = useOrderStore((state) => state.orders.find((item) => item.id === orderId)) const storeOrder = useOrderStore((state) => state.orders.find((item) => item.id === orderId))
const order = orderProp ?? storeOrder
const sessions = useChatStore((state) => state.sessions) const sessions = useChatStore((state) => state.sessions)
const dispatchMode = useShopStore((state) => { const dispatchMode = useShopStore((state) => {
if (!order?.shopId) return "manual" if (!order?.shopId) return "manual"
@@ -60,7 +65,9 @@ export default function OrderActions({
const isConsumer = order?.consumerId === currentUserId const isConsumer = order?.consumerId === currentUserId
const isPlayer = order?.playerId === currentUserId const isPlayer = order?.playerId === currentUserId
const handleDecision = useCallback((okMessage: string, result: { decision: ApiDecision }) => { type ActionResult = { decision: ApiDecision; order?: Order }
const handleDecision = useCallback((okMessage: string, result: ActionResult) => {
if (result.decision.ok) { if (result.decision.ok) {
showFeedback(okMessage) showFeedback(okMessage)
return return
@@ -69,6 +76,20 @@ export default function OrderActions({
notifyInfo(result.decision.error.msg) notifyInfo(result.decision.error.msg)
}, []) }, [])
const runAction = useCallback(
(okMessage: string, actionCall: ActionResult | Promise<ActionResult>) => {
Promise.resolve(actionCall)
.then((result) => {
handleDecision(okMessage, result)
if (result.order) onOrderChange?.(result.order)
})
.catch(() => {
notifyInfo("操作失败")
})
},
[handleDecision, onOrderChange],
)
return ( return (
<div className="flex gap-2 flex-wrap"> <div className="flex gap-2 flex-wrap">
{status === "pending_payment" && isConsumer && ( {status === "pending_payment" && isConsumer && (
@@ -80,8 +101,7 @@ export default function OrderActions({
notifyInfo("请先登录") notifyInfo("请先登录")
return return
} }
const result = cancelPreAccept(orderId) runAction("订单已取消", cancelPreAccept(orderId))
handleDecision("订单已取消", result)
}} }}
> >
<XCircle className="mr-1 h-4 w-4" /> <XCircle className="mr-1 h-4 w-4" />
@@ -93,8 +113,7 @@ export default function OrderActions({
notifyInfo("请先登录") notifyInfo("请先登录")
return return
} }
const result = payOrder(orderId) runAction("订单支付成功", payOrder(orderId))
handleDecision("订单支付成功", result)
}} }}
> >
<CheckCircle2 className="mr-1 h-4 w-4" /> <CheckCircle2 className="mr-1 h-4 w-4" />
@@ -113,8 +132,7 @@ export default function OrderActions({
notifyInfo("请先登录") notifyInfo("请先登录")
return return
} }
const result = cancelPreAccept(orderId) runAction("订单已取消", cancelPreAccept(orderId))
handleDecision("订单已取消", result)
}} }}
> >
<XCircle className="mr-1 h-4 w-4" /> <XCircle className="mr-1 h-4 w-4" />
@@ -134,8 +152,7 @@ export default function OrderActions({
notifyInfo("请先登录") notifyInfo("请先登录")
return return
} }
const result = acceptOrder(orderId) runAction("已接单", acceptOrder(orderId))
handleDecision("已接单", result)
}} }}
> >
<CheckCircle2 className="mr-1 h-4 w-4" /> <CheckCircle2 className="mr-1 h-4 w-4" />
@@ -163,8 +180,7 @@ export default function OrderActions({
return return
} }
const result = requestClose(orderId) runAction("已发起结单", requestClose(orderId))
handleDecision("已发起结单", result)
}} }}
> >
@@ -187,8 +203,7 @@ export default function OrderActions({
notifyInfo("请先登录") notifyInfo("请先登录")
return return
} }
const result = confirmClose(orderId) runAction("已确认结单", confirmClose(orderId))
handleDecision("已确认结单", result)
}} }}
> >
+123 -64
View File
@@ -1,21 +1,87 @@
import type { Actor } from "@/lib/actor"
import { allow, deny } from "@/lib/decision" import { allow, deny } from "@/lib/decision"
import { resolveOwnerShop } from "@/lib/domain/resolve-current-shop" import { isApiError, toApiError, type ApiDecision } from "@/lib/errors"
import type { ApiDecision } from "@/lib/errors" import type { Order, OrderStatus } from "@/lib/types"
import { useAuthStore } from "@/store/auth"
import { useOrderStore } from "@/store/orders"
import { useShopStore } from "@/store/shops"
export function listOrders() { import { httpJson } from "./http"
return useOrderStore.getState().orders
type Paginated<T> = {
items: T[]
meta: {
total: number
offset: number
limit: number
}
} }
export function getOrderById(orderId: string) { export type ListOrdersOptions = {
return useOrderStore.getState().orders.find((order) => order.id === orderId) role?: "consumer" | "player" | "owner"
status?: OrderStatus
offset?: number
limit?: number
} }
export function listOrdersByConsumer(consumerId: string) { type ActionResult = { decision: ApiDecision; order?: Order }
return useOrderStore.getState().orders.filter((order) => order.consumerId === consumerId)
function withOffsetLimit(path: string, options?: ListOrdersOptions): string {
const offset = options?.offset ?? 0
const limit = options?.limit ?? 1000
const searchParams = new URLSearchParams({
offset: String(offset),
limit: String(limit),
})
if (options?.role) searchParams.set("role", options.role)
if (options?.status) searchParams.set("status", options.status)
return `${path}?${searchParams.toString()}`
}
function unwrapOrder(value: unknown): Order | undefined {
if (typeof value !== "object" || value === null) return undefined
if ("order" in value) {
const envelope = value as { order?: Order }
return envelope.order
}
return value as Order
}
function denyFromError(error: unknown): ApiDecision {
if (error instanceof Error && error.message === "UNAUTHORIZED") {
return deny(401, "请先登录")
}
const apiError = toApiError(error)
return deny(apiError.code, apiError.msg)
}
export async function listOrders(options?: ListOrdersOptions): Promise<Order[]> {
const res = await httpJson<Paginated<Order>>(withOffsetLimit("/api/v1/orders", options), {
cache: "no-store",
})
return res.items
}
export async function getOrderById(orderId: string): Promise<Order | undefined> {
try {
const res = await httpJson<unknown>(`/api/v1/orders/${encodeURIComponent(orderId)}`, {
cache: "no-store",
})
return unwrapOrder(res)
} catch (error) {
if (error instanceof Error && error.message === "UNAUTHORIZED") {
throw error
}
if (isApiError(error) && error.code === 404) {
return undefined
}
throw error
}
}
export async function listOrdersByConsumer(consumerId: string): Promise<Order[]> {
const items = await listOrders({ role: "consumer" })
return items.filter((order) => order.consumerId === consumerId)
} }
interface CreatePaidOrderInput { interface CreatePaidOrderInput {
@@ -26,69 +92,62 @@ interface CreatePaidOrderInput {
note?: string note?: string
} }
function resolveActorContext(): { actor?: Actor; decision: ApiDecision } { export async function createPaidOrder(input: CreatePaidOrderInput): Promise<ActionResult> {
const auth = useAuthStore.getState() try {
if (!auth.user?.id) { const res = await httpJson<unknown>("/api/v1/orders/paid", {
return { decision: deny(401, "请先登录") } method: "POST",
} cache: "no-store",
json: input,
const shopId = })
auth.currentRole === "owner" const order = unwrapOrder(res)
? resolveOwnerShop(auth.user.id, useShopStore.getState().shops)?.id if (!order) {
: undefined return { decision: deny(500, "订单创建失败") }
}
return { return { decision: allow(), order }
actor: { } catch (error) {
userId: auth.user.id, return { decision: denyFromError(error) }
role: auth.currentRole,
shopId,
},
decision: allow(),
} }
} }
export function createPaidOrder(input: CreatePaidOrderInput) { async function postOrderAction(orderId: string, action: string): Promise<ActionResult> {
const { actor, decision } = resolveActorContext() try {
if (!actor) return { decision } const res = await httpJson<unknown>(`/api/v1/orders/${encodeURIComponent(orderId)}/${action}`, {
return useOrderStore.getState().createPaidOrder(input, actor) method: "POST",
cache: "no-store",
})
const order = unwrapOrder(res)
if (order) {
return { decision: allow(), order }
}
const refetched = await getOrderById(orderId).catch(() => undefined)
if (refetched) {
return { decision: allow(), order: refetched }
}
return { decision: allow() }
} catch (error) {
return { decision: denyFromError(error) }
}
} }
export function payOrder(orderId: string) { export async function payOrder(orderId: string): Promise<ActionResult> {
const { actor, decision } = resolveActorContext() return postOrderAction(orderId, "pay")
if (!actor) return { decision }
return useOrderStore.getState().payOrder(orderId, actor)
} }
export function acceptOrder(orderId: string) { export async function acceptOrder(orderId: string): Promise<ActionResult> {
const { actor, decision } = resolveActorContext() return postOrderAction(orderId, "accept")
if (!actor) return { decision }
return useOrderStore.getState().acceptOrder(orderId, actor)
} }
export function acceptOrderAsActor(orderId: string, actor: Actor) { export async function requestClose(orderId: string): Promise<ActionResult> {
return useOrderStore.getState().acceptOrder(orderId, actor) return postOrderAction(orderId, "request-close")
} }
export function requestClose(orderId: string) { export async function confirmClose(orderId: string): Promise<ActionResult> {
const { actor, decision } = resolveActorContext() return postOrderAction(orderId, "confirm-close")
if (!actor) return { decision }
return useOrderStore.getState().requestClose(orderId, actor)
} }
export function confirmClose(orderId: string) { export async function cancelPreAccept(orderId: string): Promise<ActionResult> {
const { actor, decision } = resolveActorContext() return postOrderAction(orderId, "cancel")
if (!actor) return { decision }
return useOrderStore.getState().confirmClose(orderId, actor)
}
export function cancelPreAccept(orderId: string) {
const { actor, decision } = resolveActorContext()
if (!actor) return { decision }
return useOrderStore.getState().cancelPreAccept(orderId, actor)
}
export function markDisputed(orderId: string) {
const { actor, decision } = resolveActorContext()
if (!actor) return { decision }
return useOrderStore.getState().markDisputed(orderId, actor)
} }
+8 -6
View File
@@ -3,14 +3,16 @@ import { useAuthStore } from "@/store/auth"
import { describe, expect, it } from "vitest" import { describe, expect, it } from "vitest"
describe("lib/api/orders", () => { describe("lib/api/orders", () => {
it("returns { decision: { ok:false, error:{code,msg} } } when unauthenticated", () => { it("returns { decision: { ok:false, error:{code,msg} } } when unauthenticated", async () => {
useAuthStore.getState().logout() useAuthStore.getState().logout()
const res = createPaidOrder({ const res = await Promise.resolve(
playerId: "1005", createPaidOrder({
serviceId: "5001", playerId: "1005",
quantity: 1, serviceId: "5001",
}) quantity: 1,
}),
)
expect(res).toEqual({ expect(res).toEqual({
decision: { decision: {