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
+123 -64
View File
@@ -1,21 +1,87 @@
import type { Actor } from "@/lib/actor"
import { allow, deny } from "@/lib/decision"
import { resolveOwnerShop } from "@/lib/domain/resolve-current-shop"
import type { ApiDecision } from "@/lib/errors"
import { useAuthStore } from "@/store/auth"
import { useOrderStore } from "@/store/orders"
import { useShopStore } from "@/store/shops"
import { isApiError, toApiError, type ApiDecision } from "@/lib/errors"
import type { Order, OrderStatus } from "@/lib/types"
export function listOrders() {
return useOrderStore.getState().orders
import { httpJson } from "./http"
type Paginated<T> = {
items: T[]
meta: {
total: number
offset: number
limit: number
}
}
export function getOrderById(orderId: string) {
return useOrderStore.getState().orders.find((order) => order.id === orderId)
export type ListOrdersOptions = {
role?: "consumer" | "player" | "owner"
status?: OrderStatus
offset?: number
limit?: number
}
export function listOrdersByConsumer(consumerId: string) {
return useOrderStore.getState().orders.filter((order) => order.consumerId === consumerId)
type ActionResult = { decision: ApiDecision; order?: Order }
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 {
@@ -26,69 +92,62 @@ interface CreatePaidOrderInput {
note?: string
}
function resolveActorContext(): { actor?: Actor; decision: ApiDecision } {
const auth = useAuthStore.getState()
if (!auth.user?.id) {
return { decision: deny(401, "请先登录") }
}
const shopId =
auth.currentRole === "owner"
? resolveOwnerShop(auth.user.id, useShopStore.getState().shops)?.id
: undefined
return {
actor: {
userId: auth.user.id,
role: auth.currentRole,
shopId,
},
decision: allow(),
export async function createPaidOrder(input: CreatePaidOrderInput): Promise<ActionResult> {
try {
const res = await httpJson<unknown>("/api/v1/orders/paid", {
method: "POST",
cache: "no-store",
json: input,
})
const order = unwrapOrder(res)
if (!order) {
return { decision: deny(500, "订单创建失败") }
}
return { decision: allow(), order }
} catch (error) {
return { decision: denyFromError(error) }
}
}
export function createPaidOrder(input: CreatePaidOrderInput) {
const { actor, decision } = resolveActorContext()
if (!actor) return { decision }
return useOrderStore.getState().createPaidOrder(input, actor)
async function postOrderAction(orderId: string, action: string): Promise<ActionResult> {
try {
const res = await httpJson<unknown>(`/api/v1/orders/${encodeURIComponent(orderId)}/${action}`, {
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) {
const { actor, decision } = resolveActorContext()
if (!actor) return { decision }
return useOrderStore.getState().payOrder(orderId, actor)
export async function payOrder(orderId: string): Promise<ActionResult> {
return postOrderAction(orderId, "pay")
}
export function acceptOrder(orderId: string) {
const { actor, decision } = resolveActorContext()
if (!actor) return { decision }
return useOrderStore.getState().acceptOrder(orderId, actor)
export async function acceptOrder(orderId: string): Promise<ActionResult> {
return postOrderAction(orderId, "accept")
}
export function acceptOrderAsActor(orderId: string, actor: Actor) {
return useOrderStore.getState().acceptOrder(orderId, actor)
export async function requestClose(orderId: string): Promise<ActionResult> {
return postOrderAction(orderId, "request-close")
}
export function requestClose(orderId: string) {
const { actor, decision } = resolveActorContext()
if (!actor) return { decision }
return useOrderStore.getState().requestClose(orderId, actor)
export async function confirmClose(orderId: string): Promise<ActionResult> {
return postOrderAction(orderId, "confirm-close")
}
export function confirmClose(orderId: string) {
const { actor, decision } = resolveActorContext()
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)
export async function cancelPreAccept(orderId: string): Promise<ActionResult> {
return postOrderAction(orderId, "cancel")
}