refactor(api): add adapter layer for order/chat/review/dispute writes
This commit is contained in:
@@ -9,6 +9,7 @@ import { Badge } from "@/components/ui/badge"
|
|||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
import { Input } from "@/components/ui/input"
|
import { Input } from "@/components/ui/input"
|
||||||
import { ScrollArea } from "@/components/ui/scroll-area"
|
import { ScrollArea } from "@/components/ui/scroll-area"
|
||||||
|
import { sendImageMessage, sendTextMessage } from "@/lib/api/chat"
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
import { useAuthStore } from "@/store/auth"
|
import { useAuthStore } from "@/store/auth"
|
||||||
import { useChatStore } from "@/store/chat"
|
import { useChatStore } from "@/store/chat"
|
||||||
@@ -25,8 +26,6 @@ export default function ChatDetailPage({ params }: { params: Promise<{ id: strin
|
|||||||
() => allMessages.filter((item) => item.sessionId === id),
|
() => allMessages.filter((item) => item.sessionId === id),
|
||||||
[allMessages, id],
|
[allMessages, id],
|
||||||
)
|
)
|
||||||
const sendTextMessage = useChatStore((state) => state.sendTextMessage)
|
|
||||||
const sendImageMessage = useChatStore((state) => state.sendImageMessage)
|
|
||||||
const [input, setInput] = useState("")
|
const [input, setInput] = useState("")
|
||||||
const imageInputRef = useRef<HTMLInputElement>(null)
|
const imageInputRef = useRef<HTMLInputElement>(null)
|
||||||
const { user } = useAuthStore()
|
const { user } = useAuthStore()
|
||||||
@@ -39,8 +38,25 @@ export default function ChatDetailPage({ params }: { params: Promise<{ id: strin
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const userId = user?.id ?? session.participants[0].id
|
if (!user?.id) {
|
||||||
const other = session.participants.find((p) => p.id !== userId) ?? session.participants[1]
|
return (
|
||||||
|
<div className="container mx-auto py-8 px-4 text-center text-muted-foreground">
|
||||||
|
请先登录后查看会话
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const userId = user.id
|
||||||
|
const isParticipant = session.participants.some((participant) => participant.id === userId)
|
||||||
|
if (!isParticipant) {
|
||||||
|
return (
|
||||||
|
<div className="container mx-auto py-8 px-4 text-center text-muted-foreground">
|
||||||
|
仅会话参与方可查看并发送消息
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const other = session.participants.find((p) => p.id !== userId) ?? session.participants[0]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-[calc(100vh-3.5rem)]">
|
<div className="flex flex-col h-[calc(100vh-3.5rem)]">
|
||||||
@@ -130,16 +146,7 @@ export default function ChatDetailPage({ params }: { params: Promise<{ id: strin
|
|||||||
onChange={(event) => {
|
onChange={(event) => {
|
||||||
const file = event.target.files?.[0]
|
const file = event.target.files?.[0]
|
||||||
if (!file) return
|
if (!file) return
|
||||||
const sender = session.participants.find((participant) => participant.id === userId)
|
sendImageMessage(session.id, URL.createObjectURL(file))
|
||||||
sendImageMessage(
|
|
||||||
session.id,
|
|
||||||
{
|
|
||||||
id: userId,
|
|
||||||
name: sender?.name ?? user?.nickname ?? "",
|
|
||||||
avatar: sender?.avatar ?? user?.avatar ?? "",
|
|
||||||
},
|
|
||||||
URL.createObjectURL(file),
|
|
||||||
)
|
|
||||||
event.target.value = ""
|
event.target.value = ""
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -150,16 +157,7 @@ export default function ChatDetailPage({ params }: { params: Promise<{ id: strin
|
|||||||
const text = input.trim()
|
const text = input.trim()
|
||||||
if (!text) return
|
if (!text) return
|
||||||
|
|
||||||
const sender = session.participants.find((participant) => participant.id === userId)
|
sendTextMessage(session.id, text)
|
||||||
sendTextMessage(
|
|
||||||
session.id,
|
|
||||||
{
|
|
||||||
id: userId,
|
|
||||||
name: sender?.name ?? user?.nickname ?? "",
|
|
||||||
avatar: sender?.avatar ?? user?.avatar ?? "",
|
|
||||||
},
|
|
||||||
text,
|
|
||||||
)
|
|
||||||
setInput("")
|
setInput("")
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
import { Badge } from "@/components/ui/badge"
|
import { Badge } from "@/components/ui/badge"
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||||
|
import { submitDispute, submitDisputeAppeal, submitDisputeResponse } from "@/lib/api/disputes"
|
||||||
import { DISPUTE_TO_RESOLVED_MS } from "@/lib/config/demo-timers"
|
import { DISPUTE_TO_RESOLVED_MS } from "@/lib/config/demo-timers"
|
||||||
import { notifyInfo } from "@/lib/toast"
|
import { notifyInfo } from "@/lib/toast"
|
||||||
import { Label } from "@/components/ui/label"
|
import { Label } from "@/components/ui/label"
|
||||||
@@ -38,11 +39,7 @@ export default function DisputePage({ params }: { params: Promise<{ id: string }
|
|||||||
const searchParams = useSearchParams()
|
const searchParams = useSearchParams()
|
||||||
const order = useOrderStore((state) => state.orders.find((item) => item.id === id))
|
const order = useOrderStore((state) => state.orders.find((item) => item.id === id))
|
||||||
const userId = useAuthStore((state) => state.user?.id)
|
const userId = useAuthStore((state) => state.user?.id)
|
||||||
const userName = useAuthStore((state) => state.user?.nickname)
|
|
||||||
const existingDispute = useDisputeStore((state) => state.getDisputeByOrderId(id))
|
const existingDispute = useDisputeStore((state) => state.getDisputeByOrderId(id))
|
||||||
const submitDispute = useDisputeStore((state) => state.submitDispute)
|
|
||||||
const submitResponse = useDisputeStore((state) => state.submitResponse)
|
|
||||||
const submitAppeal = useDisputeStore((state) => state.submitAppeal)
|
|
||||||
|
|
||||||
const [reason, setReason] = useState("")
|
const [reason, setReason] = useState("")
|
||||||
const [files, setFiles] = useState<string[]>([])
|
const [files, setFiles] = useState<string[]>([])
|
||||||
@@ -108,11 +105,9 @@ export default function DisputePage({ params }: { params: Promise<{ id: string }
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleSubmit = () => {
|
const handleSubmit = () => {
|
||||||
if (!userId || !userName || !reason.trim()) return
|
if (!userId || !reason.trim()) return
|
||||||
const result = submitDispute({
|
const result = submitDispute({
|
||||||
orderId: id,
|
orderId: id,
|
||||||
initiatorId: userId,
|
|
||||||
initiatorName: userName,
|
|
||||||
reason,
|
reason,
|
||||||
evidence: files,
|
evidence: files,
|
||||||
})
|
})
|
||||||
@@ -287,12 +282,11 @@ export default function DisputePage({ params }: { params: Promise<{ id: string }
|
|||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (!userId) return
|
if (!userId) return
|
||||||
const decision = submitResponse(
|
const decision = submitDisputeResponse({
|
||||||
existingDispute.id,
|
disputeId: existingDispute.id,
|
||||||
userId,
|
reason: responseReason,
|
||||||
responseReason,
|
evidence: responseFiles,
|
||||||
responseFiles,
|
})
|
||||||
)
|
|
||||||
if (!decision.ok) {
|
if (!decision.ok) {
|
||||||
notifyInfo(decision.message ?? "提交回应失败")
|
notifyInfo(decision.message ?? "提交回应失败")
|
||||||
}
|
}
|
||||||
@@ -337,7 +331,10 @@ export default function DisputePage({ params }: { params: Promise<{ id: string }
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (!userId) return
|
if (!userId) return
|
||||||
const decision = submitAppeal(existingDispute.id, userId, appealReason)
|
const decision = submitDisputeAppeal({
|
||||||
|
disputeId: existingDispute.id,
|
||||||
|
reason: appealReason,
|
||||||
|
})
|
||||||
if (!decision.ok) {
|
if (!decision.ok) {
|
||||||
notifyInfo(decision.message ?? "提交申诉失败")
|
notifyInfo(decision.message ?? "提交申诉失败")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,7 +37,6 @@ export default function OrderDetailPage({ params }: { params: Promise<{ id: stri
|
|||||||
const { id } = use(params)
|
const { id } = use(params)
|
||||||
const order = useOrderStore((state) => state.orders.find((item) => item.id === id))
|
const order = useOrderStore((state) => state.orders.find((item) => item.id === id))
|
||||||
const sessions = useChatStore((state) => state.sessions)
|
const sessions = useChatStore((state) => state.sessions)
|
||||||
const ensureOrderSession = useChatStore((state) => state.ensureOrderSession)
|
|
||||||
const allReviews = useReviewStore((state) => state.reviews)
|
const allReviews = useReviewStore((state) => state.reviews)
|
||||||
// Filtering is deferred to useMemo after reading the raw store array.
|
// Filtering is deferred to useMemo after reading the raw store array.
|
||||||
// Zustand v5 compares selector outputs by reference stability.
|
// Zustand v5 compares selector outputs by reference stability.
|
||||||
@@ -46,12 +45,6 @@ export default function OrderDetailPage({ params }: { params: Promise<{ id: stri
|
|||||||
const reviews = useMemo(() => allReviews.filter((item) => item.orderId === id), [allReviews, id])
|
const reviews = useMemo(() => allReviews.filter((item) => item.orderId === id), [allReviews, id])
|
||||||
const [nowTs, setNowTs] = useState(0)
|
const [nowTs, setNowTs] = useState(0)
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!order) return
|
|
||||||
if (order.status === "pending_payment" || order.status === "cancelled") return
|
|
||||||
ensureOrderSession(order)
|
|
||||||
}, [order, ensureOrderSession])
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!order) return
|
if (!order) return
|
||||||
if (order.status !== "pending_accept" && order.status !== "pending_close") return
|
if (order.status !== "pending_accept" && order.status !== "pending_close") return
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { Button } from "@/components/ui/button"
|
|||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||||
import { Label } from "@/components/ui/label"
|
import { Label } from "@/components/ui/label"
|
||||||
import { Textarea } from "@/components/ui/textarea"
|
import { Textarea } from "@/components/ui/textarea"
|
||||||
|
import { submitReview } from "@/lib/api/reviews"
|
||||||
import { notifyInfo, notifySuccess } from "@/lib/toast"
|
import { notifyInfo, notifySuccess } from "@/lib/toast"
|
||||||
import { useAuthStore } from "@/store/auth"
|
import { useAuthStore } from "@/store/auth"
|
||||||
import { useOrderStore } from "@/store/orders"
|
import { useOrderStore } from "@/store/orders"
|
||||||
@@ -16,7 +17,6 @@ export default function ReviewPage({ params }: { params: Promise<{ id: string }>
|
|||||||
const { id } = use(params)
|
const { id } = use(params)
|
||||||
const order = useOrderStore((state) => state.orders.find((item) => item.id === id))
|
const order = useOrderStore((state) => state.orders.find((item) => item.id === id))
|
||||||
const userId = useAuthStore((state) => state.user?.id)
|
const userId = useAuthStore((state) => state.user?.id)
|
||||||
const submitReview = useReviewStore((state) => state.submitReview)
|
|
||||||
const allReviews = useReviewStore((state) => state.reviews)
|
const allReviews = useReviewStore((state) => state.reviews)
|
||||||
// The selector returns the raw store array and useMemo derives the subset.
|
// The selector returns the raw store array and useMemo derives the subset.
|
||||||
// This keeps useSyncExternalStore snapshots stable across render checks.
|
// This keeps useSyncExternalStore snapshots stable across render checks.
|
||||||
@@ -138,10 +138,13 @@ export default function ReviewPage({ params }: { params: Promise<{ id: string }>
|
|||||||
className="w-full"
|
className="w-full"
|
||||||
disabled={rating === 0 || !userId}
|
disabled={rating === 0 || !userId}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (!userId) return
|
if (!userId) {
|
||||||
|
notifyInfo("请先登录")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const decision = submitReview({
|
const decision = submitReview({
|
||||||
orderId: id,
|
orderId: id,
|
||||||
fromUserId: userId,
|
|
||||||
rating,
|
rating,
|
||||||
content,
|
content,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -12,6 +12,14 @@ import {
|
|||||||
import Link from "next/link"
|
import Link from "next/link"
|
||||||
import { useCallback, useEffect } from "react"
|
import { useCallback, useEffect } from "react"
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
|
import {
|
||||||
|
acceptOrder,
|
||||||
|
acceptOrderAsActor,
|
||||||
|
cancelPreAccept,
|
||||||
|
confirmClose,
|
||||||
|
payOrder,
|
||||||
|
requestClose,
|
||||||
|
} from "@/lib/api/orders"
|
||||||
import type { Actor } from "@/lib/policy/actor"
|
import type { Actor } from "@/lib/policy/actor"
|
||||||
import { notifyInfo, notifySuccess } from "@/lib/toast"
|
import { notifyInfo, notifySuccess } from "@/lib/toast"
|
||||||
import type { OrderStatus } from "@/lib/types"
|
import type { OrderStatus } from "@/lib/types"
|
||||||
@@ -37,21 +45,9 @@ export default function OrderActions({
|
|||||||
chatSessionId,
|
chatSessionId,
|
||||||
serviceId,
|
serviceId,
|
||||||
}: OrderActionsProps) {
|
}: OrderActionsProps) {
|
||||||
const currentRole = useAuthStore((state) => state.currentRole)
|
|
||||||
const currentUserId = useAuthStore((state) => state.user?.id)
|
const currentUserId = useAuthStore((state) => state.user?.id)
|
||||||
const payOrder = useOrderStore((state) => state.payOrder)
|
|
||||||
const acceptOrder = useOrderStore((state) => state.acceptOrder)
|
|
||||||
const requestClose = useOrderStore((state) => state.requestClose)
|
|
||||||
const confirmClose = useOrderStore((state) => state.confirmClose)
|
|
||||||
const cancelPreAccept = useOrderStore((state) => state.cancelPreAccept)
|
|
||||||
const order = useOrderStore((state) => state.orders.find((item) => item.id === orderId))
|
const order = useOrderStore((state) => state.orders.find((item) => item.id === orderId))
|
||||||
const sessions = useChatStore((state) => state.sessions)
|
const sessions = useChatStore((state) => state.sessions)
|
||||||
const ensureOrderSession = useChatStore((state) => state.ensureOrderSession)
|
|
||||||
const actorShopId = useShopStore((state) => {
|
|
||||||
if (!currentUserId || currentRole !== "owner") return undefined
|
|
||||||
const owned = state.shops.find((shop) => shop.owner.id === currentUserId)
|
|
||||||
return owned?.id
|
|
||||||
})
|
|
||||||
const dispatchMode = useShopStore((state) => {
|
const dispatchMode = useShopStore((state) => {
|
||||||
if (!order?.shopId) return "manual"
|
if (!order?.shopId) return "manual"
|
||||||
const shop = state.shops.find((item) => item.id === order.shopId)
|
const shop = state.shops.find((item) => item.id === order.shopId)
|
||||||
@@ -62,13 +58,6 @@ export default function OrderActions({
|
|||||||
sessions.find((session) => session.type === "order" && session.orderId === orderId)?.id
|
sessions.find((session) => session.type === "order" && session.orderId === orderId)?.id
|
||||||
|
|
||||||
const status = order?.status ?? initialStatus
|
const status = order?.status ?? initialStatus
|
||||||
const actor: Actor | undefined = currentUserId
|
|
||||||
? {
|
|
||||||
userId: currentUserId,
|
|
||||||
role: currentRole,
|
|
||||||
shopId: actorShopId,
|
|
||||||
}
|
|
||||||
: undefined
|
|
||||||
|
|
||||||
const handleDecision = useCallback(
|
const handleDecision = useCallback(
|
||||||
(okMessage: string, result: { decision: { ok: boolean; message?: string } }) => {
|
(okMessage: string, result: { decision: { ok: boolean; message?: string } }) => {
|
||||||
@@ -82,11 +71,6 @@ export default function OrderActions({
|
|||||||
[],
|
[],
|
||||||
)
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (chatSessionId || !order || resolvedChatSessionId) return
|
|
||||||
ensureOrderSession(order)
|
|
||||||
}, [chatSessionId, order, ensureOrderSession, resolvedChatSessionId])
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!order) return
|
if (!order) return
|
||||||
if (order.status !== "pending_accept") return
|
if (order.status !== "pending_accept") return
|
||||||
@@ -99,12 +83,12 @@ export default function OrderActions({
|
|||||||
role: "player",
|
role: "player",
|
||||||
shopId: order.shopId,
|
shopId: order.shopId,
|
||||||
}
|
}
|
||||||
const result = acceptOrder(orderId, systemActor)
|
const result = acceptOrderAsActor(orderId, systemActor)
|
||||||
handleDecision("系统已自动派单", result)
|
handleDecision("系统已自动派单", result)
|
||||||
}, 3000)
|
}, 3000)
|
||||||
|
|
||||||
return () => clearTimeout(timer)
|
return () => clearTimeout(timer)
|
||||||
}, [acceptOrder, dispatchMode, handleDecision, order, orderId])
|
}, [dispatchMode, handleDecision, order, orderId])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex gap-2 flex-wrap">
|
<div className="flex gap-2 flex-wrap">
|
||||||
@@ -113,12 +97,12 @@ export default function OrderActions({
|
|||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (!actor) {
|
if (!currentUserId) {
|
||||||
notifyInfo("请先登录")
|
notifyInfo("请先登录")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = cancelPreAccept(orderId, actor)
|
const result = cancelPreAccept(orderId)
|
||||||
handleDecision("订单已取消", result)
|
handleDecision("订单已取消", result)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -127,12 +111,12 @@ export default function OrderActions({
|
|||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (!actor) {
|
if (!currentUserId) {
|
||||||
notifyInfo("请先登录")
|
notifyInfo("请先登录")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = payOrder(orderId, actor)
|
const result = payOrder(orderId)
|
||||||
handleDecision("订单支付成功", result)
|
handleDecision("订单支付成功", result)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -147,12 +131,12 @@ export default function OrderActions({
|
|||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (!actor) {
|
if (!currentUserId) {
|
||||||
notifyInfo("请先登录")
|
notifyInfo("请先登录")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = cancelPreAccept(orderId, actor)
|
const result = cancelPreAccept(orderId)
|
||||||
handleDecision("订单已取消", result)
|
handleDecision("订单已取消", result)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -167,12 +151,12 @@ export default function OrderActions({
|
|||||||
) : (
|
) : (
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (!actor) {
|
if (!currentUserId) {
|
||||||
notifyInfo("请先登录")
|
notifyInfo("请先登录")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = acceptOrder(orderId, actor)
|
const result = acceptOrder(orderId)
|
||||||
handleDecision("已接单", result)
|
handleDecision("已接单", result)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -196,12 +180,12 @@ export default function OrderActions({
|
|||||||
<>
|
<>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (!actor) {
|
if (!currentUserId) {
|
||||||
notifyInfo("请先登录")
|
notifyInfo("请先登录")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = requestClose(orderId, actor)
|
const result = requestClose(orderId)
|
||||||
handleDecision("已发起结单", result)
|
handleDecision("已发起结单", result)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -220,12 +204,12 @@ export default function OrderActions({
|
|||||||
<>
|
<>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (!actor) {
|
if (!currentUserId) {
|
||||||
notifyInfo("请先登录")
|
notifyInfo("请先登录")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = confirmClose(orderId, actor)
|
const result = confirmClose(orderId)
|
||||||
handleDecision("已确认结单", result)
|
handleDecision("已确认结单", result)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
|
import { allow, deny } from "@/lib/policy/assert"
|
||||||
import { useChatStore } from "@/store/chat"
|
import { useChatStore } from "@/store/chat"
|
||||||
|
import { useAuthStore } from "@/store/auth"
|
||||||
|
|
||||||
export function listChatSessions() {
|
export function listChatSessions() {
|
||||||
return useChatStore.getState().sessions
|
return useChatStore.getState().sessions
|
||||||
@@ -11,3 +13,43 @@ export function getChatSessionById(sessionId: string) {
|
|||||||
export function listChatMessages(sessionId: string) {
|
export function listChatMessages(sessionId: string) {
|
||||||
return useChatStore.getState().messages.filter((message) => message.sessionId === sessionId)
|
return useChatStore.getState().messages.filter((message) => message.sessionId === sessionId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function sendTextMessage(sessionId: string, content: string) {
|
||||||
|
const userId = useAuthStore.getState().user?.id
|
||||||
|
if (!userId) return deny("AUTH_REQUIRED", "请先登录")
|
||||||
|
|
||||||
|
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.participants.some((participant) => participant.id === userId)) {
|
||||||
|
return deny("NOT_PARTICIPANT", "仅会话参与方可发送消息")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!content.trim()) {
|
||||||
|
return deny("VALIDATION_FAILED", "消息不能为空")
|
||||||
|
}
|
||||||
|
|
||||||
|
chatState.sendTextMessage(sessionId, userId, content)
|
||||||
|
return allow()
|
||||||
|
}
|
||||||
|
|
||||||
|
export function sendImageMessage(sessionId: string, imageUrl: string) {
|
||||||
|
const userId = useAuthStore.getState().user?.id
|
||||||
|
if (!userId) return deny("AUTH_REQUIRED", "请先登录")
|
||||||
|
|
||||||
|
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.participants.some((participant) => participant.id === userId)) {
|
||||||
|
return deny("NOT_PARTICIPANT", "仅会话参与方可发送消息")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!imageUrl.trim()) {
|
||||||
|
return deny("VALIDATION_FAILED", "图片地址无效")
|
||||||
|
}
|
||||||
|
|
||||||
|
chatState.sendImageMessage(sessionId, userId, imageUrl)
|
||||||
|
return allow()
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { deny } from "@/lib/policy/assert"
|
||||||
|
import { useAuthStore } from "@/store/auth"
|
||||||
import { useDisputeStore } from "@/store/disputes"
|
import { useDisputeStore } from "@/store/disputes"
|
||||||
|
|
||||||
export function listDisputes() {
|
export function listDisputes() {
|
||||||
@@ -7,3 +9,42 @@ export function listDisputes() {
|
|||||||
export function getDisputeByOrderId(orderId: string) {
|
export function getDisputeByOrderId(orderId: string) {
|
||||||
return useDisputeStore.getState().disputes.find((dispute) => dispute.orderId === orderId)
|
return useDisputeStore.getState().disputes.find((dispute) => dispute.orderId === orderId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 useDisputeStore.getState().submitDispute({
|
||||||
|
orderId: input.orderId,
|
||||||
|
initiatorId: user.id,
|
||||||
|
initiatorName: user.nickname,
|
||||||
|
reason: input.reason,
|
||||||
|
evidence: input.evidence,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function submitDisputeResponse(input: {
|
||||||
|
disputeId: string
|
||||||
|
reason: string
|
||||||
|
evidence: string[]
|
||||||
|
}) {
|
||||||
|
const userId = useAuthStore.getState().user?.id
|
||||||
|
if (!userId) {
|
||||||
|
return deny("AUTH_REQUIRED", "请先登录")
|
||||||
|
}
|
||||||
|
|
||||||
|
return useDisputeStore
|
||||||
|
.getState()
|
||||||
|
.submitResponse(input.disputeId, userId, input.reason, input.evidence)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function submitDisputeAppeal(input: { disputeId: string; reason: string }) {
|
||||||
|
const userId = useAuthStore.getState().user?.id
|
||||||
|
if (!userId) {
|
||||||
|
return deny("AUTH_REQUIRED", "请先登录")
|
||||||
|
}
|
||||||
|
|
||||||
|
return useDisputeStore.getState().submitAppeal(input.disputeId, userId, input.reason)
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,4 +1,11 @@
|
|||||||
|
import { resolveOwnerShop } from "@/lib/domain/resolve-current-shop"
|
||||||
|
import { allow, deny } from "@/lib/policy/assert"
|
||||||
|
import type { Actor } from "@/lib/policy/actor"
|
||||||
|
import type { PolicyDecision } from "@/lib/policy/decision"
|
||||||
|
import type { PlayerService } from "@/lib/types"
|
||||||
|
import { useAuthStore } from "@/store/auth"
|
||||||
import { useOrderStore } from "@/store/orders"
|
import { useOrderStore } from "@/store/orders"
|
||||||
|
import { useShopStore } from "@/store/shops"
|
||||||
|
|
||||||
export function listOrders() {
|
export function listOrders() {
|
||||||
return useOrderStore.getState().orders
|
return useOrderStore.getState().orders
|
||||||
@@ -11,3 +18,82 @@ export function getOrderById(orderId: string) {
|
|||||||
export function listOrdersByConsumer(consumerId: string) {
|
export function listOrdersByConsumer(consumerId: string) {
|
||||||
return useOrderStore.getState().orders.filter((order) => order.consumerId === consumerId)
|
return useOrderStore.getState().orders.filter((order) => order.consumerId === consumerId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface CreatePaidOrderInput {
|
||||||
|
consumerId: string
|
||||||
|
consumerName: string
|
||||||
|
playerId: string
|
||||||
|
playerName: string
|
||||||
|
shopId?: string
|
||||||
|
shopName?: string
|
||||||
|
service: PlayerService
|
||||||
|
totalPrice: number
|
||||||
|
note?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveActorContext(): { actor?: Actor; decision: PolicyDecision } {
|
||||||
|
const auth = useAuthStore.getState()
|
||||||
|
if (!auth.user?.id) {
|
||||||
|
return { decision: deny("AUTH_REQUIRED", "请先登录") }
|
||||||
|
}
|
||||||
|
|
||||||
|
const shopId =
|
||||||
|
auth.currentRole === "owner"
|
||||||
|
? resolveOwnerShop(auth.user.id, useShopStore.getState().shops)?.id
|
||||||
|
: undefined
|
||||||
|
|
||||||
|
return {
|
||||||
|
actor: {
|
||||||
|
userId: auth.user.id,
|
||||||
|
role: auth.currentRole,
|
||||||
|
shopId,
|
||||||
|
},
|
||||||
|
decision: allow(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createPaidOrder(input: CreatePaidOrderInput) {
|
||||||
|
const { actor, decision } = resolveActorContext()
|
||||||
|
if (!actor) return { decision }
|
||||||
|
return useOrderStore.getState().createPaidOrder(input, actor)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function payOrder(orderId: string) {
|
||||||
|
const { actor, decision } = resolveActorContext()
|
||||||
|
if (!actor) return { decision }
|
||||||
|
return useOrderStore.getState().payOrder(orderId, actor)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function acceptOrder(orderId: string) {
|
||||||
|
const { actor, decision } = resolveActorContext()
|
||||||
|
if (!actor) return { decision }
|
||||||
|
return useOrderStore.getState().acceptOrder(orderId, actor)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function acceptOrderAsActor(orderId: string, actor: Actor) {
|
||||||
|
return useOrderStore.getState().acceptOrder(orderId, actor)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function requestClose(orderId: string) {
|
||||||
|
const { actor, decision } = resolveActorContext()
|
||||||
|
if (!actor) return { decision }
|
||||||
|
return useOrderStore.getState().requestClose(orderId, actor)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function confirmClose(orderId: string) {
|
||||||
|
const { actor, decision } = resolveActorContext()
|
||||||
|
if (!actor) return { decision }
|
||||||
|
return useOrderStore.getState().confirmClose(orderId, actor)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function cancelPreAccept(orderId: string) {
|
||||||
|
const { actor, decision } = resolveActorContext()
|
||||||
|
if (!actor) return { decision }
|
||||||
|
return useOrderStore.getState().cancelPreAccept(orderId, actor)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function markDisputed(orderId: string) {
|
||||||
|
const { actor, decision } = resolveActorContext()
|
||||||
|
if (!actor) return { decision }
|
||||||
|
return useOrderStore.getState().markDisputed(orderId, actor)
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { deny } from "@/lib/policy/assert"
|
||||||
|
import { useAuthStore } from "@/store/auth"
|
||||||
import { useReviewStore } from "@/store/reviews"
|
import { useReviewStore } from "@/store/reviews"
|
||||||
|
|
||||||
export function listReviews() {
|
export function listReviews() {
|
||||||
@@ -11,3 +13,17 @@ export function listReviewsByOrder(orderId: string) {
|
|||||||
export function listReviewsByTargetUser(userId: string) {
|
export function listReviewsByTargetUser(userId: string) {
|
||||||
return useReviewStore.getState().reviews.filter((review) => review.toUserId === userId)
|
return useReviewStore.getState().reviews.filter((review) => review.toUserId === userId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function submitReview(input: { orderId: string; rating: number; content?: string }) {
|
||||||
|
const userId = useAuthStore.getState().user?.id
|
||||||
|
if (!userId) {
|
||||||
|
return deny("AUTH_REQUIRED", "请先登录")
|
||||||
|
}
|
||||||
|
|
||||||
|
return useReviewStore.getState().submitReview({
|
||||||
|
orderId: input.orderId,
|
||||||
|
fromUserId: userId,
|
||||||
|
rating: input.rating,
|
||||||
|
content: input.content,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
+12
-12
@@ -3,18 +3,12 @@ import { generateId } from "@/lib/id"
|
|||||||
import { mockChatMessages, mockChatSessions, mockUsers } from "@/lib/mock"
|
import { mockChatMessages, mockChatSessions, mockUsers } from "@/lib/mock"
|
||||||
import type { ChatMessage, ChatSession, Order } from "@/lib/types"
|
import type { ChatMessage, ChatSession, Order } from "@/lib/types"
|
||||||
|
|
||||||
interface Sender {
|
|
||||||
id: string
|
|
||||||
name: string
|
|
||||||
avatar: string
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ChatState {
|
interface ChatState {
|
||||||
sessions: ChatSession[]
|
sessions: ChatSession[]
|
||||||
messages: ChatMessage[]
|
messages: ChatMessage[]
|
||||||
ensureOrderSession: (order: Order) => ChatSession
|
ensureOrderSession: (order: Order) => ChatSession
|
||||||
sendTextMessage: (sessionId: string, sender: Sender, content: string) => void
|
sendTextMessage: (sessionId: string, actorId: string, content: string) => void
|
||||||
sendImageMessage: (sessionId: string, sender: Sender, imageUrl: string) => void
|
sendImageMessage: (sessionId: string, actorId: string, imageUrl: string) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveAvatar(userId: string) {
|
function resolveAvatar(userId: string) {
|
||||||
@@ -72,17 +66,20 @@ export const useChatStore = create<ChatState>((set, get) => ({
|
|||||||
|
|
||||||
return session
|
return session
|
||||||
},
|
},
|
||||||
sendTextMessage: (sessionId, sender, content) => {
|
sendTextMessage: (sessionId, actorId, content) => {
|
||||||
const text = content.trim()
|
const text = content.trim()
|
||||||
if (!text) return
|
if (!text) return
|
||||||
const session = get().sessions.find((item) => item.id === sessionId)
|
const session = get().sessions.find((item) => item.id === sessionId)
|
||||||
if (!session || session.readonly) return
|
if (!session || session.readonly) return
|
||||||
|
|
||||||
|
const sender = session.participants.find((participant) => participant.id === actorId)
|
||||||
|
if (!sender) return
|
||||||
|
|
||||||
const now = new Date().toISOString()
|
const now = new Date().toISOString()
|
||||||
const message: ChatMessage = {
|
const message: ChatMessage = {
|
||||||
id: generateId("msg"),
|
id: generateId("msg"),
|
||||||
sessionId,
|
sessionId,
|
||||||
senderId: sender.id,
|
senderId: actorId,
|
||||||
senderName: sender.name,
|
senderName: sender.name,
|
||||||
senderAvatar: sender.avatar,
|
senderAvatar: sender.avatar,
|
||||||
type: "text",
|
type: "text",
|
||||||
@@ -103,17 +100,20 @@ export const useChatStore = create<ChatState>((set, get) => ({
|
|||||||
),
|
),
|
||||||
}))
|
}))
|
||||||
},
|
},
|
||||||
sendImageMessage: (sessionId, sender, imageUrl) => {
|
sendImageMessage: (sessionId, actorId, imageUrl) => {
|
||||||
const content = imageUrl.trim()
|
const content = imageUrl.trim()
|
||||||
if (!content) return
|
if (!content) return
|
||||||
const session = get().sessions.find((item) => item.id === sessionId)
|
const session = get().sessions.find((item) => item.id === sessionId)
|
||||||
if (!session || session.readonly) return
|
if (!session || session.readonly) return
|
||||||
|
|
||||||
|
const sender = session.participants.find((participant) => participant.id === actorId)
|
||||||
|
if (!sender) return
|
||||||
|
|
||||||
const now = new Date().toISOString()
|
const now = new Date().toISOString()
|
||||||
const message: ChatMessage = {
|
const message: ChatMessage = {
|
||||||
id: generateId("msg"),
|
id: generateId("msg"),
|
||||||
sessionId,
|
sessionId,
|
||||||
senderId: sender.id,
|
senderId: actorId,
|
||||||
senderName: sender.name,
|
senderName: sender.name,
|
||||||
senderAvatar: sender.avatar,
|
senderAvatar: sender.avatar,
|
||||||
type: "image",
|
type: "image",
|
||||||
|
|||||||
Reference in New Issue
Block a user