cf0fea9926
Strip store/orders.ts to a thin local cache with setOrders and updateOrder only. Remove all client-side state transition logic, actor validation, chat sync, notification generation, and wallet integration — these are now handled by the backend API. Fix components/order-actions.tsx and stores that depended on the removed order store methods (markDisputed, autoTimeout*).
237 lines
6.4 KiB
TypeScript
237 lines
6.4 KiB
TypeScript
import type { Actor } from "@/lib/actor"
|
|
import { allow, deny } from "@/lib/decision"
|
|
import type { ApiDecision } from "@/lib/errors"
|
|
import { generateId } from "@/lib/id"
|
|
import type { Dispute } from "@/lib/types"
|
|
import { useAuthStore } from "@/store/auth"
|
|
import { useNotificationStore } from "@/store/notifications"
|
|
import { useOrderStore } from "@/store/orders"
|
|
import { create } from "zustand"
|
|
|
|
type DisputeTimelineType = "created" | "response" | "reviewing" | "resolved" | "appealed"
|
|
|
|
interface DisputeTimelineItem {
|
|
id: string
|
|
type: DisputeTimelineType
|
|
content: string
|
|
createdAt: string
|
|
}
|
|
|
|
export interface DisputeRecord extends Dispute {
|
|
respondentReason?: string
|
|
respondentEvidence: string[]
|
|
appealReason?: string
|
|
appealedAt?: string
|
|
timeline: DisputeTimelineItem[]
|
|
}
|
|
|
|
interface SubmitDisputeInput {
|
|
orderId: string
|
|
reason: string
|
|
evidence: string[]
|
|
}
|
|
|
|
interface DisputeMutationResult {
|
|
decision: ApiDecision
|
|
dispute?: DisputeRecord
|
|
}
|
|
|
|
interface DisputeState {
|
|
disputes: DisputeRecord[]
|
|
getDisputeByOrderId: (orderId: string) => DisputeRecord | undefined
|
|
submitDispute: (input: SubmitDisputeInput) => DisputeMutationResult
|
|
submitResponse: (
|
|
disputeId: string,
|
|
actorId: string,
|
|
reason: string,
|
|
evidence: string[],
|
|
) => ApiDecision
|
|
submitAppeal: (disputeId: string, actorId: string, reason: string) => ApiDecision
|
|
}
|
|
|
|
function resolveParticipantActor(orderId: string, userId: string): Actor | null {
|
|
const order = useOrderStore.getState().orders.find((item) => item.id === orderId)
|
|
if (!order) return null
|
|
|
|
if (order.consumerId === userId) {
|
|
return {
|
|
userId,
|
|
role: "consumer",
|
|
shopId: order.shopId,
|
|
}
|
|
}
|
|
|
|
if (order.playerId === userId) {
|
|
return {
|
|
userId,
|
|
role: "player",
|
|
shopId: order.shopId,
|
|
}
|
|
}
|
|
|
|
return null
|
|
}
|
|
|
|
function notifyDispute(orderId: string, title: string, content: string) {
|
|
if (!useAuthStore.getState().notificationPrefs.order) {
|
|
return
|
|
}
|
|
|
|
useNotificationStore.getState().addNotification({
|
|
type: "order",
|
|
title,
|
|
content,
|
|
link: `/dispute/${orderId}`,
|
|
})
|
|
}
|
|
|
|
export const useDisputeStore = create<DisputeState>((set, get) => {
|
|
return {
|
|
disputes: [],
|
|
getDisputeByOrderId: (orderId) => get().disputes.find((dispute) => dispute.orderId === orderId),
|
|
submitDispute: (input) => {
|
|
const order = useOrderStore.getState().orders.find((item) => item.id === input.orderId)
|
|
if (!order) {
|
|
return { decision: deny(404, "订单不存在") }
|
|
}
|
|
|
|
if (order.status !== "in_progress" && order.status !== "pending_close") {
|
|
return { decision: deny(400, "当前阶段不可发起争议") }
|
|
}
|
|
|
|
if (!input.reason.trim()) {
|
|
return { decision: deny(400, "请填写争议原因") }
|
|
}
|
|
|
|
const actor = resolveParticipantActor(input.orderId, useAuthStore.getState().user?.id ?? "")
|
|
if (!actor) {
|
|
return { decision: deny(403, "仅订单参与方可发起争议") }
|
|
}
|
|
|
|
const createdAt = new Date().toISOString()
|
|
const dispute: DisputeRecord = {
|
|
id: generateId("disp"),
|
|
orderId: input.orderId,
|
|
reason: input.reason.trim(),
|
|
evidence: input.evidence,
|
|
status: "open",
|
|
createdAt,
|
|
respondentEvidence: [],
|
|
timeline: [
|
|
{
|
|
id: generateId("timeline"),
|
|
type: "created",
|
|
content: "争议已提交",
|
|
createdAt,
|
|
},
|
|
],
|
|
}
|
|
|
|
set((state) => ({ disputes: [dispute, ...state.disputes] }))
|
|
return { decision: allow(), dispute }
|
|
},
|
|
submitResponse: (disputeId, actorId, reason, evidence) => {
|
|
const dispute = get().disputes.find((item) => item.id === disputeId)
|
|
if (!dispute) {
|
|
return deny(404, "争议不存在")
|
|
}
|
|
|
|
const order = useOrderStore.getState().orders.find((item) => item.id === dispute.orderId)
|
|
if (!order) {
|
|
return deny(404, "关联订单不存在")
|
|
}
|
|
|
|
const actor = resolveParticipantActor(dispute.orderId, actorId)
|
|
if (!actor) {
|
|
return deny(403, "仅订单参与方可提交回应")
|
|
}
|
|
|
|
if (dispute.respondentReason) {
|
|
return deny(400, "回应已提交")
|
|
}
|
|
|
|
if (dispute.status !== "open" && dispute.status !== "reviewing") {
|
|
return deny(400, "当前状态不可提交回应")
|
|
}
|
|
|
|
if (!reason.trim()) {
|
|
return deny(400, "请填写回应理由")
|
|
}
|
|
|
|
set((state) => ({
|
|
disputes: state.disputes.map((item) => {
|
|
if (item.id !== disputeId) return item
|
|
|
|
return {
|
|
...item,
|
|
respondentReason: reason.trim(),
|
|
respondentEvidence: evidence,
|
|
timeline: [
|
|
...item.timeline,
|
|
{
|
|
id: generateId("timeline"),
|
|
type: "response",
|
|
content: "对方已提交回应材料",
|
|
createdAt: new Date().toISOString(),
|
|
},
|
|
],
|
|
}
|
|
}),
|
|
}))
|
|
|
|
notifyDispute(dispute.orderId, "争议收到回应", "对方已提交争议回应材料")
|
|
|
|
return allow()
|
|
},
|
|
submitAppeal: (disputeId, actorId, reason) => {
|
|
const dispute = get().disputes.find((item) => item.id === disputeId)
|
|
if (!dispute) {
|
|
return deny(404, "争议不存在")
|
|
}
|
|
|
|
const actor = resolveParticipantActor(dispute.orderId, actorId)
|
|
if (!actor) {
|
|
return deny(403, "仅订单参与方可提交申诉")
|
|
}
|
|
|
|
if (dispute.status !== "resolved") {
|
|
return deny(400, "当前状态不可申诉")
|
|
}
|
|
|
|
if (dispute.appealedAt) {
|
|
return deny(400, "该争议已申诉过")
|
|
}
|
|
|
|
if (!reason.trim()) {
|
|
return deny(400, "请填写申诉理由")
|
|
}
|
|
|
|
set((state) => ({
|
|
disputes: state.disputes.map((item) => {
|
|
if (item.id !== disputeId) return item
|
|
|
|
return {
|
|
...item,
|
|
status: "appealed",
|
|
appealReason: reason.trim(),
|
|
appealedAt: new Date().toISOString(),
|
|
timeline: [
|
|
...item.timeline,
|
|
{
|
|
id: generateId("timeline"),
|
|
type: "appealed",
|
|
content: "已提交申诉,平台将复核",
|
|
createdAt: new Date().toISOString(),
|
|
},
|
|
],
|
|
}
|
|
}),
|
|
}))
|
|
|
|
notifyDispute(dispute.orderId, "争议已申诉", "申诉已提交,平台将继续复核")
|
|
|
|
return allow()
|
|
},
|
|
}
|
|
})
|