后端 (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
124 lines
4.0 KiB
Rust
124 lines
4.0 KiB
Rust
// 通知服务 — 将日记事件转化为 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;
|
||
}
|
||
}
|