refactor(errors): migrate decisions to {code,msg}
This commit is contained in:
@@ -150,7 +150,7 @@ export default function ChatDetailPage({ params }: { params: Promise<{ id: strin
|
|||||||
const file = event.target.files?.[0]
|
const file = event.target.files?.[0]
|
||||||
if (!file) return
|
if (!file) return
|
||||||
const result = sendImageMessage(session.id, URL.createObjectURL(file))
|
const result = sendImageMessage(session.id, URL.createObjectURL(file))
|
||||||
if (result && !result.ok) notifyInfo(result.message ?? "发送失败")
|
if (!result.ok) notifyInfo(result.error.msg)
|
||||||
event.target.value = ""
|
event.target.value = ""
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -162,8 +162,8 @@ export default function ChatDetailPage({ params }: { params: Promise<{ id: strin
|
|||||||
if (!text) return
|
if (!text) return
|
||||||
|
|
||||||
const result = sendTextMessage(session.id, text)
|
const result = sendTextMessage(session.id, text)
|
||||||
if (result && !result.ok) {
|
if (!result.ok) {
|
||||||
notifyInfo(result.message ?? "发送失败")
|
notifyInfo(result.error.msg)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
setInput("")
|
setInput("")
|
||||||
|
|||||||
@@ -112,7 +112,7 @@ export default function DisputePage({ params }: { params: Promise<{ id: string }
|
|||||||
evidence: files,
|
evidence: files,
|
||||||
})
|
})
|
||||||
if (!result.decision.ok) {
|
if (!result.decision.ok) {
|
||||||
notifyInfo(result.decision.message ?? "提交争议失败")
|
notifyInfo(result.decision.error.msg)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
router.replace(`/dispute/${id}?submitted=1`)
|
router.replace(`/dispute/${id}?submitted=1`)
|
||||||
@@ -288,7 +288,7 @@ export default function DisputePage({ params }: { params: Promise<{ id: string }
|
|||||||
evidence: responseFiles,
|
evidence: responseFiles,
|
||||||
})
|
})
|
||||||
if (!decision.ok) {
|
if (!decision.ok) {
|
||||||
notifyInfo(decision.message ?? "提交回应失败")
|
notifyInfo(decision.error.msg)
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
disabled={!responseReason.trim()}
|
disabled={!responseReason.trim()}
|
||||||
@@ -336,7 +336,7 @@ export default function DisputePage({ params }: { params: Promise<{ id: string }
|
|||||||
reason: appealReason,
|
reason: appealReason,
|
||||||
})
|
})
|
||||||
if (!decision.ok) {
|
if (!decision.ok) {
|
||||||
notifyInfo(decision.message ?? "提交申诉失败")
|
notifyInfo(decision.error.msg)
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
disabled={!appealReason.trim()}
|
disabled={!appealReason.trim()}
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ 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 type { Actor } from "@/lib/policy/actor"
|
|
||||||
import { notifySuccess } from "@/lib/toast"
|
import { notifySuccess } from "@/lib/toast"
|
||||||
import { useRequireAuth } from "@/lib/use-require-auth"
|
import { useRequireAuth } from "@/lib/use-require-auth"
|
||||||
import { useAuthStore } from "@/store/auth"
|
import { useAuthStore } from "@/store/auth"
|
||||||
|
|||||||
@@ -154,7 +154,7 @@ export default function ReviewPage({ params }: { params: Promise<{ id: string }>
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
notifyInfo(decision.message ?? "评价提交失败")
|
notifyInfo(decision.error.msg)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
提交评价
|
提交评价
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
payOrder,
|
payOrder,
|
||||||
requestClose,
|
requestClose,
|
||||||
} from "@/lib/api/orders"
|
} from "@/lib/api/orders"
|
||||||
|
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 { OrderStatus } from "@/lib/types"
|
||||||
import { useAuthStore } from "@/store/auth"
|
import { useAuthStore } from "@/store/auth"
|
||||||
@@ -59,17 +60,14 @@ 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(
|
const handleDecision = useCallback((okMessage: string, result: { decision: ApiDecision }) => {
|
||||||
(okMessage: string, result: { decision: { ok: boolean; message?: string } }) => {
|
if (result.decision.ok) {
|
||||||
if (result.decision.ok) {
|
showFeedback(okMessage)
|
||||||
showFeedback(okMessage)
|
return
|
||||||
return
|
}
|
||||||
}
|
|
||||||
|
|
||||||
notifyInfo(result.decision.message ?? "当前操作不允许")
|
notifyInfo(result.decision.error.msg)
|
||||||
},
|
}, [])
|
||||||
[],
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex gap-2 flex-wrap">
|
<div className="flex gap-2 flex-wrap">
|
||||||
|
|||||||
+11
-11
@@ -1,4 +1,4 @@
|
|||||||
import { allow, deny } from "@/lib/policy/assert"
|
import { allow, deny } from "@/lib/decision"
|
||||||
import { useAuthStore } from "@/store/auth"
|
import { useAuthStore } from "@/store/auth"
|
||||||
import { useChatStore } from "@/store/chat"
|
import { useChatStore } from "@/store/chat"
|
||||||
|
|
||||||
@@ -16,18 +16,18 @@ export function listChatMessages(sessionId: string) {
|
|||||||
|
|
||||||
export function sendTextMessage(sessionId: string, content: string) {
|
export function sendTextMessage(sessionId: string, content: string) {
|
||||||
const userId = useAuthStore.getState().user?.id
|
const userId = useAuthStore.getState().user?.id
|
||||||
if (!userId) return deny("AUTH_REQUIRED", "请先登录")
|
if (!userId) return deny(401, "请先登录")
|
||||||
|
|
||||||
const chatState = useChatStore.getState()
|
const chatState = useChatStore.getState()
|
||||||
const session = chatState.sessions.find((item) => item.id === sessionId)
|
const session = chatState.sessions.find((item) => item.id === sessionId)
|
||||||
if (!session) return deny("NOT_FOUND", "会话不存在")
|
if (!session) return deny(404, "会话不存在")
|
||||||
if (session.readonly) return deny("INVALID_STATUS", "当前会话只读")
|
if (session.readonly) return deny(400, "当前会话只读")
|
||||||
if (!session.participants.some((participant) => participant.id === userId)) {
|
if (!session.participants.some((participant) => participant.id === userId)) {
|
||||||
return deny("NOT_PARTICIPANT", "仅会话参与方可发送消息")
|
return deny(403, "仅会话参与方可发送消息")
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!content.trim()) {
|
if (!content.trim()) {
|
||||||
return deny("VALIDATION_FAILED", "消息不能为空")
|
return deny(400, "消息不能为空")
|
||||||
}
|
}
|
||||||
|
|
||||||
chatState.sendTextMessage(sessionId, userId, content)
|
chatState.sendTextMessage(sessionId, userId, content)
|
||||||
@@ -36,18 +36,18 @@ export function sendTextMessage(sessionId: string, content: string) {
|
|||||||
|
|
||||||
export function sendImageMessage(sessionId: string, imageUrl: string) {
|
export function sendImageMessage(sessionId: string, imageUrl: string) {
|
||||||
const userId = useAuthStore.getState().user?.id
|
const userId = useAuthStore.getState().user?.id
|
||||||
if (!userId) return deny("AUTH_REQUIRED", "请先登录")
|
if (!userId) return deny(401, "请先登录")
|
||||||
|
|
||||||
const chatState = useChatStore.getState()
|
const chatState = useChatStore.getState()
|
||||||
const session = chatState.sessions.find((item) => item.id === sessionId)
|
const session = chatState.sessions.find((item) => item.id === sessionId)
|
||||||
if (!session) return deny("NOT_FOUND", "会话不存在")
|
if (!session) return deny(404, "会话不存在")
|
||||||
if (session.readonly) return deny("INVALID_STATUS", "当前会话只读")
|
if (session.readonly) return deny(400, "当前会话只读")
|
||||||
if (!session.participants.some((participant) => participant.id === userId)) {
|
if (!session.participants.some((participant) => participant.id === userId)) {
|
||||||
return deny("NOT_PARTICIPANT", "仅会话参与方可发送消息")
|
return deny(403, "仅会话参与方可发送消息")
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!imageUrl.trim()) {
|
if (!imageUrl.trim()) {
|
||||||
return deny("VALIDATION_FAILED", "图片地址无效")
|
return deny(400, "图片地址无效")
|
||||||
}
|
}
|
||||||
|
|
||||||
chatState.sendImageMessage(sessionId, userId, imageUrl)
|
chatState.sendImageMessage(sessionId, userId, imageUrl)
|
||||||
|
|||||||
+6
-6
@@ -1,5 +1,5 @@
|
|||||||
import { addNotification } from "@/lib/api/notifications"
|
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 { useAuthStore } from "@/store/auth"
|
||||||
import { useCommentStore } from "@/store/comments"
|
import { useCommentStore } from "@/store/comments"
|
||||||
import { usePostStore } from "@/store/posts"
|
import { usePostStore } from "@/store/posts"
|
||||||
@@ -15,17 +15,17 @@ export function listCommentsByPost(postId: string) {
|
|||||||
export function addComment(postId: string, content: string) {
|
export function addComment(postId: string, content: string) {
|
||||||
const user = useAuthStore.getState().user
|
const user = useAuthStore.getState().user
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return deny("AUTH_REQUIRED", "请先登录")
|
return deny(401, "请先登录")
|
||||||
}
|
}
|
||||||
|
|
||||||
const post = usePostStore.getState().posts.find((item) => item.id === postId)
|
const post = usePostStore.getState().posts.find((item) => item.id === postId)
|
||||||
if (!post) {
|
if (!post) {
|
||||||
return deny("NOT_FOUND", "帖子不存在")
|
return deny(404, "帖子不存在")
|
||||||
}
|
}
|
||||||
|
|
||||||
const comment = useCommentStore.getState().addComment(postId, user, content)
|
const comment = useCommentStore.getState().addComment(postId, user, content)
|
||||||
if (!comment) {
|
if (!comment) {
|
||||||
return deny("VALIDATION_FAILED", "评论内容不能为空")
|
return deny(400, "评论内容不能为空")
|
||||||
}
|
}
|
||||||
|
|
||||||
usePostStore.getState().incrementCommentCount(postId)
|
usePostStore.getState().incrementCommentCount(postId)
|
||||||
@@ -43,12 +43,12 @@ export function addComment(postId: string, content: string) {
|
|||||||
export function toggleCommentLike(commentId: string) {
|
export function toggleCommentLike(commentId: string) {
|
||||||
const user = useAuthStore.getState().user
|
const user = useAuthStore.getState().user
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return deny("AUTH_REQUIRED", "请先登录")
|
return deny(401, "请先登录")
|
||||||
}
|
}
|
||||||
|
|
||||||
const comment = useCommentStore.getState().comments.find((item) => item.id === commentId)
|
const comment = useCommentStore.getState().comments.find((item) => item.id === commentId)
|
||||||
if (!comment) {
|
if (!comment) {
|
||||||
return deny("NOT_FOUND", "评论不存在")
|
return deny(404, "评论不存在")
|
||||||
}
|
}
|
||||||
|
|
||||||
useCommentStore.getState().toggleCommentLike(commentId)
|
useCommentStore.getState().toggleCommentLike(commentId)
|
||||||
|
|||||||
+4
-4
@@ -1,4 +1,4 @@
|
|||||||
import { deny } from "@/lib/policy/assert"
|
import { deny } from "@/lib/decision"
|
||||||
import { useAuthStore } from "@/store/auth"
|
import { useAuthStore } from "@/store/auth"
|
||||||
import { useDisputeStore } from "@/store/disputes"
|
import { useDisputeStore } from "@/store/disputes"
|
||||||
|
|
||||||
@@ -13,7 +13,7 @@ export function getDisputeByOrderId(orderId: string) {
|
|||||||
export function submitDispute(input: { orderId: string; reason: string; evidence: string[] }) {
|
export function submitDispute(input: { orderId: string; reason: string; evidence: string[] }) {
|
||||||
const user = useAuthStore.getState().user
|
const user = useAuthStore.getState().user
|
||||||
if (!user?.id || !user.nickname) {
|
if (!user?.id || !user.nickname) {
|
||||||
return { decision: deny("AUTH_REQUIRED", "请先登录") }
|
return { decision: deny(401, "请先登录") }
|
||||||
}
|
}
|
||||||
|
|
||||||
return useDisputeStore.getState().submitDispute({
|
return useDisputeStore.getState().submitDispute({
|
||||||
@@ -32,7 +32,7 @@ export function submitDisputeResponse(input: {
|
|||||||
}) {
|
}) {
|
||||||
const userId = useAuthStore.getState().user?.id
|
const userId = useAuthStore.getState().user?.id
|
||||||
if (!userId) {
|
if (!userId) {
|
||||||
return deny("AUTH_REQUIRED", "请先登录")
|
return deny(401, "请先登录")
|
||||||
}
|
}
|
||||||
|
|
||||||
return useDisputeStore
|
return useDisputeStore
|
||||||
@@ -43,7 +43,7 @@ export function submitDisputeResponse(input: {
|
|||||||
export function submitDisputeAppeal(input: { disputeId: string; reason: string }) {
|
export function submitDisputeAppeal(input: { disputeId: string; reason: string }) {
|
||||||
const userId = useAuthStore.getState().user?.id
|
const userId = useAuthStore.getState().user?.id
|
||||||
if (!userId) {
|
if (!userId) {
|
||||||
return deny("AUTH_REQUIRED", "请先登录")
|
return deny(401, "请先登录")
|
||||||
}
|
}
|
||||||
|
|
||||||
return useDisputeStore.getState().submitAppeal(input.disputeId, userId, input.reason)
|
return useDisputeStore.getState().submitAppeal(input.disputeId, userId, input.reason)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { allow, deny } from "@/lib/policy/assert"
|
import { allow, deny } from "@/lib/decision"
|
||||||
import type { Notification } from "@/lib/types"
|
import type { Notification } from "@/lib/types"
|
||||||
import { useAuthStore } from "@/store/auth"
|
import { useAuthStore } from "@/store/auth"
|
||||||
import { useNotificationStore } from "@/store/notifications"
|
import { useNotificationStore } from "@/store/notifications"
|
||||||
@@ -21,7 +21,7 @@ export function addNotification(input: {
|
|||||||
link?: string
|
link?: string
|
||||||
}) {
|
}) {
|
||||||
if (!isNotificationEnabled(input.type)) {
|
if (!isNotificationEnabled(input.type)) {
|
||||||
return deny("IDEMPOTENT_NOOP", "该类通知已关闭")
|
return deny(400, "该类通知已关闭")
|
||||||
}
|
}
|
||||||
|
|
||||||
useNotificationStore.getState().addNotification(input)
|
useNotificationStore.getState().addNotification(input)
|
||||||
|
|||||||
+5
-5
@@ -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 { resolveOwnerShop } from "@/lib/domain/resolve-current-shop"
|
||||||
import type { Actor } from "@/lib/policy/actor"
|
import type { ApiDecision } from "@/lib/errors"
|
||||||
import { allow, deny } from "@/lib/policy/assert"
|
|
||||||
import type { PolicyDecision } from "@/lib/policy/decision"
|
|
||||||
import type { PlayerService } from "@/lib/types"
|
import type { PlayerService } from "@/lib/types"
|
||||||
import { useAuthStore } from "@/store/auth"
|
import { useAuthStore } from "@/store/auth"
|
||||||
import { useOrderStore } from "@/store/orders"
|
import { useOrderStore } from "@/store/orders"
|
||||||
@@ -31,10 +31,10 @@ interface CreatePaidOrderInput {
|
|||||||
note?: string
|
note?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveActorContext(): { actor?: Actor; decision: PolicyDecision } {
|
function resolveActorContext(): { actor?: Actor; decision: ApiDecision } {
|
||||||
const auth = useAuthStore.getState()
|
const auth = useAuthStore.getState()
|
||||||
if (!auth.user?.id) {
|
if (!auth.user?.id) {
|
||||||
return { decision: deny("AUTH_REQUIRED", "请先登录") }
|
return { decision: deny(401, "请先登录") }
|
||||||
}
|
}
|
||||||
|
|
||||||
const shopId =
|
const shopId =
|
||||||
|
|||||||
+3
-3
@@ -1,5 +1,5 @@
|
|||||||
import { addNotification } from "@/lib/api/notifications"
|
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 { useAuthStore } from "@/store/auth"
|
||||||
import { usePostStore } from "@/store/posts"
|
import { usePostStore } from "@/store/posts"
|
||||||
|
|
||||||
@@ -18,12 +18,12 @@ export function listPostsByAuthor(userId: string) {
|
|||||||
export function togglePostLike(postId: string) {
|
export function togglePostLike(postId: string) {
|
||||||
const user = useAuthStore.getState().user
|
const user = useAuthStore.getState().user
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return deny("AUTH_REQUIRED", "请先登录")
|
return deny(401, "请先登录")
|
||||||
}
|
}
|
||||||
|
|
||||||
const post = usePostStore.getState().posts.find((item) => item.id === postId)
|
const post = usePostStore.getState().posts.find((item) => item.id === postId)
|
||||||
if (!post) {
|
if (!post) {
|
||||||
return deny("NOT_FOUND", "帖子不存在")
|
return deny(404, "帖子不存在")
|
||||||
}
|
}
|
||||||
|
|
||||||
const shouldNotify = !post.liked
|
const shouldNotify = !post.liked
|
||||||
|
|||||||
+2
-2
@@ -1,4 +1,4 @@
|
|||||||
import { deny } from "@/lib/policy/assert"
|
import { deny } from "@/lib/decision"
|
||||||
import { useAuthStore } from "@/store/auth"
|
import { useAuthStore } from "@/store/auth"
|
||||||
import { useReviewStore } from "@/store/reviews"
|
import { useReviewStore } from "@/store/reviews"
|
||||||
|
|
||||||
@@ -17,7 +17,7 @@ export function listReviewsByTargetUser(userId: string) {
|
|||||||
export function submitReview(input: { orderId: string; rating: number; content?: string }) {
|
export function submitReview(input: { orderId: string; rating: number; content?: string }) {
|
||||||
const userId = useAuthStore.getState().user?.id
|
const userId = useAuthStore.getState().user?.id
|
||||||
if (!userId) {
|
if (!userId) {
|
||||||
return deny("AUTH_REQUIRED", "请先登录")
|
return deny(401, "请先登录")
|
||||||
}
|
}
|
||||||
|
|
||||||
return useReviewStore.getState().submitReview({
|
return useReviewStore.getState().submitReview({
|
||||||
|
|||||||
@@ -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
@@ -1,6 +1,6 @@
|
|||||||
import type { Actor } from "@/lib/policy/actor"
|
import type { Actor } from "@/lib/actor"
|
||||||
import { allow, deny, requireAuth } from "@/lib/policy/assert"
|
import { allow, deny, requireAuth } from "@/lib/decision"
|
||||||
import type { PolicyDecision } from "@/lib/policy/decision"
|
import type { ApiDecision } from "@/lib/errors"
|
||||||
import type { OrderStatus } from "@/lib/types"
|
import type { OrderStatus } from "@/lib/types"
|
||||||
|
|
||||||
export type OrderAction =
|
export type OrderAction =
|
||||||
@@ -36,7 +36,7 @@ interface TransitionContext {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface OrderTransitionResult {
|
export interface OrderTransitionResult {
|
||||||
decision: PolicyDecision
|
decision: ApiDecision
|
||||||
nextStatus?: OrderStatus
|
nextStatus?: OrderStatus
|
||||||
sideEffects: OrderTransitionSideEffect[]
|
sideEffects: OrderTransitionSideEffect[]
|
||||||
}
|
}
|
||||||
@@ -77,7 +77,7 @@ function isAutoAction(action: OrderAction): boolean {
|
|||||||
return action.startsWith("AUTO_TIMEOUT_")
|
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)) {
|
if (isAutoAction(action) || (action === "RESOLVE_DISPUTE" && !actor?.userId)) {
|
||||||
return allow()
|
return allow()
|
||||||
}
|
}
|
||||||
@@ -88,26 +88,26 @@ function checkRolePermission(action: OrderAction, actor?: Actor | null): PolicyD
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!actor) {
|
if (!actor) {
|
||||||
return deny("AUTH_REQUIRED", "请先登录")
|
return deny(401, "请先登录")
|
||||||
}
|
}
|
||||||
|
|
||||||
if (action === "PAY" || action === "CANCEL_PRE_ACCEPT") {
|
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") {
|
if (action === "ACCEPT") {
|
||||||
return actor.role === "player" || actor.role === "owner"
|
return actor.role === "player" || actor.role === "owner"
|
||||||
? allow()
|
? allow()
|
||||||
: deny("ROLE_FORBIDDEN", "仅打手或店主可执行该操作")
|
: deny(403, "仅打手或店主可执行该操作")
|
||||||
}
|
}
|
||||||
|
|
||||||
if (action === "RESOLVE_DISPUTE") {
|
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"
|
return actor.role === "consumer" || actor.role === "player"
|
||||||
? allow()
|
? allow()
|
||||||
: deny("ROLE_FORBIDDEN", "当前身份不可执行该操作")
|
: deny(403, "当前身份不可执行该操作")
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildSideEffects(nextStatus: OrderStatus): OrderTransitionSideEffect[] {
|
function buildSideEffects(nextStatus: OrderStatus): OrderTransitionSideEffect[] {
|
||||||
@@ -144,14 +144,14 @@ export function evaluateOrderTransition(context: TransitionContext): OrderTransi
|
|||||||
const nextStatus = orderTransitionTable[context.order.status][context.action]
|
const nextStatus = orderTransitionTable[context.order.status][context.action]
|
||||||
if (!nextStatus) {
|
if (!nextStatus) {
|
||||||
return {
|
return {
|
||||||
decision: deny("INVALID_STATUS", "当前状态不可执行该操作"),
|
decision: deny(400, "当前状态不可执行该操作"),
|
||||||
sideEffects: [],
|
sideEffects: [],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (nextStatus === context.order.status) {
|
if (nextStatus === context.order.status) {
|
||||||
return {
|
return {
|
||||||
decision: deny("IDEMPOTENT_NOOP", "状态未变化"),
|
decision: deny(400, "状态未变化"),
|
||||||
sideEffects: [],
|
sideEffects: [],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 }
|
||||||
@@ -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()
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
+21
-21
@@ -1,9 +1,9 @@
|
|||||||
|
import type { Actor } from "@/lib/actor"
|
||||||
import { DISPUTE_TO_RESOLVED_MS, DISPUTE_TO_REVIEWING_MS } from "@/lib/config/demo-timers"
|
import { DISPUTE_TO_RESOLVED_MS, DISPUTE_TO_REVIEWING_MS } from "@/lib/config/demo-timers"
|
||||||
|
import { allow, deny } from "@/lib/decision"
|
||||||
|
import type { ApiDecision } from "@/lib/errors"
|
||||||
import { generateId } from "@/lib/id"
|
import { generateId } from "@/lib/id"
|
||||||
import { mockDisputes } from "@/lib/mock"
|
import { mockDisputes } from "@/lib/mock"
|
||||||
import type { Actor } from "@/lib/policy/actor"
|
|
||||||
import { allow, deny } from "@/lib/policy/assert"
|
|
||||||
import type { PolicyDecision } from "@/lib/policy/decision"
|
|
||||||
import type { Dispute } from "@/lib/types"
|
import type { Dispute } from "@/lib/types"
|
||||||
import { useAuthStore } from "@/store/auth"
|
import { useAuthStore } from "@/store/auth"
|
||||||
import { useNotificationStore } from "@/store/notifications"
|
import { useNotificationStore } from "@/store/notifications"
|
||||||
@@ -36,7 +36,7 @@ interface SubmitDisputeInput {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface DisputeMutationResult {
|
interface DisputeMutationResult {
|
||||||
decision: PolicyDecision
|
decision: ApiDecision
|
||||||
dispute?: DisputeRecord
|
dispute?: DisputeRecord
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,8 +49,8 @@ interface DisputeState {
|
|||||||
actorId: string,
|
actorId: string,
|
||||||
reason: string,
|
reason: string,
|
||||||
evidence: string[],
|
evidence: string[],
|
||||||
) => PolicyDecision
|
) => ApiDecision
|
||||||
submitAppeal: (disputeId: string, actorId: string, reason: string) => PolicyDecision
|
submitAppeal: (disputeId: string, actorId: string, reason: string) => ApiDecision
|
||||||
}
|
}
|
||||||
|
|
||||||
const progressTimers = new Map<string, ReturnType<typeof setTimeout>[]>()
|
const progressTimers = new Map<string, ReturnType<typeof setTimeout>[]>()
|
||||||
@@ -205,20 +205,20 @@ export const useDisputeStore = create<DisputeState>((set, get) => {
|
|||||||
submitDispute: (input) => {
|
submitDispute: (input) => {
|
||||||
const order = useOrderStore.getState().orders.find((item) => item.id === input.orderId)
|
const order = useOrderStore.getState().orders.find((item) => item.id === input.orderId)
|
||||||
if (!order) {
|
if (!order) {
|
||||||
return { decision: deny("NOT_FOUND", "订单不存在") }
|
return { decision: deny(404, "订单不存在") }
|
||||||
}
|
}
|
||||||
|
|
||||||
if (order.status !== "in_progress" && order.status !== "pending_close") {
|
if (order.status !== "in_progress" && order.status !== "pending_close") {
|
||||||
return { decision: deny("INVALID_STATUS", "当前阶段不可发起争议") }
|
return { decision: deny(400, "当前阶段不可发起争议") }
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!input.reason.trim()) {
|
if (!input.reason.trim()) {
|
||||||
return { decision: deny("VALIDATION_FAILED", "请填写争议原因") }
|
return { decision: deny(400, "请填写争议原因") }
|
||||||
}
|
}
|
||||||
|
|
||||||
const actor = resolveParticipantActor(input.orderId, input.initiatorId)
|
const actor = resolveParticipantActor(input.orderId, input.initiatorId)
|
||||||
if (!actor) {
|
if (!actor) {
|
||||||
return { decision: deny("NOT_PARTICIPANT", "仅订单参与方可发起争议") }
|
return { decision: deny(403, "仅订单参与方可发起争议") }
|
||||||
}
|
}
|
||||||
|
|
||||||
const markResult = useOrderStore.getState().markDisputed(input.orderId, actor)
|
const markResult = useOrderStore.getState().markDisputed(input.orderId, actor)
|
||||||
@@ -254,28 +254,28 @@ export const useDisputeStore = create<DisputeState>((set, get) => {
|
|||||||
submitResponse: (disputeId, actorId, reason, evidence) => {
|
submitResponse: (disputeId, actorId, reason, evidence) => {
|
||||||
const dispute = get().disputes.find((item) => item.id === disputeId)
|
const dispute = get().disputes.find((item) => item.id === disputeId)
|
||||||
if (!dispute) {
|
if (!dispute) {
|
||||||
return deny("NOT_FOUND", "争议不存在")
|
return deny(404, "争议不存在")
|
||||||
}
|
}
|
||||||
|
|
||||||
const actor = resolveParticipantActor(dispute.orderId, actorId)
|
const actor = resolveParticipantActor(dispute.orderId, actorId)
|
||||||
if (!actor) {
|
if (!actor) {
|
||||||
return deny("NOT_PARTICIPANT", "仅订单参与方可提交回应")
|
return deny(403, "仅订单参与方可提交回应")
|
||||||
}
|
}
|
||||||
|
|
||||||
if (actorId === dispute.initiatorId) {
|
if (actorId === dispute.initiatorId) {
|
||||||
return deny("ROLE_FORBIDDEN", "争议发起方不可提交回应")
|
return deny(403, "争议发起方不可提交回应")
|
||||||
}
|
}
|
||||||
|
|
||||||
if (dispute.respondentReason) {
|
if (dispute.respondentReason) {
|
||||||
return deny("ALREADY_DONE", "回应已提交")
|
return deny(400, "回应已提交")
|
||||||
}
|
}
|
||||||
|
|
||||||
if (dispute.status !== "open" && dispute.status !== "reviewing") {
|
if (dispute.status !== "open" && dispute.status !== "reviewing") {
|
||||||
return deny("INVALID_STATUS", "当前状态不可提交回应")
|
return deny(400, "当前状态不可提交回应")
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!reason.trim()) {
|
if (!reason.trim()) {
|
||||||
return deny("VALIDATION_FAILED", "请填写回应理由")
|
return deny(400, "请填写回应理由")
|
||||||
}
|
}
|
||||||
|
|
||||||
set((state) => ({
|
set((state) => ({
|
||||||
@@ -306,24 +306,24 @@ export const useDisputeStore = create<DisputeState>((set, get) => {
|
|||||||
submitAppeal: (disputeId, actorId, reason) => {
|
submitAppeal: (disputeId, actorId, reason) => {
|
||||||
const dispute = get().disputes.find((item) => item.id === disputeId)
|
const dispute = get().disputes.find((item) => item.id === disputeId)
|
||||||
if (!dispute) {
|
if (!dispute) {
|
||||||
return deny("NOT_FOUND", "争议不存在")
|
return deny(404, "争议不存在")
|
||||||
}
|
}
|
||||||
|
|
||||||
const actor = resolveParticipantActor(dispute.orderId, actorId)
|
const actor = resolveParticipantActor(dispute.orderId, actorId)
|
||||||
if (!actor) {
|
if (!actor) {
|
||||||
return deny("NOT_PARTICIPANT", "仅订单参与方可提交申诉")
|
return deny(403, "仅订单参与方可提交申诉")
|
||||||
}
|
}
|
||||||
|
|
||||||
if (dispute.status !== "resolved") {
|
if (dispute.status !== "resolved") {
|
||||||
return deny("INVALID_STATUS", "当前状态不可申诉")
|
return deny(400, "当前状态不可申诉")
|
||||||
}
|
}
|
||||||
|
|
||||||
if (dispute.appealedAt) {
|
if (dispute.appealedAt) {
|
||||||
return deny("ALREADY_DONE", "该争议已申诉过")
|
return deny(400, "该争议已申诉过")
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!reason.trim()) {
|
if (!reason.trim()) {
|
||||||
return deny("VALIDATION_FAILED", "请填写申诉理由")
|
return deny(400, "请填写申诉理由")
|
||||||
}
|
}
|
||||||
|
|
||||||
set((state) => ({
|
set((state) => ({
|
||||||
|
|||||||
+18
-24
@@ -1,14 +1,14 @@
|
|||||||
|
import type { Actor } from "@/lib/actor"
|
||||||
import {
|
import {
|
||||||
ORDER_ACCEPT_TIMEOUT_MS,
|
ORDER_ACCEPT_TIMEOUT_MS,
|
||||||
ORDER_CLOSE_TIMEOUT_MS,
|
ORDER_CLOSE_TIMEOUT_MS,
|
||||||
ORDER_REVIEW_TIMEOUT_MS,
|
ORDER_REVIEW_TIMEOUT_MS,
|
||||||
} from "@/lib/config/demo-timers"
|
} from "@/lib/config/demo-timers"
|
||||||
|
import { allow, deny } from "@/lib/decision"
|
||||||
import { evaluateOrderTransition, type OrderAction } from "@/lib/domain/order-machine"
|
import { evaluateOrderTransition, type OrderAction } from "@/lib/domain/order-machine"
|
||||||
|
import type { ApiDecision } from "@/lib/errors"
|
||||||
import { generateId } from "@/lib/id"
|
import { generateId } from "@/lib/id"
|
||||||
import { mockOrders } from "@/lib/mock"
|
import { mockOrders } from "@/lib/mock"
|
||||||
import type { Actor } from "@/lib/policy/actor"
|
|
||||||
import { allow, deny } from "@/lib/policy/assert"
|
|
||||||
import type { PolicyDecision } from "@/lib/policy/decision"
|
|
||||||
import type { Order, OrderStatus, PlayerService } from "@/lib/types"
|
import type { Order, OrderStatus, PlayerService } from "@/lib/types"
|
||||||
import { useAuthStore } from "@/store/auth"
|
import { useAuthStore } from "@/store/auth"
|
||||||
import { useChatStore } from "@/store/chat"
|
import { useChatStore } from "@/store/chat"
|
||||||
@@ -35,7 +35,7 @@ interface CreatePaidOrderInput extends CreateOrderInput {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface OrderMutationResult {
|
interface OrderMutationResult {
|
||||||
decision: PolicyDecision
|
decision: ApiDecision
|
||||||
order?: Order
|
order?: Order
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,41 +73,35 @@ function isOrderOwnerActor(order: Order, actor: Actor) {
|
|||||||
return actor.role === "owner" && Boolean(order.shopId) && actor.shopId === order.shopId
|
return actor.role === "owner" && Boolean(order.shopId) && actor.shopId === order.shopId
|
||||||
}
|
}
|
||||||
|
|
||||||
function validateActorForAction(order: Order, action: OrderAction, actor?: Actor): PolicyDecision {
|
function validateActorForAction(order: Order, action: OrderAction, actor?: Actor): ApiDecision {
|
||||||
if (action.startsWith("AUTO_TIMEOUT_") || (action === "RESOLVE_DISPUTE" && !actor?.userId)) {
|
if (action.startsWith("AUTO_TIMEOUT_") || (action === "RESOLVE_DISPUTE" && !actor?.userId)) {
|
||||||
return allow()
|
return allow()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!actor?.userId) {
|
if (!actor?.userId) {
|
||||||
return deny("AUTH_REQUIRED", "请先登录")
|
return deny(401, "请先登录")
|
||||||
}
|
}
|
||||||
|
|
||||||
if (order.status === "disputed" && action !== "RESOLVE_DISPUTE") {
|
if (order.status === "disputed" && action !== "RESOLVE_DISPUTE") {
|
||||||
return deny("DISPUTE_LOCKED", "争议处理中,暂不可执行此操作")
|
return deny(400, "争议处理中,暂不可执行此操作")
|
||||||
}
|
}
|
||||||
|
|
||||||
if (action === "PAY" || action === "CANCEL_PRE_ACCEPT" || action === "CONFIRM_CLOSE") {
|
if (action === "PAY" || action === "CANCEL_PRE_ACCEPT" || action === "CONFIRM_CLOSE") {
|
||||||
return order.consumerId === actor.userId
|
return order.consumerId === actor.userId ? allow() : deny(403, "仅下单客户可执行该操作")
|
||||||
? allow()
|
|
||||||
: deny("NOT_PARTICIPANT", "仅下单客户可执行该操作")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (action === "ACCEPT") {
|
if (action === "ACCEPT") {
|
||||||
return order.playerId === actor.userId || isOrderOwnerActor(order, actor)
|
return order.playerId === actor.userId || isOrderOwnerActor(order, actor)
|
||||||
? allow()
|
? allow()
|
||||||
: deny("NOT_PARTICIPANT", "仅该订单打手或所属店主可执行接单")
|
: deny(403, "仅该订单打手或所属店主可执行接单")
|
||||||
}
|
}
|
||||||
|
|
||||||
if (action === "REQUEST_CLOSE" || action === "OPEN_DISPUTE" || action === "SUBMIT_REVIEW") {
|
if (action === "REQUEST_CLOSE" || action === "OPEN_DISPUTE" || action === "SUBMIT_REVIEW") {
|
||||||
return isParticipant(order, actor.userId)
|
return isParticipant(order, actor.userId) ? allow() : deny(403, "仅订单参与方可执行该操作")
|
||||||
? allow()
|
|
||||||
: deny("NOT_PARTICIPANT", "仅订单参与方可执行该操作")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (action === "RESOLVE_DISPUTE") {
|
if (action === "RESOLVE_DISPUTE") {
|
||||||
return isOrderOwnerActor(order, actor)
|
return isOrderOwnerActor(order, actor) ? allow() : deny(403, "仅订单所属店主可执行该操作")
|
||||||
? allow()
|
|
||||||
: deny("ROLE_FORBIDDEN", "仅订单所属店主可执行该操作")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return allow()
|
return allow()
|
||||||
@@ -261,7 +255,7 @@ export const useOrderStore = create<OrderState>((set, get) => {
|
|||||||
actor?: Actor,
|
actor?: Actor,
|
||||||
): OrderMutationResult => {
|
): OrderMutationResult => {
|
||||||
const order = get().orders.find((item) => item.id === orderId)
|
const order = get().orders.find((item) => item.id === orderId)
|
||||||
if (!order) return { decision: deny("NOT_FOUND", "订单不存在") }
|
if (!order) return { decision: deny(404, "订单不存在") }
|
||||||
|
|
||||||
const actorDecision = validateActorForAction(order, action, actor)
|
const actorDecision = validateActorForAction(order, action, actor)
|
||||||
if (!actorDecision.ok) return { decision: actorDecision }
|
if (!actorDecision.ok) return { decision: actorDecision }
|
||||||
@@ -285,7 +279,7 @@ export const useOrderStore = create<OrderState>((set, get) => {
|
|||||||
}))
|
}))
|
||||||
|
|
||||||
if (!updatedOrder || !previousOrder) {
|
if (!updatedOrder || !previousOrder) {
|
||||||
return { decision: deny("NOT_FOUND", "订单不存在") }
|
return { decision: deny(404, "订单不存在") }
|
||||||
}
|
}
|
||||||
|
|
||||||
if (previousOrder.status !== "completed" && updatedOrder.status === "completed") {
|
if (previousOrder.status !== "completed" && updatedOrder.status === "completed") {
|
||||||
@@ -339,19 +333,19 @@ export const useOrderStore = create<OrderState>((set, get) => {
|
|||||||
},
|
},
|
||||||
createPaidOrder: (input, actor) => {
|
createPaidOrder: (input, actor) => {
|
||||||
if (actor.role !== "consumer" || actor.userId !== input.consumerId) {
|
if (actor.role !== "consumer" || actor.userId !== input.consumerId) {
|
||||||
return { decision: deny("ROLE_FORBIDDEN", "仅客户可下单支付") }
|
return { decision: deny(403, "仅客户可下单支付") }
|
||||||
}
|
}
|
||||||
|
|
||||||
const dedupeKey = `${input.consumerId}-${input.service.id}`
|
const dedupeKey = `${input.consumerId}-${input.service.id}`
|
||||||
if (pendingCreations.has(dedupeKey)) {
|
if (pendingCreations.has(dedupeKey)) {
|
||||||
return { decision: deny("DUPLICATE_REQUEST", "订单正在创建中,请勿重复提交") }
|
return { decision: deny(400, "订单正在创建中,请勿重复提交") }
|
||||||
}
|
}
|
||||||
pendingCreations.add(dedupeKey)
|
pendingCreations.add(dedupeKey)
|
||||||
const orderId = generateId("ord")
|
const orderId = generateId("ord")
|
||||||
const paid = useWalletStore.getState().deductBalance(orderId, input.totalPrice)
|
const paid = useWalletStore.getState().deductBalance(orderId, input.totalPrice)
|
||||||
if (!paid) {
|
if (!paid) {
|
||||||
pendingCreations.delete(dedupeKey)
|
pendingCreations.delete(dedupeKey)
|
||||||
return { decision: deny("PAYMENT_FAILED", "余额不足或订单已支付") }
|
return { decision: deny(400, "余额不足或订单已支付") }
|
||||||
}
|
}
|
||||||
|
|
||||||
const order: Order = {
|
const order: Order = {
|
||||||
@@ -380,9 +374,9 @@ export const useOrderStore = create<OrderState>((set, get) => {
|
|||||||
},
|
},
|
||||||
payOrder: (orderId, actor) => {
|
payOrder: (orderId, actor) => {
|
||||||
const order = get().orders.find((item) => item.id === orderId)
|
const order = get().orders.find((item) => item.id === orderId)
|
||||||
if (!order) return { decision: deny("NOT_FOUND", "订单不存在") }
|
if (!order) return { decision: deny(404, "订单不存在") }
|
||||||
const paid = useWalletStore.getState().deductBalance(orderId, order.totalPrice)
|
const paid = useWalletStore.getState().deductBalance(orderId, order.totalPrice)
|
||||||
if (!paid) return { decision: deny("PAYMENT_FAILED", "余额不足或订单已支付") }
|
if (!paid) return { decision: deny(400, "余额不足或订单已支付") }
|
||||||
return applyTransition(orderId, "PAY", actor)
|
return applyTransition(orderId, "PAY", actor)
|
||||||
},
|
},
|
||||||
acceptOrder: (orderId, actor) => applyTransition(orderId, "ACCEPT", actor),
|
acceptOrder: (orderId, actor) => applyTransition(orderId, "ACCEPT", actor),
|
||||||
|
|||||||
+9
-9
@@ -1,7 +1,7 @@
|
|||||||
|
import { allow, deny } from "@/lib/decision"
|
||||||
|
import type { ApiDecision } from "@/lib/errors"
|
||||||
import { generateId } from "@/lib/id"
|
import { generateId } from "@/lib/id"
|
||||||
import { mockReviews, mockUsers } from "@/lib/mock"
|
import { mockReviews, mockUsers } from "@/lib/mock"
|
||||||
import { allow, deny } from "@/lib/policy/assert"
|
|
||||||
import type { PolicyDecision } from "@/lib/policy/decision"
|
|
||||||
import type { Review } from "@/lib/types"
|
import type { Review } from "@/lib/types"
|
||||||
import { useOrderStore } from "@/store/orders"
|
import { useOrderStore } from "@/store/orders"
|
||||||
import { create } from "zustand"
|
import { create } from "zustand"
|
||||||
@@ -15,7 +15,7 @@ interface SubmitReviewInput {
|
|||||||
|
|
||||||
interface ReviewState {
|
interface ReviewState {
|
||||||
reviews: Review[]
|
reviews: Review[]
|
||||||
submitReview: (input: SubmitReviewInput) => PolicyDecision
|
submitReview: (input: SubmitReviewInput) => ApiDecision
|
||||||
getReviewsByOrder: (orderId: string) => Review[]
|
getReviewsByOrder: (orderId: string) => Review[]
|
||||||
hasUserReviewed: (orderId: string, userId: string) => boolean
|
hasUserReviewed: (orderId: string, userId: string) => boolean
|
||||||
}
|
}
|
||||||
@@ -54,30 +54,30 @@ export const useReviewStore = create<ReviewState>((set, get) => ({
|
|||||||
get().reviews.some((review) => review.orderId === orderId && review.fromUserId === userId),
|
get().reviews.some((review) => review.orderId === orderId && review.fromUserId === userId),
|
||||||
submitReview: (input) => {
|
submitReview: (input) => {
|
||||||
if (!input.fromUserId) {
|
if (!input.fromUserId) {
|
||||||
return deny("AUTH_REQUIRED", "请先登录")
|
return deny(401, "请先登录")
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!Number.isFinite(input.rating) || input.rating < 1 || input.rating > 5) {
|
if (!Number.isFinite(input.rating) || input.rating < 1 || input.rating > 5) {
|
||||||
return deny("VALIDATION_FAILED", "评分范围应为 1-5")
|
return deny(400, "评分范围应为 1-5")
|
||||||
}
|
}
|
||||||
|
|
||||||
const order = useOrderStore.getState().orders.find((item) => item.id === input.orderId)
|
const order = useOrderStore.getState().orders.find((item) => item.id === input.orderId)
|
||||||
if (!order) {
|
if (!order) {
|
||||||
return deny("NOT_FOUND", "订单不存在")
|
return deny(404, "订单不存在")
|
||||||
}
|
}
|
||||||
|
|
||||||
if (order.status !== "pending_review") {
|
if (order.status !== "pending_review") {
|
||||||
return deny("INVALID_STATUS", "仅待评价订单可提交评价")
|
return deny(400, "仅待评价订单可提交评价")
|
||||||
}
|
}
|
||||||
|
|
||||||
const relation = resolveOrderUser(input.orderId, input.fromUserId)
|
const relation = resolveOrderUser(input.orderId, input.fromUserId)
|
||||||
if (!relation) {
|
if (!relation) {
|
||||||
return deny("NOT_PARTICIPANT", "仅订单参与方可评价")
|
return deny(403, "仅订单参与方可评价")
|
||||||
}
|
}
|
||||||
|
|
||||||
const exists = get().hasUserReviewed(input.orderId, input.fromUserId)
|
const exists = get().hasUserReviewed(input.orderId, input.fromUserId)
|
||||||
if (exists) {
|
if (exists) {
|
||||||
return deny("ALREADY_DONE", "该订单已提交过评价")
|
return deny(400, "该订单已提交过评价")
|
||||||
}
|
}
|
||||||
|
|
||||||
const fromUser = resolveUser(input.fromUserId)
|
const fromUser = resolveUser(input.fromUserId)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
|
import type { Actor } from "@/lib/actor"
|
||||||
import { evaluateOrderTransition } from "@/lib/domain/order-machine"
|
import { evaluateOrderTransition } from "@/lib/domain/order-machine"
|
||||||
import type { Actor } from "@/lib/policy/actor"
|
|
||||||
import type { UserRole } from "@/lib/types"
|
import type { UserRole } from "@/lib/types"
|
||||||
import { describe, expect, it } from "vitest"
|
import { describe, expect, it } from "vitest"
|
||||||
|
|
||||||
@@ -32,7 +32,7 @@ describe("evaluateOrderTransition", () => {
|
|||||||
|
|
||||||
expect(result.decision).toMatchObject({
|
expect(result.decision).toMatchObject({
|
||||||
ok: false,
|
ok: false,
|
||||||
reasonCode: "INVALID_STATUS",
|
error: { code: 400 },
|
||||||
})
|
})
|
||||||
expect(result.nextStatus).toBeUndefined()
|
expect(result.nextStatus).toBeUndefined()
|
||||||
})
|
})
|
||||||
@@ -46,7 +46,7 @@ describe("evaluateOrderTransition", () => {
|
|||||||
|
|
||||||
expect(result.decision).toMatchObject({
|
expect(result.decision).toMatchObject({
|
||||||
ok: false,
|
ok: false,
|
||||||
reasonCode: "ROLE_FORBIDDEN",
|
error: { code: 403 },
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -117,7 +117,7 @@ describe("evaluateOrderTransition", () => {
|
|||||||
|
|
||||||
expect(result.decision).toMatchObject({
|
expect(result.decision).toMatchObject({
|
||||||
ok: false,
|
ok: false,
|
||||||
reasonCode: "ROLE_FORBIDDEN",
|
error: { code: 403 },
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { allow, deny, requireAuth } from "@/lib/policy/assert"
|
import { allow, deny, requireAuth } from "@/lib/decision"
|
||||||
import { describe, expect, it } from "vitest"
|
import { describe, expect, it } from "vitest"
|
||||||
|
|
||||||
describe("policy decision helpers", () => {
|
describe("policy decision helpers", () => {
|
||||||
@@ -6,24 +6,22 @@ describe("policy decision helpers", () => {
|
|||||||
expect(allow()).toEqual({ ok: true })
|
expect(allow()).toEqual({ ok: true })
|
||||||
})
|
})
|
||||||
|
|
||||||
it("returns reason code for deny", () => {
|
it("returns error for deny", () => {
|
||||||
expect(deny("ROLE_FORBIDDEN", "forbidden")).toEqual({
|
expect(deny(403, "forbidden")).toEqual({
|
||||||
ok: false,
|
ok: false,
|
||||||
reasonCode: "ROLE_FORBIDDEN",
|
error: { code: 403, msg: "forbidden" },
|
||||||
message: "forbidden",
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it("requires auth actor", () => {
|
it("requires auth actor", () => {
|
||||||
expect(requireAuth(undefined)).toEqual({
|
expect(requireAuth(undefined)).toEqual({
|
||||||
ok: false,
|
ok: false,
|
||||||
reasonCode: "AUTH_REQUIRED",
|
error: { code: 401, msg: "请先登录" },
|
||||||
message: "请先登录",
|
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
requireAuth({
|
requireAuth({
|
||||||
userId: "u1",
|
userId: "1001",
|
||||||
role: "consumer",
|
role: "consumer",
|
||||||
}),
|
}),
|
||||||
).toEqual({ ok: true })
|
).toEqual({ ok: true })
|
||||||
|
|||||||
Reference in New Issue
Block a user