fix(review): enforce pending-review submission and remove auto reply
This commit is contained in:
@@ -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 { 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"
|
||||||
import { useReviewStore } from "@/store/reviews"
|
import { useReviewStore } from "@/store/reviews"
|
||||||
@@ -37,6 +38,18 @@ export default function ReviewPage({ params }: { params: Promise<{ id: string }>
|
|||||||
const hasSubmitted = Boolean(userId && reviews.some((review) => review.fromUserId === userId))
|
const hasSubmitted = Boolean(userId && reviews.some((review) => review.fromUserId === userId))
|
||||||
const isRevealed = reviews.length >= 2 && reviews.every((review) => !review.sealed)
|
const isRevealed = reviews.length >= 2 && reviews.every((review) => !review.sealed)
|
||||||
|
|
||||||
|
if (order.status !== "pending_review") {
|
||||||
|
return (
|
||||||
|
<div className="container mx-auto py-8 px-4 max-w-lg text-center space-y-4">
|
||||||
|
<h2 className="text-xl font-bold">当前阶段不可评价</h2>
|
||||||
|
<p className="text-sm text-muted-foreground">仅待评价状态的订单可以提交评价。</p>
|
||||||
|
<Link href={`/order/${id}`} className="text-sm text-primary hover:underline">
|
||||||
|
返回订单详情
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
if (hasSubmitted && !isRevealed) {
|
if (hasSubmitted && !isRevealed) {
|
||||||
return (
|
return (
|
||||||
<div className="container mx-auto py-8 px-4 max-w-lg text-center space-y-4">
|
<div className="container mx-auto py-8 px-4 max-w-lg text-center space-y-4">
|
||||||
@@ -126,12 +139,19 @@ export default function ReviewPage({ params }: { params: Promise<{ id: string }>
|
|||||||
disabled={rating === 0 || !userId}
|
disabled={rating === 0 || !userId}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (!userId) return
|
if (!userId) return
|
||||||
submitReview({
|
const decision = submitReview({
|
||||||
orderId: id,
|
orderId: id,
|
||||||
fromUserId: userId,
|
fromUserId: userId,
|
||||||
rating,
|
rating,
|
||||||
content,
|
content,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if (decision.ok) {
|
||||||
|
notifySuccess("评价已提交")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
notifyInfo(decision.message ?? "评价提交失败")
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
提交评价
|
提交评价
|
||||||
|
|||||||
+27
-30
@@ -1,5 +1,7 @@
|
|||||||
import { create } from "zustand"
|
import { create } from "zustand"
|
||||||
import { generateId } from "@/lib/id"
|
import { generateId } from "@/lib/id"
|
||||||
|
import { allow, deny } from "@/lib/policy/assert"
|
||||||
|
import type { PolicyDecision } from "@/lib/policy/decision"
|
||||||
import { mockReviews, mockUsers } from "@/lib/mock"
|
import { mockReviews, mockUsers } from "@/lib/mock"
|
||||||
import type { Review } from "@/lib/types"
|
import type { Review } from "@/lib/types"
|
||||||
import { useOrderStore } from "@/store/orders"
|
import { useOrderStore } from "@/store/orders"
|
||||||
@@ -13,13 +15,11 @@ interface SubmitReviewInput {
|
|||||||
|
|
||||||
interface ReviewState {
|
interface ReviewState {
|
||||||
reviews: Review[]
|
reviews: Review[]
|
||||||
submitReview: (input: SubmitReviewInput) => void
|
submitReview: (input: SubmitReviewInput) => PolicyDecision
|
||||||
getReviewsByOrder: (orderId: string) => Review[]
|
getReviewsByOrder: (orderId: string) => Review[]
|
||||||
hasUserReviewed: (orderId: string, userId: string) => boolean
|
hasUserReviewed: (orderId: string, userId: string) => boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const autoReplyTimers = new Map<string, ReturnType<typeof setTimeout>>()
|
|
||||||
|
|
||||||
function resolveUser(userId: string) {
|
function resolveUser(userId: string) {
|
||||||
return mockUsers.find((user) => user.id === userId)
|
return mockUsers.find((user) => user.id === userId)
|
||||||
}
|
}
|
||||||
@@ -53,11 +53,32 @@ export const useReviewStore = create<ReviewState>((set, get) => ({
|
|||||||
hasUserReviewed: (orderId, userId) =>
|
hasUserReviewed: (orderId, userId) =>
|
||||||
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) {
|
||||||
|
return deny("AUTH_REQUIRED", "请先登录")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Number.isFinite(input.rating) || input.rating < 1 || input.rating > 5) {
|
||||||
|
return deny("VALIDATION_FAILED", "评分范围应为 1-5")
|
||||||
|
}
|
||||||
|
|
||||||
|
const order = useOrderStore.getState().orders.find((item) => item.id === input.orderId)
|
||||||
|
if (!order) {
|
||||||
|
return deny("NOT_FOUND", "订单不存在")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (order.status !== "pending_review") {
|
||||||
|
return deny("INVALID_STATUS", "仅待评价订单可提交评价")
|
||||||
|
}
|
||||||
|
|
||||||
const relation = resolveOrderUser(input.orderId, input.fromUserId)
|
const relation = resolveOrderUser(input.orderId, input.fromUserId)
|
||||||
if (!relation) return
|
if (!relation) {
|
||||||
|
return deny("NOT_PARTICIPANT", "仅订单参与方可评价")
|
||||||
|
}
|
||||||
|
|
||||||
const exists = get().hasUserReviewed(input.orderId, input.fromUserId)
|
const exists = get().hasUserReviewed(input.orderId, input.fromUserId)
|
||||||
if (exists) return
|
if (exists) {
|
||||||
|
return deny("ALREADY_DONE", "该订单已提交过评价")
|
||||||
|
}
|
||||||
|
|
||||||
const fromUser = resolveUser(input.fromUserId)
|
const fromUser = resolveUser(input.fromUserId)
|
||||||
const createdAt = new Date().toISOString()
|
const createdAt = new Date().toISOString()
|
||||||
@@ -79,37 +100,13 @@ export const useReviewStore = create<ReviewState>((set, get) => ({
|
|||||||
|
|
||||||
const orderReviews = get().getReviewsByOrder(input.orderId)
|
const orderReviews = get().getReviewsByOrder(input.orderId)
|
||||||
if (orderReviews.length >= 2) {
|
if (orderReviews.length >= 2) {
|
||||||
const timer = autoReplyTimers.get(input.orderId)
|
|
||||||
if (timer) {
|
|
||||||
clearTimeout(timer)
|
|
||||||
autoReplyTimers.delete(input.orderId)
|
|
||||||
}
|
|
||||||
|
|
||||||
set((state) => ({
|
set((state) => ({
|
||||||
reviews: state.reviews.map((item) =>
|
reviews: state.reviews.map((item) =>
|
||||||
item.orderId === input.orderId ? { ...item, sealed: false } : item,
|
item.orderId === input.orderId ? { ...item, sealed: false } : item,
|
||||||
),
|
),
|
||||||
}))
|
}))
|
||||||
useOrderStore.getState().updateOrderStatus(input.orderId, "completed")
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (autoReplyTimers.has(input.orderId)) return
|
return allow()
|
||||||
|
|
||||||
const timer = setTimeout(() => {
|
|
||||||
autoReplyTimers.delete(input.orderId)
|
|
||||||
if (get().hasUserReviewed(input.orderId, relation.toUserId)) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
get().submitReview({
|
|
||||||
orderId: input.orderId,
|
|
||||||
fromUserId: relation.toUserId,
|
|
||||||
rating: 5,
|
|
||||||
content: `收到 ${relation.toUserName} 的评价,感谢本次对局。`,
|
|
||||||
})
|
|
||||||
}, 3000)
|
|
||||||
|
|
||||||
autoReplyTimers.set(input.orderId, timer)
|
|
||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
|
|||||||
Reference in New Issue
Block a user