diff --git a/app/(account)/notifications/page.tsx b/app/(account)/notifications/page.tsx index 51e55ef..2360214 100644 --- a/app/(account)/notifications/page.tsx +++ b/app/(account)/notifications/page.tsx @@ -56,7 +56,7 @@ export default function NotificationsPage() { const systemNotifs = notifications.filter((notification) => notification.type === "system") return ( -
+

通知中心

@@ -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") {