refactor(order): rewrite store around state machine transitions
This commit is contained in:
@@ -12,10 +12,10 @@ import { Label } from "@/components/ui/label"
|
||||
import { Separator } from "@/components/ui/separator"
|
||||
import { Textarea } from "@/components/ui/textarea"
|
||||
import { getPlayerById, getServiceById } from "@/lib/api"
|
||||
import type { Actor } from "@/lib/policy/actor"
|
||||
import { notifySuccess } from "@/lib/toast"
|
||||
import { useRequireAuth } from "@/lib/use-require-auth"
|
||||
import { useAuthStore } from "@/store/auth"
|
||||
import { useChatStore } from "@/store/chat"
|
||||
import { useOrderStore } from "@/store/orders"
|
||||
import { useWalletStore } from "@/store/wallet"
|
||||
|
||||
@@ -23,10 +23,8 @@ export default function NewOrderPage() {
|
||||
const router = useRouter()
|
||||
const searchParams = useSearchParams()
|
||||
const { requireAuth } = useRequireAuth()
|
||||
const createOrder = useOrderStore((state) => state.createOrder)
|
||||
const ensureOrderSession = useChatStore((state) => state.ensureOrderSession)
|
||||
const createPaidOrder = useOrderStore((state) => state.createPaidOrder)
|
||||
const balance = useWalletStore((state) => state.balance)
|
||||
const deductBalance = useWalletStore((state) => state.deductBalance)
|
||||
const serviceId = searchParams.get("serviceId")
|
||||
|
||||
const service = serviceId ? getServiceById(serviceId) : undefined
|
||||
@@ -186,12 +184,17 @@ export default function NewOrderPage() {
|
||||
size="lg"
|
||||
disabled={balance < totalPrice}
|
||||
onClick={() =>
|
||||
requireAuth(async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 500))
|
||||
requireAuth(() => {
|
||||
const authUser = useAuthStore.getState().user
|
||||
if (!authUser) return
|
||||
|
||||
const order = createOrder({
|
||||
const actor: Actor = {
|
||||
userId: authUser.id,
|
||||
role: "consumer",
|
||||
}
|
||||
|
||||
const result = createPaidOrder(
|
||||
{
|
||||
consumerId: authUser.id,
|
||||
consumerName: authUser.nickname,
|
||||
playerId: player.id,
|
||||
@@ -201,19 +204,18 @@ export default function NewOrderPage() {
|
||||
service,
|
||||
totalPrice,
|
||||
note,
|
||||
status: "pending_accept",
|
||||
})
|
||||
|
||||
const paid = deductBalance(order.id, totalPrice)
|
||||
if (!paid) {
|
||||
},
|
||||
actor,
|
||||
)
|
||||
if (!result.decision.ok || !result.order) {
|
||||
return
|
||||
}
|
||||
const nextOrder = result.order
|
||||
|
||||
ensureOrderSession(order)
|
||||
setSubmitted(true)
|
||||
notifySuccess("下单成功")
|
||||
setTimeout(() => {
|
||||
router.push(`/order/${order.id}`)
|
||||
router.push(`/order/${nextOrder.id}`)
|
||||
}, 800)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -10,10 +10,12 @@ import {
|
||||
XCircle,
|
||||
} from "lucide-react"
|
||||
import Link from "next/link"
|
||||
import { useEffect } from "react"
|
||||
import { useCallback, useEffect } from "react"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { notifySuccess } from "@/lib/toast"
|
||||
import type { Actor } from "@/lib/policy/actor"
|
||||
import { notifyInfo, notifySuccess } from "@/lib/toast"
|
||||
import type { OrderStatus } from "@/lib/types"
|
||||
import { useAuthStore } from "@/store/auth"
|
||||
import { useChatStore } from "@/store/chat"
|
||||
import { useOrderStore } from "@/store/orders"
|
||||
import { useShopStore } from "@/store/shops"
|
||||
@@ -35,10 +37,21 @@ export default function OrderActions({
|
||||
chatSessionId,
|
||||
serviceId,
|
||||
}: OrderActionsProps) {
|
||||
const currentRole = useAuthStore((state) => state.currentRole)
|
||||
const currentUserId = useAuthStore((state) => state.user?.id)
|
||||
const payOrder = useOrderStore((state) => state.payOrder)
|
||||
const acceptOrder = useOrderStore((state) => state.acceptOrder)
|
||||
const requestClose = useOrderStore((state) => state.requestClose)
|
||||
const confirmClose = useOrderStore((state) => state.confirmClose)
|
||||
const cancelPreAccept = useOrderStore((state) => state.cancelPreAccept)
|
||||
const order = useOrderStore((state) => state.orders.find((item) => item.id === orderId))
|
||||
const updateOrderStatus = useOrderStore((state) => state.updateOrderStatus)
|
||||
const sessions = useChatStore((state) => state.sessions)
|
||||
const ensureOrderSession = useChatStore((state) => state.ensureOrderSession)
|
||||
const actorShopId = useShopStore((state) => {
|
||||
if (!currentUserId || currentRole !== "owner") return undefined
|
||||
const owned = state.shops.find((shop) => shop.owner.id === currentUserId)
|
||||
return owned?.id
|
||||
})
|
||||
const dispatchMode = useShopStore((state) => {
|
||||
if (!order?.shopId) return "manual"
|
||||
const shop = state.shops.find((item) => item.id === order.shopId)
|
||||
@@ -49,6 +62,25 @@ export default function OrderActions({
|
||||
sessions.find((session) => session.type === "order" && session.orderId === orderId)?.id
|
||||
|
||||
const status = order?.status ?? initialStatus
|
||||
const actor: Actor | undefined = currentUserId
|
||||
? {
|
||||
userId: currentUserId,
|
||||
role: currentRole,
|
||||
shopId: actorShopId,
|
||||
}
|
||||
: undefined
|
||||
|
||||
const handleDecision = useCallback(
|
||||
(okMessage: string, result: { decision: { ok: boolean; message?: string } }) => {
|
||||
if (result.decision.ok) {
|
||||
showFeedback(okMessage)
|
||||
return
|
||||
}
|
||||
|
||||
notifyInfo(result.decision.message ?? "当前操作不允许")
|
||||
},
|
||||
[],
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (chatSessionId || !order || resolvedChatSessionId) return
|
||||
@@ -62,12 +94,17 @@ export default function OrderActions({
|
||||
if (dispatchMode !== "auto") return
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
updateOrderStatus(orderId, "in_progress")
|
||||
showFeedback("系统已自动派单")
|
||||
const systemActor: Actor = {
|
||||
userId: order.playerId,
|
||||
role: "player",
|
||||
shopId: order.shopId,
|
||||
}
|
||||
const result = acceptOrder(orderId, systemActor)
|
||||
handleDecision("系统已自动派单", result)
|
||||
}, 3000)
|
||||
|
||||
return () => clearTimeout(timer)
|
||||
}, [dispatchMode, order, orderId, updateOrderStatus])
|
||||
}, [acceptOrder, dispatchMode, handleDecision, order, orderId])
|
||||
|
||||
return (
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
@@ -76,8 +113,13 @@ export default function OrderActions({
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
updateOrderStatus(orderId, "cancelled")
|
||||
showFeedback("订单已取消")
|
||||
if (!actor) {
|
||||
notifyInfo("请先登录")
|
||||
return
|
||||
}
|
||||
|
||||
const result = cancelPreAccept(orderId, actor)
|
||||
handleDecision("订单已取消", result)
|
||||
}}
|
||||
>
|
||||
<XCircle className="mr-1 h-4 w-4" />
|
||||
@@ -85,7 +127,13 @@ export default function OrderActions({
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
updateOrderStatus(orderId, "pending_accept")
|
||||
if (!actor) {
|
||||
notifyInfo("请先登录")
|
||||
return
|
||||
}
|
||||
|
||||
const result = payOrder(orderId, actor)
|
||||
handleDecision("订单支付成功", result)
|
||||
}}
|
||||
>
|
||||
<CheckCircle2 className="mr-1 h-4 w-4" />
|
||||
@@ -99,8 +147,13 @@ export default function OrderActions({
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
updateOrderStatus(orderId, "cancelled")
|
||||
showFeedback("订单已取消")
|
||||
if (!actor) {
|
||||
notifyInfo("请先登录")
|
||||
return
|
||||
}
|
||||
|
||||
const result = cancelPreAccept(orderId, actor)
|
||||
handleDecision("订单已取消", result)
|
||||
}}
|
||||
>
|
||||
<XCircle className="mr-1 h-4 w-4" />
|
||||
@@ -114,8 +167,13 @@ export default function OrderActions({
|
||||
) : (
|
||||
<Button
|
||||
onClick={() => {
|
||||
updateOrderStatus(orderId, "in_progress")
|
||||
showFeedback("已接单")
|
||||
if (!actor) {
|
||||
notifyInfo("请先登录")
|
||||
return
|
||||
}
|
||||
|
||||
const result = acceptOrder(orderId, actor)
|
||||
handleDecision("已接单", result)
|
||||
}}
|
||||
>
|
||||
<CheckCircle2 className="mr-1 h-4 w-4" />
|
||||
@@ -138,8 +196,13 @@ export default function OrderActions({
|
||||
<>
|
||||
<Button
|
||||
onClick={() => {
|
||||
updateOrderStatus(orderId, "pending_close")
|
||||
showFeedback("已发起结单")
|
||||
if (!actor) {
|
||||
notifyInfo("请先登录")
|
||||
return
|
||||
}
|
||||
|
||||
const result = requestClose(orderId, actor)
|
||||
handleDecision("已发起结单", result)
|
||||
}}
|
||||
>
|
||||
发起结单
|
||||
@@ -157,7 +220,13 @@ export default function OrderActions({
|
||||
<>
|
||||
<Button
|
||||
onClick={() => {
|
||||
updateOrderStatus(orderId, "pending_review")
|
||||
if (!actor) {
|
||||
notifyInfo("请先登录")
|
||||
return
|
||||
}
|
||||
|
||||
const result = confirmClose(orderId, actor)
|
||||
handleDecision("已确认结单", result)
|
||||
}}
|
||||
>
|
||||
确认结单
|
||||
|
||||
+244
-41
@@ -1,8 +1,13 @@
|
||||
import { create } from "zustand"
|
||||
import { ORDER_ACCEPT_TIMEOUT_MS, ORDER_CLOSE_TIMEOUT_MS } from "@/lib/config/demo-timers"
|
||||
import { evaluateOrderTransition, type OrderAction } from "@/lib/domain/order-machine"
|
||||
import { generateId } from "@/lib/id"
|
||||
import { allow, deny } from "@/lib/policy/assert"
|
||||
import type { Actor } from "@/lib/policy/actor"
|
||||
import type { PolicyDecision } from "@/lib/policy/decision"
|
||||
import { mockOrders } from "@/lib/mock"
|
||||
import type { Order, OrderStatus, PlayerService } from "@/lib/types"
|
||||
import { useChatStore } from "@/store/chat"
|
||||
import { useWalletStore } from "@/store/wallet"
|
||||
|
||||
interface CreateOrderInput {
|
||||
@@ -18,9 +23,27 @@ interface CreateOrderInput {
|
||||
status?: OrderStatus
|
||||
}
|
||||
|
||||
interface CreatePaidOrderInput extends CreateOrderInput {
|
||||
status?: never
|
||||
}
|
||||
|
||||
interface OrderMutationResult {
|
||||
decision: PolicyDecision
|
||||
order?: Order
|
||||
}
|
||||
|
||||
interface OrderState {
|
||||
orders: Order[]
|
||||
createOrder: (input: CreateOrderInput) => Order
|
||||
createPaidOrder: (input: CreatePaidOrderInput, actor: Actor) => OrderMutationResult
|
||||
payOrder: (orderId: string, actor: Actor) => OrderMutationResult
|
||||
acceptOrder: (orderId: string, actor: Actor) => OrderMutationResult
|
||||
requestClose: (orderId: string, actor: Actor) => OrderMutationResult
|
||||
confirmClose: (orderId: string, actor: Actor) => OrderMutationResult
|
||||
cancelPreAccept: (orderId: string, actor: Actor) => OrderMutationResult
|
||||
markDisputed: (orderId: string, actor: Actor) => OrderMutationResult
|
||||
autoTimeoutPendingAccept: (orderId: string) => OrderMutationResult
|
||||
autoTimeoutPendingClose: (orderId: string) => OrderMutationResult
|
||||
updateOrderStatus: (orderId: string, status: OrderStatus) => void
|
||||
}
|
||||
|
||||
@@ -33,6 +56,110 @@ function clearOrderTimeout(orderId: string) {
|
||||
orderTimeouts.delete(orderId)
|
||||
}
|
||||
|
||||
function isParticipant(order: Order, userId: string) {
|
||||
return order.consumerId === userId || order.playerId === userId
|
||||
}
|
||||
|
||||
function isOrderOwnerActor(order: Order, actor: Actor) {
|
||||
return actor.role === "owner" && Boolean(order.shopId) && actor.shopId === order.shopId
|
||||
}
|
||||
|
||||
function validateActorForAction(order: Order, action: OrderAction, actor?: Actor): PolicyDecision {
|
||||
if (action.startsWith("AUTO_TIMEOUT_")) {
|
||||
return allow()
|
||||
}
|
||||
|
||||
if (!actor?.userId) {
|
||||
return deny("AUTH_REQUIRED", "请先登录")
|
||||
}
|
||||
|
||||
if (order.status === "disputed" && action !== "RESOLVE_DISPUTE") {
|
||||
return deny("DISPUTE_LOCKED", "争议处理中,暂不可执行此操作")
|
||||
}
|
||||
|
||||
if (action === "PAY" || action === "CANCEL_PRE_ACCEPT" || action === "CONFIRM_CLOSE") {
|
||||
return order.consumerId === actor.userId
|
||||
? allow()
|
||||
: deny("NOT_PARTICIPANT", "仅下单客户可执行该操作")
|
||||
}
|
||||
|
||||
if (action === "ACCEPT") {
|
||||
return order.playerId === actor.userId || isOrderOwnerActor(order, actor)
|
||||
? allow()
|
||||
: deny("NOT_PARTICIPANT", "仅该订单打手或所属店主可执行接单")
|
||||
}
|
||||
|
||||
if (action === "REQUEST_CLOSE" || action === "OPEN_DISPUTE" || action === "SUBMIT_REVIEW") {
|
||||
return isParticipant(order, actor.userId)
|
||||
? allow()
|
||||
: deny("NOT_PARTICIPANT", "仅订单参与方可执行该操作")
|
||||
}
|
||||
|
||||
if (action === "RESOLVE_DISPUTE") {
|
||||
return isOrderOwnerActor(order, actor)
|
||||
? allow()
|
||||
: deny("ROLE_FORBIDDEN", "仅订单所属店主可执行该操作")
|
||||
}
|
||||
|
||||
return allow()
|
||||
}
|
||||
|
||||
function applyStatus(order: Order, status: OrderStatus): Order {
|
||||
const now = new Date().toISOString()
|
||||
|
||||
switch (status) {
|
||||
case "in_progress":
|
||||
return {
|
||||
...order,
|
||||
status,
|
||||
acceptedAt: order.acceptedAt ?? now,
|
||||
}
|
||||
case "pending_close":
|
||||
return {
|
||||
...order,
|
||||
status,
|
||||
closedAt: order.closedAt ?? now,
|
||||
}
|
||||
case "pending_review":
|
||||
return {
|
||||
...order,
|
||||
status,
|
||||
closedAt: order.closedAt ?? now,
|
||||
}
|
||||
case "completed":
|
||||
return {
|
||||
...order,
|
||||
status,
|
||||
closedAt: order.closedAt ?? now,
|
||||
completedAt: order.completedAt ?? now,
|
||||
}
|
||||
default:
|
||||
return {
|
||||
...order,
|
||||
status,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function syncChatSession(order: Order, previousStatus: OrderStatus) {
|
||||
const chatStore = useChatStore.getState()
|
||||
if (order.status === "pending_payment") return
|
||||
|
||||
if (order.status === "cancelled") {
|
||||
const sessionExists = chatStore.sessions.some(
|
||||
(session) => session.type === "order" && session.orderId === order.id,
|
||||
)
|
||||
if (sessionExists) {
|
||||
chatStore.ensureOrderSession(order)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (previousStatus !== order.status) {
|
||||
chatStore.ensureOrderSession(order)
|
||||
}
|
||||
}
|
||||
|
||||
function scheduleOrderTimeout(orderId: string, status: OrderStatus) {
|
||||
clearOrderTimeout(orderId)
|
||||
|
||||
@@ -51,11 +178,11 @@ function scheduleOrderTimeout(orderId: string, status: OrderStatus) {
|
||||
}
|
||||
|
||||
if (status === "pending_accept") {
|
||||
state.updateOrderStatus(orderId, "cancelled")
|
||||
state.autoTimeoutPendingAccept(orderId)
|
||||
}
|
||||
|
||||
if (status === "pending_close") {
|
||||
state.updateOrderStatus(orderId, "pending_review")
|
||||
state.autoTimeoutPendingClose(orderId)
|
||||
}
|
||||
|
||||
orderTimeouts.delete(orderId)
|
||||
@@ -64,7 +191,58 @@ function scheduleOrderTimeout(orderId: string, status: OrderStatus) {
|
||||
orderTimeouts.set(orderId, timer)
|
||||
}
|
||||
|
||||
export const useOrderStore = create<OrderState>((set) => ({
|
||||
export const useOrderStore = create<OrderState>((set, get) => {
|
||||
const applyTransition = (
|
||||
orderId: string,
|
||||
action: OrderAction,
|
||||
actor?: Actor,
|
||||
): OrderMutationResult => {
|
||||
const order = get().orders.find((item) => item.id === orderId)
|
||||
if (!order) return { decision: deny("NOT_FOUND", "订单不存在") }
|
||||
|
||||
const actorDecision = validateActorForAction(order, action, actor)
|
||||
if (!actorDecision.ok) return { decision: actorDecision }
|
||||
|
||||
const transition = evaluateOrderTransition({ actor, order, action })
|
||||
if (!transition.decision.ok || !transition.nextStatus) {
|
||||
return { decision: transition.decision }
|
||||
}
|
||||
const nextStatus = transition.nextStatus
|
||||
|
||||
let previousOrder: Order | undefined
|
||||
let updatedOrder: Order | undefined
|
||||
|
||||
set((state) => ({
|
||||
orders: state.orders.map((item) => {
|
||||
if (item.id !== orderId) return item
|
||||
previousOrder = item
|
||||
updatedOrder = applyStatus(item, nextStatus)
|
||||
return updatedOrder
|
||||
}),
|
||||
}))
|
||||
|
||||
if (!updatedOrder || !previousOrder) {
|
||||
return { decision: deny("NOT_FOUND", "订单不存在") }
|
||||
}
|
||||
|
||||
if (previousOrder.status !== "completed" && updatedOrder.status === "completed") {
|
||||
useWalletStore.getState().addIncome(updatedOrder.id, updatedOrder.totalPrice)
|
||||
}
|
||||
|
||||
const shouldRefund =
|
||||
previousOrder.status === "pending_accept" &&
|
||||
updatedOrder.status === "cancelled" &&
|
||||
(action === "CANCEL_PRE_ACCEPT" || action === "AUTO_TIMEOUT_PENDING_ACCEPT")
|
||||
|
||||
if (shouldRefund) {
|
||||
useWalletStore.getState().refundPayment(updatedOrder.id, updatedOrder.totalPrice)
|
||||
}
|
||||
|
||||
syncChatSession(updatedOrder, previousOrder.status)
|
||||
return { decision: allow(), order: updatedOrder }
|
||||
}
|
||||
|
||||
return {
|
||||
orders: mockOrders,
|
||||
createOrder: (input) => {
|
||||
const order: Order = {
|
||||
@@ -90,51 +268,76 @@ export const useOrderStore = create<OrderState>((set) => ({
|
||||
|
||||
return order
|
||||
},
|
||||
updateOrderStatus: (orderId, status) =>
|
||||
createPaidOrder: (input, actor) => {
|
||||
if (actor.role !== "consumer" || actor.userId !== input.consumerId) {
|
||||
return { decision: deny("ROLE_FORBIDDEN", "仅客户可下单支付") }
|
||||
}
|
||||
|
||||
const orderId = generateId("ord")
|
||||
const paid = useWalletStore.getState().deductBalance(orderId, input.totalPrice)
|
||||
if (!paid) {
|
||||
return { decision: deny("PAYMENT_FAILED", "余额不足或订单已支付") }
|
||||
}
|
||||
|
||||
const order: Order = {
|
||||
id: orderId,
|
||||
consumerId: input.consumerId,
|
||||
consumerName: input.consumerName,
|
||||
playerId: input.playerId,
|
||||
playerName: input.playerName,
|
||||
shopId: input.shopId,
|
||||
shopName: input.shopName,
|
||||
service: input.service,
|
||||
status: "pending_accept",
|
||||
totalPrice: input.totalPrice,
|
||||
note: input.note?.trim() ? input.note.trim() : undefined,
|
||||
createdAt: new Date().toISOString(),
|
||||
}
|
||||
|
||||
set((state) => ({
|
||||
orders: state.orders.map((order) => {
|
||||
orders: [order, ...state.orders],
|
||||
}))
|
||||
|
||||
useChatStore.getState().ensureOrderSession(order)
|
||||
return { decision: allow(), order }
|
||||
},
|
||||
payOrder: (orderId, actor) => applyTransition(orderId, "PAY", actor),
|
||||
acceptOrder: (orderId, actor) => applyTransition(orderId, "ACCEPT", actor),
|
||||
requestClose: (orderId, actor) => applyTransition(orderId, "REQUEST_CLOSE", actor),
|
||||
confirmClose: (orderId, actor) => applyTransition(orderId, "CONFIRM_CLOSE", actor),
|
||||
cancelPreAccept: (orderId, actor) => applyTransition(orderId, "CANCEL_PRE_ACCEPT", actor),
|
||||
markDisputed: (orderId, actor) => applyTransition(orderId, "OPEN_DISPUTE", actor),
|
||||
autoTimeoutPendingAccept: (orderId) => applyTransition(orderId, "AUTO_TIMEOUT_PENDING_ACCEPT"),
|
||||
autoTimeoutPendingClose: (orderId) => applyTransition(orderId, "AUTO_TIMEOUT_PENDING_CLOSE"),
|
||||
updateOrderStatus: (orderId, status) => {
|
||||
set((state) => {
|
||||
let previousOrder: Order | undefined
|
||||
let updatedOrder: Order | undefined
|
||||
|
||||
const orders = state.orders.map((order) => {
|
||||
if (order.id !== orderId) return order
|
||||
previousOrder = order
|
||||
updatedOrder = applyStatus(order, status)
|
||||
return updatedOrder
|
||||
})
|
||||
|
||||
const now = new Date().toISOString()
|
||||
if (previousOrder && updatedOrder) {
|
||||
if (previousOrder.status !== "completed" && updatedOrder.status === "completed") {
|
||||
useWalletStore.getState().addIncome(updatedOrder.id, updatedOrder.totalPrice)
|
||||
}
|
||||
|
||||
switch (status) {
|
||||
case "in_progress":
|
||||
return {
|
||||
...order,
|
||||
status,
|
||||
acceptedAt: order.acceptedAt ?? now,
|
||||
if (previousOrder.status === "pending_accept" && updatedOrder.status === "cancelled") {
|
||||
useWalletStore.getState().refundPayment(updatedOrder.id, updatedOrder.totalPrice)
|
||||
}
|
||||
case "pending_close":
|
||||
return {
|
||||
...order,
|
||||
status,
|
||||
closedAt: order.closedAt ?? now,
|
||||
|
||||
syncChatSession(updatedOrder, previousOrder.status)
|
||||
}
|
||||
case "pending_review":
|
||||
return {
|
||||
...order,
|
||||
status,
|
||||
closedAt: order.closedAt ?? now,
|
||||
|
||||
return { orders }
|
||||
})
|
||||
},
|
||||
}
|
||||
case "completed":
|
||||
if (order.status !== "completed") {
|
||||
useWalletStore.getState().addIncome(order.id, order.totalPrice)
|
||||
}
|
||||
return {
|
||||
...order,
|
||||
status,
|
||||
closedAt: order.closedAt ?? now,
|
||||
completedAt: order.completedAt ?? now,
|
||||
}
|
||||
default:
|
||||
return {
|
||||
...order,
|
||||
status,
|
||||
}
|
||||
}
|
||||
}),
|
||||
})),
|
||||
}))
|
||||
})
|
||||
|
||||
useOrderStore.subscribe((state, prevState) => {
|
||||
state.orders.forEach((order) => {
|
||||
|
||||
@@ -9,6 +9,7 @@ interface WalletState {
|
||||
topUp: (amount: number) => void
|
||||
withdraw: (amount: number) => void
|
||||
deductBalance: (orderId: string, amount: number) => boolean
|
||||
refundPayment: (orderId: string, amount: number) => boolean
|
||||
addIncome: (orderId: string, amount: number) => void
|
||||
addTransaction: (transaction: WalletTransaction) => void
|
||||
}
|
||||
@@ -76,6 +77,38 @@ export const useWalletStore = create<WalletState>((set, get) => ({
|
||||
}))
|
||||
return true
|
||||
},
|
||||
refundPayment: (orderId, amount) => {
|
||||
if (!Number.isFinite(amount) || amount <= 0) return false
|
||||
|
||||
const state = get()
|
||||
const paid = state.transactions.some(
|
||||
(transaction) => transaction.type === "payment" && transaction.description.includes(orderId),
|
||||
)
|
||||
const refunded = state.transactions.some(
|
||||
(transaction) => transaction.type === "refund" && transaction.description.includes(orderId),
|
||||
)
|
||||
|
||||
if (!paid || refunded) {
|
||||
return false
|
||||
}
|
||||
|
||||
const now = new Date().toISOString()
|
||||
set((prev) => ({
|
||||
balance: prev.balance + amount,
|
||||
transactions: [
|
||||
{
|
||||
id: generateId("tx"),
|
||||
type: "refund",
|
||||
amount,
|
||||
description: `订单 ${orderId} 退款`,
|
||||
createdAt: now,
|
||||
},
|
||||
...prev.transactions,
|
||||
],
|
||||
}))
|
||||
|
||||
return true
|
||||
},
|
||||
addIncome: (orderId, amount) => {
|
||||
if (!Number.isFinite(amount) || amount <= 0) return
|
||||
|
||||
|
||||
Reference in New Issue
Block a user