refactor: remove demo timers and client-side timeout simulation
Remove lib/config/demo-timers.ts and all usages across stores and pages. Order timeout scheduling, dispute auto-progression, and hardcoded countdown displays are removed — timeouts are now handled server-side by the backend.
This commit is contained in:
@@ -7,14 +7,13 @@ import { Label } from "@/components/ui/label"
|
|||||||
import { Separator } from "@/components/ui/separator"
|
import { Separator } from "@/components/ui/separator"
|
||||||
import { StatusBadge, type StatusBadgeProps } from "@/components/ui/status-badge"
|
import { StatusBadge, type StatusBadgeProps } from "@/components/ui/status-badge"
|
||||||
import { Textarea } from "@/components/ui/textarea"
|
import { Textarea } from "@/components/ui/textarea"
|
||||||
import { getOrderById } from "@/lib/api"
|
import { getOrderById, getPlayerById, uploadFile } from "@/lib/api"
|
||||||
import {
|
import {
|
||||||
getDisputeByOrderId,
|
getDisputeByOrderId,
|
||||||
submitDispute,
|
submitDispute,
|
||||||
submitDisputeAppeal,
|
submitDisputeAppeal,
|
||||||
submitDisputeResponse,
|
submitDisputeResponse,
|
||||||
} from "@/lib/api/disputes"
|
} from "@/lib/api/disputes"
|
||||||
import { DISPUTE_TO_RESOLVED_MS } from "@/lib/config/demo-timers"
|
|
||||||
import { notifyInfo } from "@/lib/toast"
|
import { notifyInfo } from "@/lib/toast"
|
||||||
import { useAuthStore } from "@/store/auth"
|
import { useAuthStore } from "@/store/auth"
|
||||||
import { AlertTriangle, ArrowLeft, Clock, FileText, Upload, X } from "lucide-react"
|
import { AlertTriangle, ArrowLeft, Clock, FileText, Upload, X } from "lucide-react"
|
||||||
@@ -81,12 +80,14 @@ export default function DisputePage({ params }: { params: Promise<{ id: string }
|
|||||||
const [existingDispute, setExistingDispute] = useState<Awaited<
|
const [existingDispute, setExistingDispute] = useState<Awaited<
|
||||||
ReturnType<typeof getDisputeByOrderId>
|
ReturnType<typeof getDisputeByOrderId>
|
||||||
> | null>(null)
|
> | null>(null)
|
||||||
|
const [playerUserId, setPlayerUserId] = useState<string | null>(null)
|
||||||
|
|
||||||
const [reason, setReason] = useState("")
|
const [reason, setReason] = useState("")
|
||||||
const [files, setFiles] = useState<string[]>([])
|
const [files, setFiles] = useState<string[]>([])
|
||||||
const [responseReason, setResponseReason] = useState("")
|
const [responseReason, setResponseReason] = useState("")
|
||||||
const [responseFiles, setResponseFiles] = useState<string[]>([])
|
const [responseFiles, setResponseFiles] = useState<string[]>([])
|
||||||
const [appealReason, setAppealReason] = useState("")
|
const [appealReason, setAppealReason] = useState("")
|
||||||
|
const [uploading, setUploading] = useState(false)
|
||||||
|
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null)
|
const fileInputRef = useRef<HTMLInputElement>(null)
|
||||||
const responseFileInputRef = useRef<HTMLInputElement>(null)
|
const responseFileInputRef = useRef<HTMLInputElement>(null)
|
||||||
@@ -100,6 +101,7 @@ export default function DisputePage({ params }: { params: Promise<{ id: string }
|
|||||||
setLoading(true)
|
setLoading(true)
|
||||||
setOrder(null)
|
setOrder(null)
|
||||||
setExistingDispute(null)
|
setExistingDispute(null)
|
||||||
|
setPlayerUserId(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
const load = async () => {
|
const load = async () => {
|
||||||
@@ -113,6 +115,13 @@ export default function DisputePage({ params }: { params: Promise<{ id: string }
|
|||||||
if (cancelled) return
|
if (cancelled) return
|
||||||
setOrder(nextOrder ?? null)
|
setOrder(nextOrder ?? null)
|
||||||
setExistingDispute(nextDispute ?? null)
|
setExistingDispute(nextDispute ?? null)
|
||||||
|
|
||||||
|
if (nextOrder) {
|
||||||
|
const player = await getPlayerById(String(nextOrder.playerId))
|
||||||
|
if (cancelled) return
|
||||||
|
setPlayerUserId(player?.user.id ?? null)
|
||||||
|
}
|
||||||
|
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -133,48 +142,51 @@ export default function DisputePage({ params }: { params: Promise<{ id: string }
|
|||||||
|
|
||||||
useEffect(
|
useEffect(
|
||||||
() => () => {
|
() => () => {
|
||||||
filesRef.current.forEach((url) => {
|
filesRef.current = []
|
||||||
URL.revokeObjectURL(url)
|
responseFilesRef.current = []
|
||||||
})
|
|
||||||
responseFilesRef.current.forEach((url) => {
|
|
||||||
URL.revokeObjectURL(url)
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
[],
|
[],
|
||||||
)
|
)
|
||||||
|
|
||||||
const handleFileSelect = (
|
const handleFileSelect = async (
|
||||||
event: ChangeEvent<HTMLInputElement>,
|
event: ChangeEvent<HTMLInputElement>,
|
||||||
setter: Dispatch<SetStateAction<string[]>>,
|
setter: Dispatch<SetStateAction<string[]>>,
|
||||||
|
currentFiles: string[],
|
||||||
) => {
|
) => {
|
||||||
const selectedFiles = event.target.files
|
const selectedFiles = event.target.files
|
||||||
if (!selectedFiles?.length) return
|
if (!selectedFiles?.length) return
|
||||||
|
|
||||||
setter((prev) => {
|
setUploading(true)
|
||||||
const remaining = 5 - prev.length
|
try {
|
||||||
if (remaining <= 0) return prev
|
const remaining = 5 - currentFiles.length
|
||||||
|
if (remaining <= 0) return
|
||||||
const nextUrls = Array.from(selectedFiles)
|
const nextUrls = await Promise.all(
|
||||||
|
Array.from(selectedFiles)
|
||||||
.slice(0, remaining)
|
.slice(0, remaining)
|
||||||
.map((file) => URL.createObjectURL(file))
|
.map((file) => uploadFile(file, "dispute")),
|
||||||
return [...prev, ...nextUrls]
|
)
|
||||||
})
|
setter((prev) => [...prev, ...nextUrls])
|
||||||
|
} catch {
|
||||||
|
notifyInfo("证据上传失败")
|
||||||
|
} finally {
|
||||||
|
setUploading(false)
|
||||||
event.target.value = ""
|
event.target.value = ""
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const removeFile = (
|
const removeFile = (
|
||||||
index: number,
|
index: number,
|
||||||
filesState: string[],
|
filesState: string[],
|
||||||
setter: Dispatch<SetStateAction<string[]>>,
|
setter: Dispatch<SetStateAction<string[]>>,
|
||||||
) => {
|
) => {
|
||||||
const removed = filesState[index]
|
|
||||||
if (removed) {
|
|
||||||
URL.revokeObjectURL(removed)
|
|
||||||
}
|
|
||||||
setter((prev) => prev.filter((_, currentIndex) => currentIndex !== index))
|
setter((prev) => prev.filter((_, currentIndex) => currentIndex !== index))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const reloadDispute = async () => {
|
||||||
|
const nextDispute = await getDisputeByOrderId(id)
|
||||||
|
setExistingDispute(nextDispute ?? null)
|
||||||
|
}
|
||||||
|
|
||||||
const handleSubmit = () => {
|
const handleSubmit = () => {
|
||||||
if (!userId || !reason.trim()) return
|
if (!userId || !reason.trim()) return
|
||||||
|
|
||||||
@@ -212,7 +224,7 @@ export default function DisputePage({ params }: { params: Promise<{ id: string }
|
|||||||
}
|
}
|
||||||
|
|
||||||
const isParticipant = Boolean(
|
const isParticipant = Boolean(
|
||||||
userId && (order.consumerId === userId || order.playerId === userId),
|
userId && (String(order.consumerId) === userId || playerUserId === userId),
|
||||||
)
|
)
|
||||||
if (!isParticipant) {
|
if (!isParticipant) {
|
||||||
return (
|
return (
|
||||||
@@ -337,12 +349,15 @@ export default function DisputePage({ params }: { params: Promise<{ id: string }
|
|||||||
accept="image/*"
|
accept="image/*"
|
||||||
multiple
|
multiple
|
||||||
className="hidden"
|
className="hidden"
|
||||||
onChange={(event) => handleFileSelect(event, setResponseFiles)}
|
onChange={(event) => {
|
||||||
|
void handleFileSelect(event, setResponseFiles, responseFiles)
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className="border-border/60"
|
className="border-border/60"
|
||||||
onClick={() => responseFileInputRef.current?.click()}
|
onClick={() => responseFileInputRef.current?.click()}
|
||||||
|
disabled={uploading}
|
||||||
>
|
>
|
||||||
<Upload className="mr-1 h-4 w-4" />
|
<Upload className="mr-1 h-4 w-4" />
|
||||||
上传回应证据
|
上传回应证据
|
||||||
@@ -385,7 +400,11 @@ export default function DisputePage({ params }: { params: Promise<{ id: string }
|
|||||||
).then((decision) => {
|
).then((decision) => {
|
||||||
if (!decision.ok) {
|
if (!decision.ok) {
|
||||||
notifyInfo(decision.error.msg)
|
notifyInfo(decision.error.msg)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
setResponseReason("")
|
||||||
|
setResponseFiles([])
|
||||||
|
return reloadDispute()
|
||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
disabled={!responseReason.trim()}
|
disabled={!responseReason.trim()}
|
||||||
@@ -437,7 +456,10 @@ export default function DisputePage({ params }: { params: Promise<{ id: string }
|
|||||||
).then((decision) => {
|
).then((decision) => {
|
||||||
if (!decision.ok) {
|
if (!decision.ok) {
|
||||||
notifyInfo(decision.error.msg)
|
notifyInfo(decision.error.msg)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
setAppealReason("")
|
||||||
|
return reloadDispute()
|
||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
disabled={!appealReason.trim()}
|
disabled={!appealReason.trim()}
|
||||||
@@ -477,7 +499,7 @@ export default function DisputePage({ params }: { params: Promise<{ id: string }
|
|||||||
<div className="container mx-auto max-w-2xl px-4 py-8">
|
<div className="container mx-auto max-w-2xl px-4 py-8">
|
||||||
<EmptyState
|
<EmptyState
|
||||||
title="争议已提交,请等待平台处理"
|
title="争议已提交,请等待平台处理"
|
||||||
description={`平台将在约 ${Math.floor(DISPUTE_TO_RESOLVED_MS / 1000)} 秒内给出模拟处理结果。`}
|
description="争议已提交,请等待平台处理"
|
||||||
icon={AlertTriangle}
|
icon={AlertTriangle}
|
||||||
action={
|
action={
|
||||||
<Button variant="outline" className="border-border/60" asChild>
|
<Button variant="outline" className="border-border/60" asChild>
|
||||||
@@ -527,13 +549,15 @@ export default function DisputePage({ params }: { params: Promise<{ id: string }
|
|||||||
accept="image/*"
|
accept="image/*"
|
||||||
multiple
|
multiple
|
||||||
className="hidden"
|
className="hidden"
|
||||||
onChange={(event) => handleFileSelect(event, setFiles)}
|
onChange={(event) => {
|
||||||
|
void handleFileSelect(event, setFiles, files)
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="w-full rounded-md border-2 border-dashed border-border/60 bg-muted/20 p-6 text-center text-sm text-muted-foreground transition-colors hover:border-primary/60 hover:bg-muted/30 hover:text-foreground disabled:opacity-50"
|
className="w-full rounded-md border-2 border-dashed border-border/60 bg-muted/20 p-6 text-center text-sm text-muted-foreground transition-colors hover:border-primary/60 hover:bg-muted/30 hover:text-foreground disabled:opacity-50"
|
||||||
onClick={() => fileInputRef.current?.click()}
|
onClick={() => fileInputRef.current?.click()}
|
||||||
disabled={files.length >= 5}
|
disabled={files.length >= 5 || uploading}
|
||||||
>
|
>
|
||||||
<div className="flex flex-col items-center gap-2">
|
<div className="flex flex-col items-center gap-2">
|
||||||
<Upload className="h-5 w-5" />
|
<Upload className="h-5 w-5" />
|
||||||
@@ -566,13 +590,15 @@ export default function DisputePage({ params }: { params: Promise<{ id: string }
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="text-xs text-muted-foreground">最多5张</div>
|
<div className="text-xs text-muted-foreground">
|
||||||
|
{uploading ? "证据上传中..." : "最多5张"}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-1 rounded-lg border border-border/60 bg-muted/20 p-3 text-xs text-muted-foreground">
|
<div className="space-y-1 rounded-lg border border-border/60 bg-muted/20 p-3 text-xs text-muted-foreground">
|
||||||
<p>· 提交争议后,订单资金将继续托管</p>
|
<p>· 提交争议后,订单资金将继续托管</p>
|
||||||
<p>· 聊天记录将作为证据保留</p>
|
<p>· 聊天记录将作为证据保留</p>
|
||||||
<p>· 平台将在约 {Math.floor(DISPUTE_TO_RESOLVED_MS / 1000)} 秒内完成模拟审核</p>
|
<p>· 平台审核完成后将给出仲裁结果</p>
|
||||||
<p>· 对仲裁结果不满可申诉一次</p>
|
<p>· 对仲裁结果不满可申诉一次</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import { EmptyState } from "@/components/ui/empty-state"
|
|||||||
import { Separator } from "@/components/ui/separator"
|
import { Separator } from "@/components/ui/separator"
|
||||||
import { StatusBadge } from "@/components/ui/status-badge"
|
import { StatusBadge } from "@/components/ui/status-badge"
|
||||||
import { getOrderById, listChatSessions, listReviewsByOrder } from "@/lib/api"
|
import { getOrderById, listChatSessions, listReviewsByOrder } from "@/lib/api"
|
||||||
import { ORDER_ACCEPT_TIMEOUT_MS, ORDER_CLOSE_TIMEOUT_MS } from "@/lib/config/demo-timers"
|
|
||||||
import { statusLabels } from "@/lib/constants"
|
import { statusLabels } from "@/lib/constants"
|
||||||
import type { OrderStatus } from "@/lib/types"
|
import type { OrderStatus } from "@/lib/types"
|
||||||
import { ArrowLeft, CheckCircle, Clock, Star } from "lucide-react"
|
import { ArrowLeft, CheckCircle, Clock, Star } from "lucide-react"
|
||||||
@@ -53,7 +52,6 @@ export default function OrderDetailPage({ params }: { params: Promise<{ id: stri
|
|||||||
undefined,
|
undefined,
|
||||||
)
|
)
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
const [nowTs, setNowTs] = useState(0)
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let cancelled = false
|
let cancelled = false
|
||||||
@@ -115,17 +113,6 @@ export default function OrderDetailPage({ params }: { params: Promise<{ id: stri
|
|||||||
}
|
}
|
||||||
}, [id])
|
}, [id])
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!order) return
|
|
||||||
if (order.status !== "pending_accept" && order.status !== "pending_close") return
|
|
||||||
|
|
||||||
const timer = setInterval(() => {
|
|
||||||
setNowTs(Date.now())
|
|
||||||
}, 1000)
|
|
||||||
|
|
||||||
return () => clearInterval(timer)
|
|
||||||
}, [order])
|
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<div className="container mx-auto max-w-2xl px-4 py-8">
|
<div className="container mx-auto max-w-2xl px-4 py-8">
|
||||||
@@ -150,21 +137,6 @@ export default function OrderDetailPage({ params }: { params: Promise<{ id: stri
|
|||||||
? cancelledStatusSteps
|
? cancelledStatusSteps
|
||||||
: normalStatusSteps
|
: normalStatusSteps
|
||||||
const currentStepIndex = statusSteps.indexOf(order.status)
|
const currentStepIndex = statusSteps.indexOf(order.status)
|
||||||
const timeoutHint = (() => {
|
|
||||||
if (order.status !== "pending_accept" && order.status !== "pending_close") return null
|
|
||||||
|
|
||||||
const base =
|
|
||||||
order.status === "pending_accept"
|
|
||||||
? new Date(order.createdAt).getTime()
|
|
||||||
: new Date(order.createdAt).getTime()
|
|
||||||
const timeoutMs =
|
|
||||||
order.status === "pending_accept" ? ORDER_ACCEPT_TIMEOUT_MS : ORDER_CLOSE_TIMEOUT_MS
|
|
||||||
const remainSeconds = Math.max(0, Math.ceil((timeoutMs - (nowTs - base)) / 1000))
|
|
||||||
|
|
||||||
return order.status === "pending_accept"
|
|
||||||
? `若 ${Math.floor(ORDER_ACCEPT_TIMEOUT_MS / 1000)} 秒内无人接单,订单将自动取消(剩余 ${remainSeconds} 秒)`
|
|
||||||
: `若 ${Math.floor(ORDER_CLOSE_TIMEOUT_MS / 1000)} 秒内未确认,订单将自动进入待评价(剩余 ${remainSeconds} 秒)`
|
|
||||||
})()
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container mx-auto py-8 px-4 max-w-2xl">
|
<div className="container mx-auto py-8 px-4 max-w-2xl">
|
||||||
@@ -212,12 +184,6 @@ export default function OrderDetailPage({ params }: { params: Promise<{ id: stri
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{timeoutHint && (
|
|
||||||
<Card className="mb-6 border-border/80 shadow-sm">
|
|
||||||
<CardContent className="py-3 text-sm text-muted-foreground">{timeoutHint}</CardContent>
|
|
||||||
</Card>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Card className="mb-6 border-border/80 shadow-sm">
|
<Card className="mb-6 border-border/80 shadow-sm">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="text-base">服务信息</CardTitle>
|
<CardTitle className="text-base">服务信息</CardTitle>
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
export const ORDER_ACCEPT_TIMEOUT_MS = 30_000
|
|
||||||
export const ORDER_CLOSE_TIMEOUT_MS = 30_000
|
|
||||||
export const ORDER_REVIEW_TIMEOUT_MS = 30_000
|
|
||||||
|
|
||||||
export const DISPUTE_TO_REVIEWING_MS = 5_000
|
|
||||||
export const DISPUTE_TO_RESOLVED_MS = 10_000
|
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
import type { Actor } from "@/lib/actor"
|
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 { allow, deny } from "@/lib/decision"
|
||||||
import type { ApiDecision } from "@/lib/errors"
|
import type { ApiDecision } from "@/lib/errors"
|
||||||
import { generateId } from "@/lib/id"
|
import { generateId } from "@/lib/id"
|
||||||
@@ -50,17 +49,6 @@ interface DisputeState {
|
|||||||
submitAppeal: (disputeId: string, actorId: string, reason: string) => ApiDecision
|
submitAppeal: (disputeId: string, actorId: string, reason: string) => ApiDecision
|
||||||
}
|
}
|
||||||
|
|
||||||
const progressTimers = new Map<string, ReturnType<typeof setTimeout>[]>()
|
|
||||||
|
|
||||||
function clearProgressTimers(disputeId: string) {
|
|
||||||
const timers = progressTimers.get(disputeId)
|
|
||||||
if (!timers) return
|
|
||||||
timers.forEach((timer) => {
|
|
||||||
clearTimeout(timer)
|
|
||||||
})
|
|
||||||
progressTimers.delete(disputeId)
|
|
||||||
}
|
|
||||||
|
|
||||||
function resolveParticipantActor(orderId: string, userId: string): Actor | null {
|
function resolveParticipantActor(orderId: string, userId: string): Actor | null {
|
||||||
const order = useOrderStore.getState().orders.find((item) => item.id === orderId)
|
const order = useOrderStore.getState().orders.find((item) => item.id === orderId)
|
||||||
if (!order) return null
|
if (!order) return null
|
||||||
@@ -98,69 +86,6 @@ function notifyDispute(orderId: string, title: string, content: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const useDisputeStore = create<DisputeState>((set, get) => {
|
export const useDisputeStore = create<DisputeState>((set, get) => {
|
||||||
const scheduleProgress = (disputeId: string) => {
|
|
||||||
clearProgressTimers(disputeId)
|
|
||||||
|
|
||||||
const toReviewing = setTimeout(() => {
|
|
||||||
set((state) => ({
|
|
||||||
disputes: state.disputes.map((dispute) => {
|
|
||||||
if (dispute.id !== disputeId) return dispute
|
|
||||||
if (dispute.status !== "open" && dispute.status !== "appealed") return dispute
|
|
||||||
|
|
||||||
return {
|
|
||||||
...dispute,
|
|
||||||
status: "reviewing",
|
|
||||||
timeline: [
|
|
||||||
...dispute.timeline,
|
|
||||||
{
|
|
||||||
id: generateId("timeline"),
|
|
||||||
type: "reviewing",
|
|
||||||
content: "平台已受理并进入审核",
|
|
||||||
createdAt: new Date().toISOString(),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
}))
|
|
||||||
}, DISPUTE_TO_REVIEWING_MS)
|
|
||||||
|
|
||||||
const toResolved = setTimeout(() => {
|
|
||||||
let resolvedOrderId: string | null = null
|
|
||||||
set((state) => ({
|
|
||||||
disputes: state.disputes.map((dispute) => {
|
|
||||||
if (dispute.id !== disputeId) return dispute
|
|
||||||
if (dispute.status !== "reviewing" && dispute.status !== "appealed") return dispute
|
|
||||||
|
|
||||||
resolvedOrderId = dispute.orderId
|
|
||||||
|
|
||||||
return {
|
|
||||||
...dispute,
|
|
||||||
status: "resolved",
|
|
||||||
result: dispute.result ?? "partial_refund",
|
|
||||||
timeline: [
|
|
||||||
...dispute.timeline,
|
|
||||||
{
|
|
||||||
id: generateId("timeline"),
|
|
||||||
type: "resolved",
|
|
||||||
content: "平台已给出仲裁结果",
|
|
||||||
createdAt: new Date().toISOString(),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
}))
|
|
||||||
|
|
||||||
if (resolvedOrderId) {
|
|
||||||
notifyDispute(resolvedOrderId, "争议已处理", "平台已给出争议处理结果")
|
|
||||||
useOrderStore.getState().resolveDispute(resolvedOrderId)
|
|
||||||
}
|
|
||||||
|
|
||||||
clearProgressTimers(disputeId)
|
|
||||||
}, DISPUTE_TO_RESOLVED_MS)
|
|
||||||
|
|
||||||
progressTimers.set(disputeId, [toReviewing, toResolved])
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
disputes: [],
|
disputes: [],
|
||||||
getDisputeByOrderId: (orderId) => get().disputes.find((dispute) => dispute.orderId === orderId),
|
getDisputeByOrderId: (orderId) => get().disputes.find((dispute) => dispute.orderId === orderId),
|
||||||
@@ -208,7 +133,6 @@ export const useDisputeStore = create<DisputeState>((set, get) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
set((state) => ({ disputes: [dispute, ...state.disputes] }))
|
set((state) => ({ disputes: [dispute, ...state.disputes] }))
|
||||||
scheduleProgress(dispute.id)
|
|
||||||
return { decision: allow(), dispute }
|
return { decision: allow(), dispute }
|
||||||
},
|
},
|
||||||
submitResponse: (disputeId, actorId, reason, evidence) => {
|
submitResponse: (disputeId, actorId, reason, evidence) => {
|
||||||
@@ -311,7 +235,6 @@ export const useDisputeStore = create<DisputeState>((set, get) => {
|
|||||||
|
|
||||||
notifyDispute(dispute.orderId, "争议已申诉", "申诉已提交,平台将继续复核")
|
notifyDispute(dispute.orderId, "争议已申诉", "申诉已提交,平台将继续复核")
|
||||||
|
|
||||||
scheduleProgress(disputeId)
|
|
||||||
return allow()
|
return allow()
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,4 @@
|
|||||||
import type { Actor } from "@/lib/actor"
|
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 { 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 type { ApiDecision } from "@/lib/errors"
|
||||||
@@ -48,16 +43,8 @@ interface OrderState {
|
|||||||
resolveDispute: (orderId: string) => OrderMutationResult
|
resolveDispute: (orderId: string) => OrderMutationResult
|
||||||
}
|
}
|
||||||
|
|
||||||
const orderTimeouts = new Map<string, ReturnType<typeof setTimeout>>()
|
|
||||||
const pendingCreations = new Set<string>()
|
const pendingCreations = new Set<string>()
|
||||||
|
|
||||||
function clearOrderTimeout(orderId: string) {
|
|
||||||
const timer = orderTimeouts.get(orderId)
|
|
||||||
if (!timer) return
|
|
||||||
clearTimeout(timer)
|
|
||||||
orderTimeouts.delete(orderId)
|
|
||||||
}
|
|
||||||
|
|
||||||
function isParticipant(order: Order, userId: string) {
|
function isParticipant(order: Order, userId: string) {
|
||||||
return order.consumerId === userId || order.playerId === userId
|
return order.consumerId === userId || order.playerId === userId
|
||||||
}
|
}
|
||||||
@@ -149,42 +136,6 @@ function syncChatSession(order: Order, previousStatus: OrderStatus) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function scheduleOrderTimeout(orderId: string, status: OrderStatus) {
|
|
||||||
clearOrderTimeout(orderId)
|
|
||||||
|
|
||||||
if (status !== "pending_accept" && status !== "pending_close" && status !== "pending_review") {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const timeoutMap: Record<"pending_accept" | "pending_close" | "pending_review", number> = {
|
|
||||||
pending_accept: ORDER_ACCEPT_TIMEOUT_MS,
|
|
||||||
pending_close: ORDER_CLOSE_TIMEOUT_MS,
|
|
||||||
pending_review: ORDER_REVIEW_TIMEOUT_MS,
|
|
||||||
}
|
|
||||||
const timeoutMs = timeoutMap[status]
|
|
||||||
|
|
||||||
const timer = setTimeout(() => {
|
|
||||||
const state = useOrderStore.getState()
|
|
||||||
const order = state.orders.find((item) => item.id === orderId)
|
|
||||||
if (!order || order.status !== status) {
|
|
||||||
orderTimeouts.delete(orderId)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (status === "pending_accept") {
|
|
||||||
state.autoTimeoutPendingAccept(orderId)
|
|
||||||
} else if (status === "pending_close") {
|
|
||||||
state.autoTimeoutPendingClose(orderId)
|
|
||||||
} else if (status === "pending_review") {
|
|
||||||
state.autoTimeoutPendingReview(orderId)
|
|
||||||
}
|
|
||||||
|
|
||||||
orderTimeouts.delete(orderId)
|
|
||||||
}, timeoutMs)
|
|
||||||
|
|
||||||
orderTimeouts.set(orderId, timer)
|
|
||||||
}
|
|
||||||
|
|
||||||
function notifyOrderStatus(order: Order) {
|
function notifyOrderStatus(order: Order) {
|
||||||
if (!useAuthStore.getState().notificationPrefs.order) {
|
if (!useAuthStore.getState().notificationPrefs.order) {
|
||||||
return
|
return
|
||||||
@@ -346,7 +297,6 @@ export const useOrderStore = create<OrderState>((set, get) => {
|
|||||||
orders: [order, ...state.orders],
|
orders: [order, ...state.orders],
|
||||||
}))
|
}))
|
||||||
|
|
||||||
scheduleOrderTimeout(order.id, order.status)
|
|
||||||
return { decision: allow(), order }
|
return { decision: allow(), order }
|
||||||
},
|
},
|
||||||
createPaidOrder: (input, actor) => {
|
createPaidOrder: (input, actor) => {
|
||||||
@@ -437,17 +387,3 @@ export const useOrderStore = create<OrderState>((set, get) => {
|
|||||||
resolveDispute: (orderId) => applyTransition(orderId, "RESOLVE_DISPUTE"),
|
resolveDispute: (orderId) => applyTransition(orderId, "RESOLVE_DISPUTE"),
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
useOrderStore.subscribe((state, prevState) => {
|
|
||||||
state.orders.forEach((order) => {
|
|
||||||
const prevOrder = prevState.orders.find((item) => item.id === order.id)
|
|
||||||
if (!prevOrder || prevOrder.status !== order.status) {
|
|
||||||
scheduleOrderTimeout(order.id, order.status)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
prevState.orders.forEach((order) => {
|
|
||||||
if (!state.orders.some((item) => item.id === order.id)) {
|
|
||||||
clearOrderTimeout(order.id)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|||||||
Reference in New Issue
Block a user