import { DISPUTE_TO_RESOLVED_MS, DISPUTE_TO_REVIEWING_MS } from "@/lib/config/demo-timers" 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" 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 initiatorId: string initiatorName: string reason: string evidence: string[] } interface DisputeMutationResult { decision: PolicyDecision dispute?: DisputeRecord } interface DisputeState { disputes: DisputeRecord[] getDisputeByOrderId: (orderId: string) => DisputeRecord | undefined submitDispute: (input: SubmitDisputeInput) => DisputeMutationResult submitResponse: ( disputeId: string, actorId: string, reason: string, evidence: string[], ) => PolicyDecision submitAppeal: (disputeId: string, actorId: string, reason: string) => PolicyDecision } const progressTimers = new Map[]>() 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 { 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 asRecord(dispute: Dispute): DisputeRecord { const timeline: DisputeTimelineItem[] = [ { id: generateId("timeline"), type: "created", content: `${dispute.initiatorName} 提交争议`, createdAt: dispute.createdAt, }, ] if (dispute.status === "reviewing") { timeline.push({ id: generateId("timeline"), type: "reviewing", content: "平台已受理并进入审核", createdAt: dispute.createdAt, }) } if (dispute.status === "resolved") { timeline.push({ id: generateId("timeline"), type: "resolved", content: "平台已给出仲裁结果", createdAt: dispute.createdAt, }) } return { ...dispute, respondentEvidence: [], timeline, } } 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((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 { disputes: mockDisputes.map(asRecord), 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("NOT_FOUND", "订单不存在") } } if (order.status !== "in_progress" && order.status !== "pending_close") { return { decision: deny("INVALID_STATUS", "当前阶段不可发起争议") } } if (!input.reason.trim()) { return { decision: deny("VALIDATION_FAILED", "请填写争议原因") } } const actor = resolveParticipantActor(input.orderId, input.initiatorId) if (!actor) { return { decision: deny("NOT_PARTICIPANT", "仅订单参与方可发起争议") } } const markResult = useOrderStore.getState().markDisputed(input.orderId, actor) if (!markResult.decision.ok) { return { decision: markResult.decision } } const createdAt = new Date().toISOString() const dispute: DisputeRecord = { id: generateId("disp"), orderId: input.orderId, initiatorId: input.initiatorId, initiatorName: input.initiatorName, reason: input.reason.trim(), evidence: input.evidence, status: "open", createdAt, respondentEvidence: [], timeline: [ { id: generateId("timeline"), type: "created", content: `${input.initiatorName} 提交争议`, createdAt, }, ], } set((state) => ({ disputes: [dispute, ...state.disputes] })) scheduleProgress(dispute.id) return { decision: allow(), dispute } }, submitResponse: (disputeId, actorId, reason, evidence) => { const dispute = get().disputes.find((item) => item.id === disputeId) if (!dispute) { return deny("NOT_FOUND", "争议不存在") } const actor = resolveParticipantActor(dispute.orderId, actorId) if (!actor) { return deny("NOT_PARTICIPANT", "仅订单参与方可提交回应") } if (actorId === dispute.initiatorId) { return deny("ROLE_FORBIDDEN", "争议发起方不可提交回应") } if (dispute.respondentReason) { return deny("ALREADY_DONE", "回应已提交") } if (dispute.status !== "open" && dispute.status !== "reviewing") { return deny("INVALID_STATUS", "当前状态不可提交回应") } if (!reason.trim()) { return deny("VALIDATION_FAILED", "请填写回应理由") } 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("NOT_FOUND", "争议不存在") } const actor = resolveParticipantActor(dispute.orderId, actorId) if (!actor) { return deny("NOT_PARTICIPANT", "仅订单参与方可提交申诉") } if (dispute.status !== "resolved") { return deny("INVALID_STATUS", "当前状态不可申诉") } if (dispute.appealedAt) { return deny("ALREADY_DONE", "该争议已申诉过") } if (!reason.trim()) { return deny("VALIDATION_FAILED", "请填写申诉理由") } 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, "争议已申诉", "申诉已提交,平台将继续复核") scheduleProgress(disputeId) return allow() }, } })