refactor(errors): migrate decisions to {code,msg}

This commit is contained in:
zetaloop
2026-02-28 07:21:51 +08:00
parent 4e2ee5be54
commit cc24a0cbc3
23 changed files with 157 additions and 165 deletions
+11 -11
View File
@@ -1,4 +1,4 @@
import { allow, deny } from "@/lib/policy/assert"
import { allow, deny } from "@/lib/decision"
import { useAuthStore } from "@/store/auth"
import { useChatStore } from "@/store/chat"
@@ -16,18 +16,18 @@ export function listChatMessages(sessionId: string) {
export function sendTextMessage(sessionId: string, content: string) {
const userId = useAuthStore.getState().user?.id
if (!userId) return deny("AUTH_REQUIRED", "请先登录")
if (!userId) return deny(401, "请先登录")
const chatState = useChatStore.getState()
const session = chatState.sessions.find((item) => item.id === sessionId)
if (!session) return deny("NOT_FOUND", "会话不存在")
if (session.readonly) return deny("INVALID_STATUS", "当前会话只读")
if (!session) return deny(404, "会话不存在")
if (session.readonly) return deny(400, "当前会话只读")
if (!session.participants.some((participant) => participant.id === userId)) {
return deny("NOT_PARTICIPANT", "仅会话参与方可发送消息")
return deny(403, "仅会话参与方可发送消息")
}
if (!content.trim()) {
return deny("VALIDATION_FAILED", "消息不能为空")
return deny(400, "消息不能为空")
}
chatState.sendTextMessage(sessionId, userId, content)
@@ -36,18 +36,18 @@ export function sendTextMessage(sessionId: string, content: string) {
export function sendImageMessage(sessionId: string, imageUrl: string) {
const userId = useAuthStore.getState().user?.id
if (!userId) return deny("AUTH_REQUIRED", "请先登录")
if (!userId) return deny(401, "请先登录")
const chatState = useChatStore.getState()
const session = chatState.sessions.find((item) => item.id === sessionId)
if (!session) return deny("NOT_FOUND", "会话不存在")
if (session.readonly) return deny("INVALID_STATUS", "当前会话只读")
if (!session) return deny(404, "会话不存在")
if (session.readonly) return deny(400, "当前会话只读")
if (!session.participants.some((participant) => participant.id === userId)) {
return deny("NOT_PARTICIPANT", "仅会话参与方可发送消息")
return deny(403, "仅会话参与方可发送消息")
}
if (!imageUrl.trim()) {
return deny("VALIDATION_FAILED", "图片地址无效")
return deny(400, "图片地址无效")
}
chatState.sendImageMessage(sessionId, userId, imageUrl)
+6 -6
View File
@@ -1,5 +1,5 @@
import { addNotification } from "@/lib/api/notifications"
import { allow, deny } from "@/lib/policy/assert"
import { allow, deny } from "@/lib/decision"
import { useAuthStore } from "@/store/auth"
import { useCommentStore } from "@/store/comments"
import { usePostStore } from "@/store/posts"
@@ -15,17 +15,17 @@ export function listCommentsByPost(postId: string) {
export function addComment(postId: string, content: string) {
const user = useAuthStore.getState().user
if (!user) {
return deny("AUTH_REQUIRED", "请先登录")
return deny(401, "请先登录")
}
const post = usePostStore.getState().posts.find((item) => item.id === postId)
if (!post) {
return deny("NOT_FOUND", "帖子不存在")
return deny(404, "帖子不存在")
}
const comment = useCommentStore.getState().addComment(postId, user, content)
if (!comment) {
return deny("VALIDATION_FAILED", "评论内容不能为空")
return deny(400, "评论内容不能为空")
}
usePostStore.getState().incrementCommentCount(postId)
@@ -43,12 +43,12 @@ export function addComment(postId: string, content: string) {
export function toggleCommentLike(commentId: string) {
const user = useAuthStore.getState().user
if (!user) {
return deny("AUTH_REQUIRED", "请先登录")
return deny(401, "请先登录")
}
const comment = useCommentStore.getState().comments.find((item) => item.id === commentId)
if (!comment) {
return deny("NOT_FOUND", "评论不存在")
return deny(404, "评论不存在")
}
useCommentStore.getState().toggleCommentLike(commentId)
+4 -4
View File
@@ -1,4 +1,4 @@
import { deny } from "@/lib/policy/assert"
import { deny } from "@/lib/decision"
import { useAuthStore } from "@/store/auth"
import { useDisputeStore } from "@/store/disputes"
@@ -13,7 +13,7 @@ export function getDisputeByOrderId(orderId: string) {
export function submitDispute(input: { orderId: string; reason: string; evidence: string[] }) {
const user = useAuthStore.getState().user
if (!user?.id || !user.nickname) {
return { decision: deny("AUTH_REQUIRED", "请先登录") }
return { decision: deny(401, "请先登录") }
}
return useDisputeStore.getState().submitDispute({
@@ -32,7 +32,7 @@ export function submitDisputeResponse(input: {
}) {
const userId = useAuthStore.getState().user?.id
if (!userId) {
return deny("AUTH_REQUIRED", "请先登录")
return deny(401, "请先登录")
}
return useDisputeStore
@@ -43,7 +43,7 @@ export function submitDisputeResponse(input: {
export function submitDisputeAppeal(input: { disputeId: string; reason: string }) {
const userId = useAuthStore.getState().user?.id
if (!userId) {
return deny("AUTH_REQUIRED", "请先登录")
return deny(401, "请先登录")
}
return useDisputeStore.getState().submitAppeal(input.disputeId, userId, input.reason)
+2 -2
View File
@@ -1,4 +1,4 @@
import { allow, deny } from "@/lib/policy/assert"
import { allow, deny } from "@/lib/decision"
import type { Notification } from "@/lib/types"
import { useAuthStore } from "@/store/auth"
import { useNotificationStore } from "@/store/notifications"
@@ -21,7 +21,7 @@ export function addNotification(input: {
link?: string
}) {
if (!isNotificationEnabled(input.type)) {
return deny("IDEMPOTENT_NOOP", "该类通知已关闭")
return deny(400, "该类通知已关闭")
}
useNotificationStore.getState().addNotification(input)
+5 -5
View File
@@ -1,7 +1,7 @@
import type { Actor } from "@/lib/actor"
import { allow, deny } from "@/lib/decision"
import { resolveOwnerShop } from "@/lib/domain/resolve-current-shop"
import type { Actor } from "@/lib/policy/actor"
import { allow, deny } from "@/lib/policy/assert"
import type { PolicyDecision } from "@/lib/policy/decision"
import type { ApiDecision } from "@/lib/errors"
import type { PlayerService } from "@/lib/types"
import { useAuthStore } from "@/store/auth"
import { useOrderStore } from "@/store/orders"
@@ -31,10 +31,10 @@ interface CreatePaidOrderInput {
note?: string
}
function resolveActorContext(): { actor?: Actor; decision: PolicyDecision } {
function resolveActorContext(): { actor?: Actor; decision: ApiDecision } {
const auth = useAuthStore.getState()
if (!auth.user?.id) {
return { decision: deny("AUTH_REQUIRED", "请先登录") }
return { decision: deny(401, "请先登录") }
}
const shopId =
+3 -3
View File
@@ -1,5 +1,5 @@
import { addNotification } from "@/lib/api/notifications"
import { allow, deny } from "@/lib/policy/assert"
import { allow, deny } from "@/lib/decision"
import { useAuthStore } from "@/store/auth"
import { usePostStore } from "@/store/posts"
@@ -18,12 +18,12 @@ export function listPostsByAuthor(userId: string) {
export function togglePostLike(postId: string) {
const user = useAuthStore.getState().user
if (!user) {
return deny("AUTH_REQUIRED", "请先登录")
return deny(401, "请先登录")
}
const post = usePostStore.getState().posts.find((item) => item.id === postId)
if (!post) {
return deny("NOT_FOUND", "帖子不存在")
return deny(404, "帖子不存在")
}
const shouldNotify = !post.liked
+2 -2
View File
@@ -1,4 +1,4 @@
import { deny } from "@/lib/policy/assert"
import { deny } from "@/lib/decision"
import { useAuthStore } from "@/store/auth"
import { useReviewStore } from "@/store/reviews"
@@ -17,7 +17,7 @@ export function listReviewsByTargetUser(userId: string) {
export function submitReview(input: { orderId: string; rating: number; content?: string }) {
const userId = useAuthStore.getState().user?.id
if (!userId) {
return deny("AUTH_REQUIRED", "请先登录")
return deny(401, "请先登录")
}
return useReviewStore.getState().submitReview({
+18
View File
@@ -0,0 +1,18 @@
import type { Actor } from "@/lib/actor"
import type { ApiDecision } from "@/lib/errors"
export function allow(): ApiDecision {
return { ok: true }
}
export function deny(code: number, msg: string): ApiDecision {
return { ok: false, error: { code, msg } }
}
export function requireAuth(actor: Actor | null | undefined): ApiDecision {
if (!actor?.userId) {
return deny(401, "请先登录")
}
return allow()
}
+12 -12
View File
@@ -1,6 +1,6 @@
import type { Actor } from "@/lib/policy/actor"
import { allow, deny, requireAuth } from "@/lib/policy/assert"
import type { PolicyDecision } from "@/lib/policy/decision"
import type { Actor } from "@/lib/actor"
import { allow, deny, requireAuth } from "@/lib/decision"
import type { ApiDecision } from "@/lib/errors"
import type { OrderStatus } from "@/lib/types"
export type OrderAction =
@@ -36,7 +36,7 @@ interface TransitionContext {
}
export interface OrderTransitionResult {
decision: PolicyDecision
decision: ApiDecision
nextStatus?: OrderStatus
sideEffects: OrderTransitionSideEffect[]
}
@@ -77,7 +77,7 @@ function isAutoAction(action: OrderAction): boolean {
return action.startsWith("AUTO_TIMEOUT_")
}
function checkRolePermission(action: OrderAction, actor?: Actor | null): PolicyDecision {
function checkRolePermission(action: OrderAction, actor?: Actor | null): ApiDecision {
if (isAutoAction(action) || (action === "RESOLVE_DISPUTE" && !actor?.userId)) {
return allow()
}
@@ -88,26 +88,26 @@ function checkRolePermission(action: OrderAction, actor?: Actor | null): PolicyD
}
if (!actor) {
return deny("AUTH_REQUIRED", "请先登录")
return deny(401, "请先登录")
}
if (action === "PAY" || action === "CANCEL_PRE_ACCEPT") {
return actor.role === "consumer" ? allow() : deny("ROLE_FORBIDDEN", "仅客户可执行该操作")
return actor.role === "consumer" ? allow() : deny(403, "仅客户可执行该操作")
}
if (action === "ACCEPT") {
return actor.role === "player" || actor.role === "owner"
? allow()
: deny("ROLE_FORBIDDEN", "仅打手或店主可执行该操作")
: deny(403, "仅打手或店主可执行该操作")
}
if (action === "RESOLVE_DISPUTE") {
return actor.role === "owner" ? allow() : deny("ROLE_FORBIDDEN", "仅店主可执行该操作")
return actor.role === "owner" ? allow() : deny(403, "仅店主可执行该操作")
}
return actor.role === "consumer" || actor.role === "player"
? allow()
: deny("ROLE_FORBIDDEN", "当前身份不可执行该操作")
: deny(403, "当前身份不可执行该操作")
}
function buildSideEffects(nextStatus: OrderStatus): OrderTransitionSideEffect[] {
@@ -144,14 +144,14 @@ export function evaluateOrderTransition(context: TransitionContext): OrderTransi
const nextStatus = orderTransitionTable[context.order.status][context.action]
if (!nextStatus) {
return {
decision: deny("INVALID_STATUS", "当前状态不可执行该操作"),
decision: deny(400, "当前状态不可执行该操作"),
sideEffects: [],
}
}
if (nextStatus === context.order.status) {
return {
decision: deny("IDEMPOTENT_NOOP", "状态未变化"),
decision: deny(400, "状态未变化"),
sideEffects: [],
}
}
+20
View File
@@ -0,0 +1,20 @@
export type ApiError = { code: number; msg: string }
export type ApiDecision = { ok: true } | { ok: false; error: ApiError }
export function isApiError(value: unknown): value is ApiError {
if (typeof value !== "object" || value === null) return false
const v = value as { code?: unknown; msg?: unknown }
return typeof v.code === "number" && typeof v.msg === "string"
}
export function toApiError(err: unknown): ApiError {
if (isApiError(err)) return err
if (err instanceof Error) return { code: 500, msg: err.message }
if (typeof err === "string") return { code: 500, msg: err }
return { code: 500, msg: "Unknown error" }
}
export type ApiResult<T> = { ok: true; data: T } | { ok: false; error: ApiError }
-18
View File
@@ -1,18 +0,0 @@
import type { Actor } from "@/lib/policy/actor"
import type { PolicyDecision, ReasonCode } from "@/lib/policy/decision"
export function allow(): PolicyDecision {
return { ok: true }
}
export function deny(reasonCode: ReasonCode, message?: string): PolicyDecision {
return { ok: false, reasonCode, message }
}
export function requireAuth(actor: Actor | null | undefined): PolicyDecision {
if (!actor?.userId) {
return deny("AUTH_REQUIRED", "请先登录")
}
return allow()
}
-18
View File
@@ -1,18 +0,0 @@
export type ReasonCode =
| "AUTH_REQUIRED"
| "NOT_FOUND"
| "NOT_PARTICIPANT"
| "ROLE_FORBIDDEN"
| "INVALID_STATUS"
| "ALREADY_DONE"
| "DISPUTE_LOCKED"
| "PAYMENT_FAILED"
| "IDEMPOTENT_NOOP"
| "VALIDATION_FAILED"
| "DUPLICATE_REQUEST"
export interface PolicyDecision {
ok: boolean
reasonCode?: ReasonCode
message?: string
}