refactor(order): add transition evaluator and timer constants

This commit is contained in:
zetaloop
2026-02-23 11:03:31 +08:00
parent 03fa447864
commit 6517018a9c
6 changed files with 188 additions and 8 deletions
+6
View File
@@ -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
+164
View File
@@ -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<Record<OrderAction, OrderStatus>>
> = {
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),
}
}