Files
NextEdu/src/shared/lib/track-event.ts
SpecialX f75602d14e feat(announcements,messaging,notifications): 实现所有长期问题 — SSE 实时推送 + 通知日志持久化 + 优先级/归档 + 消息星标/草稿 + 公告已读回执/置顶 + 分类筛选/桌面推送 + 测试覆盖
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、组件描述)
2026-06-23 10:13:57 +08:00

158 lines
4.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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,
})
}