Files
nj/crates/erp-diary/src/service/notification_service.rs
iven 7e3597dc77 feat(diary): B4+B5+B6 后端服务 + F5/F6/F7 前端模块
后端 (erp-diary):
- B4: CommentService 班级成员验证 + 删除评语 + SSE 通知推送
- B4: NotificationService 评语/主题/成就三类通知事件
- B5: StickerService 贴纸包列表 + 贴纸查询 + 模板管理
- B5: AchievementService 成就列表 + 解锁 + SSE 通知
- B6: MoodStatsService 心情统计 + 连续天数
- B6: ContentSafetyService 敏感词过滤框架
- SSE handler 增加 diary.notification.* 事件处理
- 新增 14 个 API 端点 + diary.comment.delete 权限

前端 (Flutter):
- F5: CalendarBloc + 月视图日历 + 日记列表
- F6: MoodBloc + fl_chart 心情饼图 + 统计卡片 + 连续天数
- F7: 贴纸库分类浏览 + 模板画廊
- 首页改为日记流 + 心情快速选择
- 成就页改为徽章收集展示

验证: cargo check ✓ cargo test 17/17 ✓ flutter analyze 0 error
2026-06-01 09:32:09 +08:00

124 lines
4.0 KiB
Rust
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.
// 通知服务 — 将日记事件转化为 SSE 推送通知
//
// 此服务监听日记模块的领域事件,通过 EventBus 发布通知事件,
// SSE handler (erp-message) 负责将通知推送给在线用户。
use sea_orm::DatabaseConnection;
use uuid::Uuid;
use erp_core::events::{DomainEvent, EventBus};
use crate::dto::{NotificationPayload, NotificationType};
/// 通知服务 — 将日记领域事件转化为 SSE 推送
pub struct NotificationService;
impl NotificationService {
/// 评语创建通知
///
/// 当老师点评日记后,通知学生收到新评语。
pub async fn notify_comment_created(
tenant_id: Uuid,
student_id: Uuid,
teacher_id: Uuid,
comment_id: Uuid,
journal_id: Uuid,
content_preview: String,
db: &DatabaseConnection,
event_bus: &EventBus,
) {
let payload = NotificationPayload {
notification_type: NotificationType::CommentReceived,
recipient_id: student_id,
title: "收到新评语".to_string(),
body: content_preview,
business_id: Some(comment_id),
extra: Some(serde_json::json!({
"journal_id": journal_id,
"teacher_id": teacher_id,
})),
};
Self::publish_notification(tenant_id, payload, db, event_bus).await;
}
/// 主题布置通知
///
/// 当老师布置新主题后,通知班级所有学生。
pub async fn notify_topic_assigned(
tenant_id: Uuid,
class_id: Uuid,
topic_id: Uuid,
title: String,
db: &DatabaseConnection,
event_bus: &EventBus,
) {
let payload = NotificationPayload {
notification_type: NotificationType::TopicAssigned,
recipient_id: Uuid::nil(), // 班级广播SSE handler 按 class_id 过滤
title: "新主题布置".to_string(),
body: title,
business_id: Some(topic_id),
extra: Some(serde_json::json!({
"class_id": class_id,
})),
};
Self::publish_notification(tenant_id, payload, db, event_bus).await;
}
/// 成就解锁通知
///
/// 当用户解锁成就后,通知该用户。
pub async fn notify_achievement_unlocked(
tenant_id: Uuid,
user_id: Uuid,
achievement_code: String,
achievement_name: String,
db: &DatabaseConnection,
event_bus: &EventBus,
) {
let payload = NotificationPayload {
notification_type: NotificationType::AchievementUnlocked,
recipient_id: user_id,
title: "恭喜解锁新成就!".to_string(),
body: format!("你解锁了「{}」成就", achievement_name),
business_id: None,
extra: Some(serde_json::json!({
"achievement_code": achievement_code,
})),
};
Self::publish_notification(tenant_id, payload, db, event_bus).await;
}
/// 发布通知事件到 EventBus
///
/// 使用 `diary.notification` 作为事件类型前缀,
/// SSE handler 可据此识别并推送给在线用户。
async fn publish_notification(
tenant_id: Uuid,
payload: NotificationPayload,
db: &DatabaseConnection,
event_bus: &EventBus,
) {
let event_type = match &payload.notification_type {
NotificationType::CommentReceived => "diary.notification.comment",
NotificationType::TopicAssigned => "diary.notification.topic",
NotificationType::AchievementUnlocked => "diary.notification.achievement",
NotificationType::ClassUpdate => "diary.notification.class_update",
};
event_bus
.publish(
DomainEvent::new(
event_type,
tenant_id,
serde_json::to_value(&payload).unwrap_or_default(),
),
db,
)
.await;
}
}