feat(disputes): migrate disputes and reviews to backend API
This commit is contained in:
+173
-34
@@ -1,50 +1,189 @@
|
||||
import { deny } from "@/lib/decision"
|
||||
import { useAuthStore } from "@/store/auth"
|
||||
import { useDisputeStore } from "@/store/disputes"
|
||||
import { allow, deny } from "@/lib/decision"
|
||||
import { isApiError, toApiError, type ApiDecision } from "@/lib/errors"
|
||||
import type { Dispute } from "@/lib/types"
|
||||
|
||||
export function listDisputes() {
|
||||
return useDisputeStore.getState().disputes
|
||||
import { httpJson } from "./http"
|
||||
|
||||
export type DisputeTimelineItem = {
|
||||
id: string
|
||||
content: string
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
export function getDisputeByOrderId(orderId: string) {
|
||||
return useDisputeStore.getState().disputes.find((dispute) => dispute.orderId === orderId)
|
||||
export type DisputeRecord = Dispute & {
|
||||
respondentReason?: string
|
||||
respondentEvidence: string[]
|
||||
appealReason?: string
|
||||
appealedAt?: string
|
||||
timeline: DisputeTimelineItem[]
|
||||
}
|
||||
|
||||
export function submitDispute(input: { orderId: string; reason: string; evidence: string[] }) {
|
||||
const user = useAuthStore.getState().user
|
||||
if (!user?.id || !user.nickname) {
|
||||
return { decision: deny(401, "请先登录") }
|
||||
export type ListDisputesOptions = {
|
||||
offset?: number
|
||||
limit?: number
|
||||
}
|
||||
|
||||
type Paginated<T> = {
|
||||
items: T[]
|
||||
meta?: {
|
||||
total: number
|
||||
offset: number
|
||||
limit: number
|
||||
}
|
||||
|
||||
return useDisputeStore.getState().submitDispute({
|
||||
orderId: input.orderId,
|
||||
initiatorId: user.id,
|
||||
initiatorName: user.nickname,
|
||||
reason: input.reason,
|
||||
evidence: input.evidence,
|
||||
})
|
||||
}
|
||||
|
||||
export function submitDisputeResponse(input: {
|
||||
function withOffsetLimit(path: string, options?: ListDisputesOptions): string {
|
||||
const offset = options?.offset ?? 0
|
||||
const limit = options?.limit ?? 1000
|
||||
|
||||
const searchParams = new URLSearchParams({
|
||||
offset: String(offset),
|
||||
limit: String(limit),
|
||||
})
|
||||
|
||||
return `${path}?${searchParams.toString()}`
|
||||
}
|
||||
|
||||
function unwrapItems<T>(value: unknown): T[] {
|
||||
if (Array.isArray(value)) return value as T[]
|
||||
if (typeof value !== "object" || value === null) throw new Error("Invalid response")
|
||||
if ("items" in value) {
|
||||
const envelope = value as { items?: unknown }
|
||||
if (Array.isArray(envelope.items)) return envelope.items as T[]
|
||||
}
|
||||
throw new Error("Invalid response")
|
||||
}
|
||||
|
||||
function unwrapDispute(value: unknown): unknown {
|
||||
if (typeof value !== "object" || value === null) return value
|
||||
if ("dispute" in value) {
|
||||
const envelope = value as { dispute?: unknown }
|
||||
return envelope.dispute
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
function deriveMinimalTimeline(dispute: {
|
||||
id: string
|
||||
status: string
|
||||
createdAt: string
|
||||
timeline?: DisputeTimelineItem[]
|
||||
}): DisputeTimelineItem[] {
|
||||
const existing = dispute.timeline
|
||||
if (existing?.length) return existing
|
||||
|
||||
const steps = [
|
||||
{ status: "open", content: "争议已提交" },
|
||||
{ status: "reviewing", content: "平台审核中" },
|
||||
{ status: "resolved", content: "争议已解决" },
|
||||
{ status: "appealed", content: "已发起申诉" },
|
||||
]
|
||||
|
||||
const currentIndex = steps.findIndex((step) => step.status === dispute.status)
|
||||
const lastIndex = currentIndex >= 0 ? currentIndex : 0
|
||||
return steps.slice(0, lastIndex + 1).map((step) => ({
|
||||
id: `${dispute.id}-${step.status}`,
|
||||
content: step.content,
|
||||
createdAt: dispute.createdAt,
|
||||
}))
|
||||
}
|
||||
|
||||
function normalizeDisputeRecord(value: unknown): DisputeRecord {
|
||||
const dispute = unwrapDispute(value) as DisputeRecord
|
||||
const respondentEvidence = Array.isArray(dispute.respondentEvidence)
|
||||
? dispute.respondentEvidence
|
||||
: []
|
||||
const evidence = Array.isArray(dispute.evidence) ? dispute.evidence : []
|
||||
const timeline = deriveMinimalTimeline({
|
||||
id: dispute.id,
|
||||
status: dispute.status,
|
||||
createdAt: dispute.createdAt,
|
||||
timeline: dispute.timeline,
|
||||
})
|
||||
|
||||
return {
|
||||
...dispute,
|
||||
evidence,
|
||||
respondentEvidence,
|
||||
timeline,
|
||||
}
|
||||
}
|
||||
|
||||
function denyFromError(error: unknown): ApiDecision {
|
||||
if (error instanceof Error && error.message === "UNAUTHORIZED") {
|
||||
return deny(401, "请先登录")
|
||||
}
|
||||
const apiError = toApiError(error)
|
||||
return deny(apiError.code, apiError.msg)
|
||||
}
|
||||
|
||||
export async function listDisputes(options?: ListDisputesOptions): Promise<DisputeRecord[]> {
|
||||
const res = await httpJson<Paginated<DisputeRecord> | DisputeRecord[]>(
|
||||
withOffsetLimit("/api/v1/disputes", options),
|
||||
{ cache: "no-store" },
|
||||
)
|
||||
return unwrapItems<DisputeRecord>(res).map((item) => normalizeDisputeRecord(item))
|
||||
}
|
||||
|
||||
export async function getDisputeByOrderId(orderId: string): Promise<DisputeRecord | undefined> {
|
||||
try {
|
||||
const res = await httpJson<unknown>(`/api/v1/orders/${encodeURIComponent(orderId)}/dispute`, {
|
||||
cache: "no-store",
|
||||
})
|
||||
return normalizeDisputeRecord(res)
|
||||
} catch (error) {
|
||||
const apiError = isApiError(error) ? error : toApiError(error)
|
||||
if (apiError.code === 404) return undefined
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
export async function submitDispute(input: {
|
||||
orderId: string
|
||||
reason: string
|
||||
evidence: string[]
|
||||
}): Promise<{ decision: ApiDecision }> {
|
||||
try {
|
||||
await httpJson<unknown>(`/api/v1/orders/${encodeURIComponent(input.orderId)}/dispute`, {
|
||||
method: "POST",
|
||||
cache: "no-store",
|
||||
json: { reason: input.reason, evidence: input.evidence },
|
||||
})
|
||||
return { decision: allow() }
|
||||
} catch (error) {
|
||||
return { decision: denyFromError(error) }
|
||||
}
|
||||
}
|
||||
|
||||
export async function submitDisputeResponse(input: {
|
||||
disputeId: string
|
||||
reason: string
|
||||
evidence: string[]
|
||||
}) {
|
||||
const userId = useAuthStore.getState().user?.id
|
||||
if (!userId) {
|
||||
return deny(401, "请先登录")
|
||||
}): Promise<ApiDecision> {
|
||||
try {
|
||||
await httpJson<unknown>(`/api/v1/disputes/${encodeURIComponent(input.disputeId)}/response`, {
|
||||
method: "POST",
|
||||
cache: "no-store",
|
||||
json: { reason: input.reason, evidence: input.evidence },
|
||||
})
|
||||
return allow()
|
||||
} catch (error) {
|
||||
return denyFromError(error)
|
||||
}
|
||||
|
||||
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(401, "请先登录")
|
||||
export async function submitDisputeAppeal(input: {
|
||||
disputeId: string
|
||||
reason: string
|
||||
}): Promise<ApiDecision> {
|
||||
try {
|
||||
await httpJson<unknown>(`/api/v1/disputes/${encodeURIComponent(input.disputeId)}/appeal`, {
|
||||
method: "POST",
|
||||
cache: "no-store",
|
||||
json: { reason: input.reason },
|
||||
})
|
||||
return allow()
|
||||
} catch (error) {
|
||||
return denyFromError(error)
|
||||
}
|
||||
|
||||
return useDisputeStore.getState().submitAppeal(input.disputeId, userId, input.reason)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user