diff --git a/app/(order)/dispute/[id]/page.tsx b/app/(order)/dispute/[id]/page.tsx
index c4250ad..30a387d 100644
--- a/app/(order)/dispute/[id]/page.tsx
+++ b/app/(order)/dispute/[id]/page.tsx
@@ -16,6 +16,7 @@ import {
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
+import { DISPUTE_TO_RESOLVED_MS } from "@/lib/config/demo-timers"
import { Label } from "@/components/ui/label"
import { Separator } from "@/components/ui/separator"
import { Textarea } from "@/components/ui/textarea"
@@ -342,7 +343,9 @@ export default function DisputePage({ params }: { params: Promise<{ id: string }
争议已提交,请等待平台处理
-
平台将在 3 个工作日内审核你的争议申请。
+
+ 平台将在约 {Math.floor(DISPUTE_TO_RESOLVED_MS / 1000)} 秒内给出模拟处理结果。
+
返回订单详情
@@ -434,7 +437,7 @@ export default function DisputePage({ params }: { params: Promise<{ id: string }
· 提交争议后,订单资金将继续托管
· 聊天记录将作为证据保留
-
· 平台将在 3 个工作日内审核
+
· 平台将在约 {Math.floor(DISPUTE_TO_RESOLVED_MS / 1000)} 秒内完成模拟审核
· 对仲裁结果不满可申诉一次
diff --git a/app/(order)/order/[id]/page.tsx b/app/(order)/order/[id]/page.tsx
index 082b661..ba07590 100644
--- a/app/(order)/order/[id]/page.tsx
+++ b/app/(order)/order/[id]/page.tsx
@@ -6,6 +6,7 @@ import { use, useEffect, useMemo, useState } from "react"
import OrderActions from "@/components/order-actions"
import { Badge } from "@/components/ui/badge"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
+import { ORDER_ACCEPT_TIMEOUT_MS, ORDER_CLOSE_TIMEOUT_MS } from "@/lib/config/demo-timers"
import { Separator } from "@/components/ui/separator"
import { statusLabels } from "@/lib/constants"
import type { OrderStatus } from "@/lib/types"
@@ -85,11 +86,13 @@ export default function OrderDetailPage({ params }: { params: Promise<{ id: stri
order.status === "pending_accept"
? new Date(order.createdAt).getTime()
: new Date(order.closedAt ?? order.createdAt).getTime()
- const remainSeconds = Math.max(0, 30 - Math.floor((nowTs - base) / 1000))
+ const timeoutMs =
+ order.status === "pending_accept" ? ORDER_ACCEPT_TIMEOUT_MS : ORDER_CLOSE_TIMEOUT_MS
+ const remainSeconds = Math.max(0, Math.ceil((timeoutMs - (nowTs - base)) / 1000))
return order.status === "pending_accept"
- ? `若 30 秒内无人接单,订单将自动取消(剩余 ${remainSeconds} 秒)`
- : `若 30 秒内未确认,订单将自动进入待评价(剩余 ${remainSeconds} 秒)`
+ ? `若 ${Math.floor(ORDER_ACCEPT_TIMEOUT_MS / 1000)} 秒内无人接单,订单将自动取消(剩余 ${remainSeconds} 秒)`
+ : `若 ${Math.floor(ORDER_CLOSE_TIMEOUT_MS / 1000)} 秒内未确认,订单将自动进入待评价(剩余 ${remainSeconds} 秒)`
})()
return (
diff --git a/lib/config/demo-timers.ts b/lib/config/demo-timers.ts
new file mode 100644
index 0000000..ca87024
--- /dev/null
+++ b/lib/config/demo-timers.ts
@@ -0,0 +1,6 @@
+export const ORDER_ACCEPT_TIMEOUT_MS = 30_000
+export const ORDER_CLOSE_TIMEOUT_MS = 30_000
+export const ORDER_REVIEW_TIMEOUT_MS = 30_000
+
+export const DISPUTE_TO_REVIEWING_MS = 5_000
+export const DISPUTE_TO_RESOLVED_MS = 10_000
diff --git a/lib/domain/order-machine.ts b/lib/domain/order-machine.ts
new file mode 100644
index 0000000..f064076
--- /dev/null
+++ b/lib/domain/order-machine.ts
@@ -0,0 +1,164 @@
+import { allow, deny, requireAuth } from "@/lib/policy/assert"
+import type { Actor } from "@/lib/policy/actor"
+import type { PolicyDecision } from "@/lib/policy/decision"
+import type { OrderStatus } from "@/lib/types"
+
+export type OrderAction =
+ | "PAY"
+ | "ACCEPT"
+ | "REQUEST_CLOSE"
+ | "CONFIRM_CLOSE"
+ | "CANCEL_PRE_ACCEPT"
+ | "OPEN_DISPUTE"
+ | "RESOLVE_DISPUTE"
+ | "SUBMIT_REVIEW"
+ | "AUTO_TIMEOUT_PENDING_ACCEPT"
+ | "AUTO_TIMEOUT_PENDING_CLOSE"
+ | "AUTO_TIMEOUT_PENDING_REVIEW"
+
+export type OrderTransitionSideEffectType =
+ | "CLEAR_TIMEOUT"
+ | "SCHEDULE_TIMEOUT"
+ | "SYNC_CHAT_SESSION"
+ | "PAYOUT_INCOME"
+
+export interface OrderTransitionSideEffect {
+ type: OrderTransitionSideEffectType
+ status?: OrderStatus
+}
+
+interface TransitionContext {
+ actor?: Actor | null
+ order: {
+ status: OrderStatus
+ }
+ action: OrderAction
+}
+
+export interface OrderTransitionResult {
+ decision: PolicyDecision
+ nextStatus?: OrderStatus
+ sideEffects: OrderTransitionSideEffect[]
+}
+
+export const orderTransitionTable: Record<
+ OrderStatus,
+ Partial
>
+> = {
+ pending_payment: {
+ PAY: "pending_accept",
+ },
+ pending_accept: {
+ ACCEPT: "in_progress",
+ CANCEL_PRE_ACCEPT: "cancelled",
+ AUTO_TIMEOUT_PENDING_ACCEPT: "cancelled",
+ },
+ in_progress: {
+ REQUEST_CLOSE: "pending_close",
+ OPEN_DISPUTE: "disputed",
+ },
+ pending_close: {
+ CONFIRM_CLOSE: "pending_review",
+ OPEN_DISPUTE: "disputed",
+ AUTO_TIMEOUT_PENDING_CLOSE: "pending_review",
+ },
+ pending_review: {
+ SUBMIT_REVIEW: "completed",
+ AUTO_TIMEOUT_PENDING_REVIEW: "completed",
+ },
+ disputed: {
+ RESOLVE_DISPUTE: "pending_review",
+ },
+ completed: {},
+ cancelled: {},
+}
+
+function isAutoAction(action: OrderAction): boolean {
+ return action.startsWith("AUTO_TIMEOUT_")
+}
+
+function checkRolePermission(action: OrderAction, actor?: Actor | null): PolicyDecision {
+ if (isAutoAction(action)) {
+ return allow()
+ }
+
+ const authDecision = requireAuth(actor)
+ if (!authDecision.ok) {
+ return authDecision
+ }
+
+ if (!actor) {
+ return deny("AUTH_REQUIRED", "请先登录")
+ }
+
+ if (action === "PAY" || action === "CANCEL_PRE_ACCEPT") {
+ return actor.role === "consumer" ? allow() : deny("ROLE_FORBIDDEN", "仅客户可执行该操作")
+ }
+
+ if (action === "ACCEPT") {
+ return actor.role === "player" || actor.role === "owner"
+ ? allow()
+ : deny("ROLE_FORBIDDEN", "仅打手或店主可执行该操作")
+ }
+
+ if (action === "RESOLVE_DISPUTE") {
+ return actor.role === "owner" ? allow() : deny("ROLE_FORBIDDEN", "仅店主可执行该操作")
+ }
+
+ return actor.role === "consumer" || actor.role === "player"
+ ? allow()
+ : deny("ROLE_FORBIDDEN", "当前身份不可执行该操作")
+}
+
+function buildSideEffects(nextStatus: OrderStatus): OrderTransitionSideEffect[] {
+ const sideEffects: OrderTransitionSideEffect[] = [{ type: "CLEAR_TIMEOUT" }]
+
+ if (
+ nextStatus === "pending_accept" ||
+ nextStatus === "pending_close" ||
+ nextStatus === "pending_review"
+ ) {
+ sideEffects.push({ type: "SCHEDULE_TIMEOUT", status: nextStatus })
+ }
+
+ if (nextStatus !== "pending_payment" && nextStatus !== "cancelled") {
+ sideEffects.push({ type: "SYNC_CHAT_SESSION" })
+ }
+
+ if (nextStatus === "completed") {
+ sideEffects.push({ type: "PAYOUT_INCOME" })
+ }
+
+ return sideEffects
+}
+
+export function evaluateOrderTransition(context: TransitionContext): OrderTransitionResult {
+ const roleDecision = checkRolePermission(context.action, context.actor)
+ if (!roleDecision.ok) {
+ return {
+ decision: roleDecision,
+ sideEffects: [],
+ }
+ }
+
+ const nextStatus = orderTransitionTable[context.order.status][context.action]
+ if (!nextStatus) {
+ return {
+ decision: deny("INVALID_STATUS", "当前状态不可执行该操作"),
+ sideEffects: [],
+ }
+ }
+
+ if (nextStatus === context.order.status) {
+ return {
+ decision: deny("IDEMPOTENT_NOOP", "状态未变化"),
+ sideEffects: [],
+ }
+ }
+
+ return {
+ decision: allow(),
+ nextStatus,
+ sideEffects: buildSideEffects(nextStatus),
+ }
+}
diff --git a/store/disputes.ts b/store/disputes.ts
index 07fb543..a239f55 100644
--- a/store/disputes.ts
+++ b/store/disputes.ts
@@ -1,4 +1,5 @@
import { create } from "zustand"
+import { DISPUTE_TO_RESOLVED_MS, DISPUTE_TO_REVIEWING_MS } from "@/lib/config/demo-timers"
import { generateId } from "@/lib/id"
import { mockDisputes } from "@/lib/mock"
import type { Dispute } from "@/lib/types"
@@ -108,7 +109,7 @@ export const useDisputeStore = create((set, get) => {
}
}),
}))
- }, 5000)
+ }, DISPUTE_TO_REVIEWING_MS)
const toResolved = setTimeout(() => {
set((state) => ({
@@ -133,7 +134,7 @@ export const useDisputeStore = create((set, get) => {
}),
}))
clearProgressTimers(disputeId)
- }, 10000)
+ }, DISPUTE_TO_RESOLVED_MS)
progressTimers.set(disputeId, [toReviewing, toResolved])
}
diff --git a/store/orders.ts b/store/orders.ts
index b166b81..92205d0 100644
--- a/store/orders.ts
+++ b/store/orders.ts
@@ -1,4 +1,5 @@
import { create } from "zustand"
+import { ORDER_ACCEPT_TIMEOUT_MS, ORDER_CLOSE_TIMEOUT_MS } from "@/lib/config/demo-timers"
import { generateId } from "@/lib/id"
import { mockOrders } from "@/lib/mock"
import type { Order, OrderStatus, PlayerService } from "@/lib/types"
@@ -39,6 +40,8 @@ function scheduleOrderTimeout(orderId: string, status: OrderStatus) {
return
}
+ const timeoutMs = status === "pending_accept" ? ORDER_ACCEPT_TIMEOUT_MS : ORDER_CLOSE_TIMEOUT_MS
+
const timer = setTimeout(() => {
const state = useOrderStore.getState()
const order = state.orders.find((item) => item.id === orderId)
@@ -56,7 +59,7 @@ function scheduleOrderTimeout(orderId: string, status: OrderStatus) {
}
orderTimeouts.delete(orderId)
- }, 30000)
+ }, timeoutMs)
orderTimeouts.set(orderId, timer)
}