P1-8 通知实时推送(SSE): - 新增 /api/notifications/stream SSE 端点(15 秒推送,5 分钟超时) - 新增 useNotificationStream Hook(SSE + 轮询降级) - NotificationDropdown 改用 SSE 实时推送 P2-12 测试覆盖: - notifications/dispatcher.test.ts(6 个测试,渠道选择逻辑) - notifications/channels/in-app-channel.test.ts(9 个测试,类型映射) - messaging/schema.test.ts(34 个测试,Zod 校验) - tests/e2e/messages.spec.ts(消息模块 E2E 测试) - vitest.unit.config.ts 添加 server-only stub P2-13a 通知发送日志持久化: - 新增 notification_logs 表(userId/title/channel/status/messageId/error/sentAt) - logNotificationSend 改为 async 写入 DB(失败降级 console) - dispatcher 传递 payload 用于持久化 P2-13b 通知优先级和归档: - messageNotifications 表新增 priority(low/normal/high/urgent)和 isArchived 字段 - getNotifications 支持归档和优先级筛选 - 新增 archiveNotificationAction - NotificationList 显示优先级 Badge 和归档按钮 P2-13c 消息星标和草稿: - messages 表新增 isStarred 字段 - 新增 message_drafts 表 - 新增 toggleMessageStar + 草稿 CRUD Server Actions - 新增 5 个草稿 data-access 函数 P2-13d 公告已读回执和置顶: - announcements 表新增 isPinned 字段 - 新增 announcement_reads 表(唯一索引保证幂等) - 新增 toggleAnnouncementPinAction + markAnnouncementAsReadAction - getAnnouncements 排序置顶优先 P2-13e 通知分类筛选和桌面推送: - NotificationList 添加按类型筛选按钮组 - 新增 useDesktopNotifications Hook(浏览器 Notification API) - NotificationDropdown 集成桌面推送(新通知触发) 架构图同步: - 004 和 005 均已更新(新增表、Action、Hook、组件描述)
158 lines
4.3 KiB
TypeScript
158 lines
4.3 KiB
TypeScript
import "server-only"
|
||
|
||
/**
|
||
* 监控埋点接口(预留)
|
||
*
|
||
* 在关键 Server Action 中调用 trackEvent 记录业务事件,用于:
|
||
* - 公告阅读率、消息回复率等关键指标统计
|
||
* - 通知发送失败告警
|
||
* - 用户行为漏斗分析
|
||
*
|
||
* 当前实现:输出到 console.info,不阻塞主流程。
|
||
* 后续扩展:可接入外部监控服务(如 Sentry / PostHog / 自建埋点系统),
|
||
* 只需在 trackEventToSink 中替换实现即可,调用方无需改动。
|
||
*/
|
||
|
||
/** 事件名称(使用点号分隔的命名空间,如 "announcement.published") */
|
||
export type EventName =
|
||
| "announcement.created"
|
||
| "announcement.updated"
|
||
| "announcement.published"
|
||
| "announcement.archived"
|
||
| "announcement.deleted"
|
||
| "announcement.pin_toggled"
|
||
| "announcement.marked_read"
|
||
| "message.sent"
|
||
| "message.deleted"
|
||
| "message.marked_read"
|
||
| "message.star_toggled"
|
||
| "notification.marked_read"
|
||
| "notification.marked_all_read"
|
||
| "notification.sent"
|
||
| "notification.send_failed"
|
||
| "notification.archived"
|
||
| "attendance.recorded"
|
||
| "attendance.batch_recorded"
|
||
| "attendance.updated"
|
||
| "attendance.deleted"
|
||
| "attendance.rules_saved"
|
||
| "elective.course_created"
|
||
| "elective.course_updated"
|
||
| "elective.course_deleted"
|
||
| "elective.selection_opened"
|
||
| "elective.selection_closed"
|
||
| "elective.course_selected"
|
||
| "elective.course_dropped"
|
||
| "elective.lottery_completed"
|
||
// 6.7: 考试/作业模块监控事件
|
||
| "exam.created"
|
||
| "exam.updated"
|
||
| "exam.published"
|
||
| "exam.archived"
|
||
| "exam.deleted"
|
||
| "exam.duplicated"
|
||
| "exam.ai_generated"
|
||
| "exam.submitted"
|
||
| "exam.graded"
|
||
| "homework.created"
|
||
| "homework.updated"
|
||
| "homework.published"
|
||
| "homework.archived"
|
||
| "homework.deleted"
|
||
| "homework.submitted"
|
||
| "homework.graded"
|
||
| "homework.auto_save_failed"
|
||
// AI 模块监控事件
|
||
| "ai.chat"
|
||
| "ai.chat_stream"
|
||
| "ai.similar_question"
|
||
| "ai.grading_assist"
|
||
| "ai.lesson_content"
|
||
| "ai.question_variant"
|
||
| "ai.weakness_analysis"
|
||
| "ai.child_summary"
|
||
| "ai.study_path"
|
||
|
||
/** 埋点事件负载 */
|
||
export interface TrackEventPayload {
|
||
/** 事件名称 */
|
||
event: EventName
|
||
/** 当前用户 ID(可选,未登录场景为 undefined) */
|
||
userId?: string
|
||
/** 目标对象 ID(如公告 ID、消息 ID) */
|
||
targetId?: string
|
||
/** 目标对象类型(如 "announcement"、"message") */
|
||
targetType?: string
|
||
/** 附加属性(如受众人数、渠道类型) */
|
||
properties?: Record<string, unknown>
|
||
}
|
||
|
||
/**
|
||
* 将事件发送到外部监控服务。
|
||
*
|
||
* 当前为占位实现:仅输出到 console.info。
|
||
* 接入真实服务时替换此函数体即可。
|
||
*/
|
||
function trackEventToSink(payload: TrackEventPayload): void {
|
||
console.info(
|
||
`[TrackEvent] ${payload.event} userId=${payload.userId ?? "-"} targetId=${payload.targetId ?? "-"}${payload.properties ? ` properties=${JSON.stringify(payload.properties)}` : ""}`
|
||
)
|
||
}
|
||
|
||
/**
|
||
* 记录一个监控事件。
|
||
*
|
||
* 非阻塞:任何异常都被吞掉,确保不影响主业务流程。
|
||
*
|
||
* @example
|
||
* ```ts
|
||
* await trackEvent({
|
||
* event: "announcement.published",
|
||
* userId: ctx.userId,
|
||
* targetId: id,
|
||
* targetType: "announcement",
|
||
* properties: { audienceSize: userIds.length },
|
||
* })
|
||
* ```
|
||
*/
|
||
export async function trackEvent(payload: TrackEventPayload): Promise<void> {
|
||
try {
|
||
trackEventToSink(payload)
|
||
} catch {
|
||
// 埋点失败不影响主流程
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 6.7: 考试/作业模块专用埋点函数
|
||
*
|
||
* 封装 trackEvent,自动设置 targetType,简化调用方代码。
|
||
*
|
||
* @example
|
||
* ```ts
|
||
* await trackExamEvent("exam.published", { userId: ctx.userId, targetId: examId })
|
||
* await trackExamEvent("homework.submitted", {
|
||
* userId: ctx.userId,
|
||
* targetId: submissionId,
|
||
* properties: { questionCount, duration: 1200 }
|
||
* })
|
||
* ```
|
||
*/
|
||
export async function trackExamEvent(
|
||
event: Extract<EventName, `exam.${string}` | `homework.${string}`>,
|
||
params: {
|
||
userId?: string
|
||
targetId?: string
|
||
properties?: Record<string, unknown>
|
||
}
|
||
): Promise<void> {
|
||
const targetType = event.startsWith("exam.") ? "exam" : "homework"
|
||
await trackEvent({
|
||
event,
|
||
userId: params.userId,
|
||
targetId: params.targetId,
|
||
targetType,
|
||
properties: params.properties,
|
||
})
|
||
}
|