refactor(errors): migrate decisions to {code,msg}
This commit is contained in:
+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 { allow, deny } from "@/lib/decision"
|
||||
import type { ApiDecision } from "@/lib/errors"
|
||||
import { generateId } from "@/lib/id"
|
||||
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 { useAuthStore } from "@/store/auth"
|
||||
import { useNotificationStore } from "@/store/notifications"
|
||||
@@ -36,7 +36,7 @@ interface SubmitDisputeInput {
|
||||
}
|
||||
|
||||
interface DisputeMutationResult {
|
||||
decision: PolicyDecision
|
||||
decision: ApiDecision
|
||||
dispute?: DisputeRecord
|
||||
}
|
||||
|
||||
@@ -49,8 +49,8 @@ interface DisputeState {
|
||||
actorId: string,
|
||||
reason: string,
|
||||
evidence: string[],
|
||||
) => PolicyDecision
|
||||
submitAppeal: (disputeId: string, actorId: string, reason: string) => PolicyDecision
|
||||
) => ApiDecision
|
||||
submitAppeal: (disputeId: string, actorId: string, reason: string) => ApiDecision
|
||||
}
|
||||
|
||||
const progressTimers = new Map<string, ReturnType<typeof setTimeout>[]>()
|
||||
@@ -205,20 +205,20 @@ export const useDisputeStore = create<DisputeState>((set, get) => {
|
||||
submitDispute: (input) => {
|
||||
const order = useOrderStore.getState().orders.find((item) => item.id === input.orderId)
|
||||
if (!order) {
|
||||
return { decision: deny("NOT_FOUND", "订单不存在") }
|
||||
return { decision: deny(404, "订单不存在") }
|
||||
}
|
||||
|
||||
if (order.status !== "in_progress" && order.status !== "pending_close") {
|
||||
return { decision: deny("INVALID_STATUS", "当前阶段不可发起争议") }
|
||||
return { decision: deny(400, "当前阶段不可发起争议") }
|
||||
}
|
||||
|
||||
if (!input.reason.trim()) {
|
||||
return { decision: deny("VALIDATION_FAILED", "请填写争议原因") }
|
||||
return { decision: deny(400, "请填写争议原因") }
|
||||
}
|
||||
|
||||
const actor = resolveParticipantActor(input.orderId, input.initiatorId)
|
||||
if (!actor) {
|
||||
return { decision: deny("NOT_PARTICIPANT", "仅订单参与方可发起争议") }
|
||||
return { decision: deny(403, "仅订单参与方可发起争议") }
|
||||
}
|
||||
|
||||
const markResult = useOrderStore.getState().markDisputed(input.orderId, actor)
|
||||
@@ -254,28 +254,28 @@ export const useDisputeStore = create<DisputeState>((set, get) => {
|
||||
submitResponse: (disputeId, actorId, reason, evidence) => {
|
||||
const dispute = get().disputes.find((item) => item.id === disputeId)
|
||||
if (!dispute) {
|
||||
return deny("NOT_FOUND", "争议不存在")
|
||||
return deny(404, "争议不存在")
|
||||
}
|
||||
|
||||
const actor = resolveParticipantActor(dispute.orderId, actorId)
|
||||
if (!actor) {
|
||||
return deny("NOT_PARTICIPANT", "仅订单参与方可提交回应")
|
||||
return deny(403, "仅订单参与方可提交回应")
|
||||
}
|
||||
|
||||
if (actorId === dispute.initiatorId) {
|
||||
return deny("ROLE_FORBIDDEN", "争议发起方不可提交回应")
|
||||
return deny(403, "争议发起方不可提交回应")
|
||||
}
|
||||
|
||||
if (dispute.respondentReason) {
|
||||
return deny("ALREADY_DONE", "回应已提交")
|
||||
return deny(400, "回应已提交")
|
||||
}
|
||||
|
||||
if (dispute.status !== "open" && dispute.status !== "reviewing") {
|
||||
return deny("INVALID_STATUS", "当前状态不可提交回应")
|
||||
return deny(400, "当前状态不可提交回应")
|
||||
}
|
||||
|
||||
if (!reason.trim()) {
|
||||
return deny("VALIDATION_FAILED", "请填写回应理由")
|
||||
return deny(400, "请填写回应理由")
|
||||
}
|
||||
|
||||
set((state) => ({
|
||||
@@ -306,24 +306,24 @@ export const useDisputeStore = create<DisputeState>((set, get) => {
|
||||
submitAppeal: (disputeId, actorId, reason) => {
|
||||
const dispute = get().disputes.find((item) => item.id === disputeId)
|
||||
if (!dispute) {
|
||||
return deny("NOT_FOUND", "争议不存在")
|
||||
return deny(404, "争议不存在")
|
||||
}
|
||||
|
||||
const actor = resolveParticipantActor(dispute.orderId, actorId)
|
||||
if (!actor) {
|
||||
return deny("NOT_PARTICIPANT", "仅订单参与方可提交申诉")
|
||||
return deny(403, "仅订单参与方可提交申诉")
|
||||
}
|
||||
|
||||
if (dispute.status !== "resolved") {
|
||||
return deny("INVALID_STATUS", "当前状态不可申诉")
|
||||
return deny(400, "当前状态不可申诉")
|
||||
}
|
||||
|
||||
if (dispute.appealedAt) {
|
||||
return deny("ALREADY_DONE", "该争议已申诉过")
|
||||
return deny(400, "该争议已申诉过")
|
||||
}
|
||||
|
||||
if (!reason.trim()) {
|
||||
return deny("VALIDATION_FAILED", "请填写申诉理由")
|
||||
return deny(400, "请填写申诉理由")
|
||||
}
|
||||
|
||||
set((state) => ({
|
||||
|
||||
+18
-24
@@ -1,14 +1,14 @@
|
||||
import type { Actor } from "@/lib/actor"
|
||||
import {
|
||||
ORDER_ACCEPT_TIMEOUT_MS,
|
||||
ORDER_CLOSE_TIMEOUT_MS,
|
||||
ORDER_REVIEW_TIMEOUT_MS,
|
||||
} from "@/lib/config/demo-timers"
|
||||
import { allow, deny } from "@/lib/decision"
|
||||
import { evaluateOrderTransition, type OrderAction } from "@/lib/domain/order-machine"
|
||||
import type { ApiDecision } from "@/lib/errors"
|
||||
import { generateId } from "@/lib/id"
|
||||
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 { useAuthStore } from "@/store/auth"
|
||||
import { useChatStore } from "@/store/chat"
|
||||
@@ -35,7 +35,7 @@ interface CreatePaidOrderInput extends CreateOrderInput {
|
||||
}
|
||||
|
||||
interface OrderMutationResult {
|
||||
decision: PolicyDecision
|
||||
decision: ApiDecision
|
||||
order?: Order
|
||||
}
|
||||
|
||||
@@ -73,41 +73,35 @@ function isOrderOwnerActor(order: Order, actor: Actor) {
|
||||
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)) {
|
||||
return allow()
|
||||
}
|
||||
|
||||
if (!actor?.userId) {
|
||||
return deny("AUTH_REQUIRED", "请先登录")
|
||||
return deny(401, "请先登录")
|
||||
}
|
||||
|
||||
if (order.status === "disputed" && action !== "RESOLVE_DISPUTE") {
|
||||
return deny("DISPUTE_LOCKED", "争议处理中,暂不可执行此操作")
|
||||
return deny(400, "争议处理中,暂不可执行此操作")
|
||||
}
|
||||
|
||||
if (action === "PAY" || action === "CANCEL_PRE_ACCEPT" || action === "CONFIRM_CLOSE") {
|
||||
return order.consumerId === actor.userId
|
||||
? allow()
|
||||
: deny("NOT_PARTICIPANT", "仅下单客户可执行该操作")
|
||||
return order.consumerId === actor.userId ? allow() : deny(403, "仅下单客户可执行该操作")
|
||||
}
|
||||
|
||||
if (action === "ACCEPT") {
|
||||
return order.playerId === actor.userId || isOrderOwnerActor(order, actor)
|
||||
? allow()
|
||||
: deny("NOT_PARTICIPANT", "仅该订单打手或所属店主可执行接单")
|
||||
: deny(403, "仅该订单打手或所属店主可执行接单")
|
||||
}
|
||||
|
||||
if (action === "REQUEST_CLOSE" || action === "OPEN_DISPUTE" || action === "SUBMIT_REVIEW") {
|
||||
return isParticipant(order, actor.userId)
|
||||
? allow()
|
||||
: deny("NOT_PARTICIPANT", "仅订单参与方可执行该操作")
|
||||
return isParticipant(order, actor.userId) ? allow() : deny(403, "仅订单参与方可执行该操作")
|
||||
}
|
||||
|
||||
if (action === "RESOLVE_DISPUTE") {
|
||||
return isOrderOwnerActor(order, actor)
|
||||
? allow()
|
||||
: deny("ROLE_FORBIDDEN", "仅订单所属店主可执行该操作")
|
||||
return isOrderOwnerActor(order, actor) ? allow() : deny(403, "仅订单所属店主可执行该操作")
|
||||
}
|
||||
|
||||
return allow()
|
||||
@@ -261,7 +255,7 @@ export const useOrderStore = create<OrderState>((set, get) => {
|
||||
actor?: Actor,
|
||||
): OrderMutationResult => {
|
||||
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)
|
||||
if (!actorDecision.ok) return { decision: actorDecision }
|
||||
@@ -285,7 +279,7 @@ export const useOrderStore = create<OrderState>((set, get) => {
|
||||
}))
|
||||
|
||||
if (!updatedOrder || !previousOrder) {
|
||||
return { decision: deny("NOT_FOUND", "订单不存在") }
|
||||
return { decision: deny(404, "订单不存在") }
|
||||
}
|
||||
|
||||
if (previousOrder.status !== "completed" && updatedOrder.status === "completed") {
|
||||
@@ -339,19 +333,19 @@ export const useOrderStore = create<OrderState>((set, get) => {
|
||||
},
|
||||
createPaidOrder: (input, actor) => {
|
||||
if (actor.role !== "consumer" || actor.userId !== input.consumerId) {
|
||||
return { decision: deny("ROLE_FORBIDDEN", "仅客户可下单支付") }
|
||||
return { decision: deny(403, "仅客户可下单支付") }
|
||||
}
|
||||
|
||||
const dedupeKey = `${input.consumerId}-${input.service.id}`
|
||||
if (pendingCreations.has(dedupeKey)) {
|
||||
return { decision: deny("DUPLICATE_REQUEST", "订单正在创建中,请勿重复提交") }
|
||||
return { decision: deny(400, "订单正在创建中,请勿重复提交") }
|
||||
}
|
||||
pendingCreations.add(dedupeKey)
|
||||
const orderId = generateId("ord")
|
||||
const paid = useWalletStore.getState().deductBalance(orderId, input.totalPrice)
|
||||
if (!paid) {
|
||||
pendingCreations.delete(dedupeKey)
|
||||
return { decision: deny("PAYMENT_FAILED", "余额不足或订单已支付") }
|
||||
return { decision: deny(400, "余额不足或订单已支付") }
|
||||
}
|
||||
|
||||
const order: Order = {
|
||||
@@ -380,9 +374,9 @@ export const useOrderStore = create<OrderState>((set, get) => {
|
||||
},
|
||||
payOrder: (orderId, actor) => {
|
||||
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)
|
||||
if (!paid) return { decision: deny("PAYMENT_FAILED", "余额不足或订单已支付") }
|
||||
if (!paid) return { decision: deny(400, "余额不足或订单已支付") }
|
||||
return applyTransition(orderId, "PAY", 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 { 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 { useOrderStore } from "@/store/orders"
|
||||
import { create } from "zustand"
|
||||
@@ -15,7 +15,7 @@ interface SubmitReviewInput {
|
||||
|
||||
interface ReviewState {
|
||||
reviews: Review[]
|
||||
submitReview: (input: SubmitReviewInput) => PolicyDecision
|
||||
submitReview: (input: SubmitReviewInput) => ApiDecision
|
||||
getReviewsByOrder: (orderId: string) => Review[]
|
||||
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),
|
||||
submitReview: (input) => {
|
||||
if (!input.fromUserId) {
|
||||
return deny("AUTH_REQUIRED", "请先登录")
|
||||
return deny(401, "请先登录")
|
||||
}
|
||||
|
||||
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)
|
||||
if (!order) {
|
||||
return deny("NOT_FOUND", "订单不存在")
|
||||
return deny(404, "订单不存在")
|
||||
}
|
||||
|
||||
if (order.status !== "pending_review") {
|
||||
return deny("INVALID_STATUS", "仅待评价订单可提交评价")
|
||||
return deny(400, "仅待评价订单可提交评价")
|
||||
}
|
||||
|
||||
const relation = resolveOrderUser(input.orderId, input.fromUserId)
|
||||
if (!relation) {
|
||||
return deny("NOT_PARTICIPANT", "仅订单参与方可评价")
|
||||
return deny(403, "仅订单参与方可评价")
|
||||
}
|
||||
|
||||
const exists = get().hasUserReviewed(input.orderId, input.fromUserId)
|
||||
if (exists) {
|
||||
return deny("ALREADY_DONE", "该订单已提交过评价")
|
||||
return deny(400, "该订单已提交过评价")
|
||||
}
|
||||
|
||||
const fromUser = resolveUser(input.fromUserId)
|
||||
|
||||
Reference in New Issue
Block a user