通知中心
@@ -76,13 +76,21 @@ export default function NotificationsPage() {
系统
-
- {notifications.map((notification) => (
-
- ))}
+
+ {notifications.length === 0 ? (
+
+
+ 暂无通知
+
+
+ ) : (
+ notifications.map((notification) => (
+
+ ))
+ )}
-
+
{orderNotifs.length === 0 ? (
@@ -94,7 +102,7 @@ export default function NotificationsPage() {
)}
-
+
{communityNotifs.length === 0 ? (
@@ -106,7 +114,7 @@ export default function NotificationsPage() {
)}
-
+
{systemNotifs.length === 0 ? (
diff --git a/app/(account)/settings/page.tsx b/app/(account)/settings/page.tsx
index 293f74a..2499048 100644
--- a/app/(account)/settings/page.tsx
+++ b/app/(account)/settings/page.tsx
@@ -17,7 +17,15 @@ import type { UserRole } from "@/lib/types"
import { useAuthStore } from "@/store/auth"
export default function SettingsPage() {
- const { currentRole, verifiedRoles, switchRole, user, updateProfile } = useAuthStore()
+ const {
+ currentRole,
+ verifiedRoles,
+ switchRole,
+ user,
+ updateProfile,
+ notificationPrefs,
+ setNotificationPref,
+ } = useAuthStore()
const [nickname, setNickname] = useState(user?.nickname ?? "")
const [bio, setBio] = useState(user?.bio ?? "")
const [avatar, setAvatar] = useState(user?.avatar ?? "")
@@ -26,7 +34,7 @@ export default function SettingsPage() {
const isRoleVerified = (role: UserRole) => verifiedRoles.includes(role)
return (
-
+
个人设置
@@ -176,7 +184,10 @@ export default function SettingsPage() {
订单通知
接单、完成、争议等状态变更
-
+
setNotificationPref("order", checked)}
+ />
@@ -184,7 +195,10 @@ export default function SettingsPage() {
社区通知
点赞、评论、关注
-
+ setNotificationPref("community", checked)}
+ />
@@ -192,7 +206,10 @@ export default function SettingsPage() {
系统通知
平台公告、活动推送
-
+
setNotificationPref("system", checked)}
+ />
diff --git a/lib/api/notifications.ts b/lib/api/notifications.ts
index 591a55b..803539e 100644
--- a/lib/api/notifications.ts
+++ b/lib/api/notifications.ts
@@ -1,5 +1,33 @@
+import { allow, deny } from "@/lib/policy/assert"
+import type { Notification } from "@/lib/types"
+import { useAuthStore } from "@/store/auth"
import { useNotificationStore } from "@/store/notifications"
export function listNotifications() {
return useNotificationStore.getState().notifications
}
+
+function isNotificationEnabled(type: Notification["type"]) {
+ const prefs = useAuthStore.getState().notificationPrefs
+ if (type === "order") return prefs.order
+ if (type === "community") return prefs.community
+ return prefs.system
+}
+
+export function addNotification(input: {
+ type: Notification["type"]
+ title: string
+ content: string
+ link?: string
+}) {
+ if (!isNotificationEnabled(input.type)) {
+ return deny("IDEMPOTENT_NOOP", "该类通知已关闭")
+ }
+
+ useNotificationStore.getState().addNotification(input)
+ return allow()
+}
+
+export function markNotificationAsRead(notificationId: string) {
+ useNotificationStore.getState().markAsRead(notificationId)
+}
diff --git a/store/auth.ts b/store/auth.ts
index cf4a214..a712ca5 100644
--- a/store/auth.ts
+++ b/store/auth.ts
@@ -1,17 +1,31 @@
import { create } from "zustand"
import type { User, UserRole, VerificationStatus } from "@/lib/types"
+interface NotificationPrefs {
+ order: boolean
+ community: boolean
+ system: boolean
+}
+
+const defaultNotificationPrefs: NotificationPrefs = {
+ order: true,
+ community: true,
+ system: false,
+}
+
interface AuthState {
isAuthenticated: boolean
currentRole: UserRole
verifiedRoles: UserRole[]
verificationStatus: Partial
>
verificationReasons: Partial>
+ notificationPrefs: NotificationPrefs
user: User | null
switchRole: (role: UserRole) => void
submitVerification: (role: UserRole) => void
approveVerification: (role: UserRole) => void
rejectVerification: (role: UserRole, reason: string) => void
+ setNotificationPref: (type: keyof NotificationPrefs, enabled: boolean) => void
updateProfile: (patch: { nickname?: string; bio?: string; avatar?: string }) => void
login: (user: User, verifiedRoles?: UserRole[]) => void
logout: () => void
@@ -23,6 +37,7 @@ export const useAuthStore = create((set, get) => ({
verifiedRoles: ["consumer"],
verificationStatus: { consumer: "approved" },
verificationReasons: {},
+ notificationPrefs: defaultNotificationPrefs,
user: null,
switchRole: (role) => {
const { verifiedRoles } = get()
@@ -84,6 +99,13 @@ export const useAuthStore = create((set, get) => ({
},
}
}),
+ setNotificationPref: (type, enabled) =>
+ set((state) => ({
+ notificationPrefs: {
+ ...state.notificationPrefs,
+ [type]: enabled,
+ },
+ })),
updateProfile: (patch) =>
set((state) => {
if (!state.user) return state
@@ -98,7 +120,7 @@ export const useAuthStore = create((set, get) => ({
}
}),
login: (user, verifiedRoles = ["consumer"]) =>
- set({
+ set((state) => ({
isAuthenticated: true,
user,
currentRole: user.role,
@@ -111,7 +133,8 @@ export const useAuthStore = create((set, get) => ({
{},
),
verificationReasons: {},
- }),
+ notificationPrefs: state.notificationPrefs,
+ })),
logout: () =>
set({
isAuthenticated: false,
@@ -119,6 +142,7 @@ export const useAuthStore = create((set, get) => ({
verifiedRoles: ["consumer"],
verificationStatus: { consumer: "approved" },
verificationReasons: {},
+ notificationPrefs: defaultNotificationPrefs,
user: null,
}),
}))
diff --git a/store/disputes.ts b/store/disputes.ts
index 5cdc3b0..eb2a15b 100644
--- a/store/disputes.ts
+++ b/store/disputes.ts
@@ -6,6 +6,8 @@ import type { Actor } from "@/lib/policy/actor"
import type { PolicyDecision } from "@/lib/policy/decision"
import { mockDisputes } from "@/lib/mock"
import type { Dispute } from "@/lib/types"
+import { useAuthStore } from "@/store/auth"
+import { useNotificationStore } from "@/store/notifications"
import { useOrderStore } from "@/store/orders"
type DisputeTimelineType = "created" | "response" | "reviewing" | "resolved" | "appealed"
@@ -120,6 +122,19 @@ function asRecord(dispute: Dispute): DisputeRecord {
}
}
+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)
@@ -148,11 +163,14 @@ export const useDisputeStore = create((set, get) => {
}, 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",
@@ -169,6 +187,11 @@ export const useDisputeStore = create((set, get) => {
}
}),
}))
+
+ if (resolvedOrderId) {
+ notifyDispute(resolvedOrderId, "争议已处理", "平台已给出争议处理结果")
+ }
+
clearProgressTimers(disputeId)
}, DISPUTE_TO_RESOLVED_MS)
@@ -275,6 +298,8 @@ export const useDisputeStore = create((set, get) => {
}),
}))
+ notifyDispute(dispute.orderId, "争议收到回应", "对方已提交争议回应材料")
+
return allow()
},
submitAppeal: (disputeId, actorId, reason) => {
@@ -322,6 +347,8 @@ export const useDisputeStore = create((set, get) => {
}),
}))
+ notifyDispute(dispute.orderId, "争议已申诉", "申诉已提交,平台将继续复核")
+
scheduleProgress(disputeId)
return allow()
},
diff --git a/store/notifications.ts b/store/notifications.ts
index 20bd2fd..9b354af 100644
--- a/store/notifications.ts
+++ b/store/notifications.ts
@@ -1,14 +1,47 @@
import { create } from "zustand"
+import { generateId } from "@/lib/id"
import { mockNotifications } from "@/lib/mock"
import type { Notification } from "@/lib/types"
+interface CreateNotificationInput {
+ type: Notification["type"]
+ title: string
+ content: string
+ link?: string
+}
+
interface NotificationState {
notifications: Notification[]
+ addNotification: (input: CreateNotificationInput) => Notification
+ markAsRead: (notificationId: string) => void
markAllAsRead: () => void
}
export const useNotificationStore = create((set) => ({
notifications: mockNotifications,
+ addNotification: (input) => {
+ const notification: Notification = {
+ id: generateId("notif"),
+ type: input.type,
+ title: input.title,
+ content: input.content,
+ link: input.link,
+ read: false,
+ createdAt: new Date().toISOString(),
+ }
+
+ set((state) => ({
+ notifications: [notification, ...state.notifications],
+ }))
+
+ return notification
+ },
+ markAsRead: (notificationId) =>
+ set((state) => ({
+ notifications: state.notifications.map((notification) =>
+ notification.id === notificationId ? { ...notification, read: true } : notification,
+ ),
+ })),
markAllAsRead: () =>
set((state) => ({
notifications: state.notifications.map((notification) => ({ ...notification, read: true })),
diff --git a/store/orders.ts b/store/orders.ts
index 7777edc..5b35d52 100644
--- a/store/orders.ts
+++ b/store/orders.ts
@@ -7,7 +7,9 @@ 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 { useAuthStore } from "@/store/auth"
import { useChatStore } from "@/store/chat"
+import { useNotificationStore } from "@/store/notifications"
import { useWalletStore } from "@/store/wallet"
interface CreateOrderInput {
@@ -191,6 +193,55 @@ function scheduleOrderTimeout(orderId: string, status: OrderStatus) {
orderTimeouts.set(orderId, timer)
}
+function notifyOrderStatus(order: Order) {
+ if (!useAuthStore.getState().notificationPrefs.order) {
+ return
+ }
+
+ const mapping: Partial> = {
+ pending_accept: {
+ title: "订单待接单",
+ content: `${order.service.title} 已支付,等待接单`,
+ },
+ in_progress: {
+ title: "订单已接单",
+ content: `${order.playerName} 已开始服务`,
+ },
+ pending_close: {
+ title: "订单发起结单",
+ content: `订单 ${order.id} 等待确认结单`,
+ },
+ pending_review: {
+ title: "订单待评价",
+ content: "服务已结束,可提交双向评价",
+ },
+ completed: {
+ title: "订单已完成",
+ content: `订单 ${order.id} 已完成`,
+ },
+ cancelled: {
+ title: "订单已取消",
+ content: `订单 ${order.id} 已取消`,
+ },
+ disputed: {
+ title: "订单进入争议",
+ content: "已发起争议,等待平台处理",
+ },
+ }
+
+ const payload = mapping[order.status]
+ if (!payload) {
+ return
+ }
+
+ useNotificationStore.getState().addNotification({
+ type: "order",
+ title: payload.title,
+ content: payload.content,
+ link: `/order/${order.id}`,
+ })
+}
+
export const useOrderStore = create((set, get) => {
const applyTransition = (
orderId: string,
@@ -226,7 +277,13 @@ export const useOrderStore = create((set, get) => {
}
if (previousOrder.status !== "completed" && updatedOrder.status === "completed") {
- useWalletStore.getState().addIncome(updatedOrder.id, updatedOrder.totalPrice)
+ useWalletStore
+ .getState()
+ .addIncome(updatedOrder.id, updatedOrder.totalPrice, updatedOrder.shopId)
+ }
+
+ if (previousOrder.status !== updatedOrder.status) {
+ notifyOrderStatus(updatedOrder)
}
const shouldRefund =
@@ -299,6 +356,7 @@ export const useOrderStore = create((set, get) => {
}))
useChatStore.getState().ensureOrderSession(order)
+ notifyOrderStatus(order)
return { decision: allow(), order }
},
payOrder: (orderId, actor) => applyTransition(orderId, "PAY", actor),
@@ -323,7 +381,9 @@ export const useOrderStore = create((set, get) => {
if (previousOrder && updatedOrder) {
if (previousOrder.status !== "completed" && updatedOrder.status === "completed") {
- useWalletStore.getState().addIncome(updatedOrder.id, updatedOrder.totalPrice)
+ useWalletStore
+ .getState()
+ .addIncome(updatedOrder.id, updatedOrder.totalPrice, updatedOrder.shopId)
}
if (previousOrder.status === "pending_accept" && updatedOrder.status === "cancelled") {