feat(app): 设置页 UI + Mood/成就/贴纸 BLoC 接入 API + B7 测试扩展

前端改动:
- 新建设置页面 (主题切换/关于/隐私政策/用户协议/儿童隐私保护)
- SettingsBloc 注册到 MultiRepositoryProvider 全局可访问
- MoodBloc 修复编译错误 + 接入 /diary/stats/mood API
- MoodPage 添加错误状态展示和重试按钮
- AchievementBloc + 页面改造接入 /diary/achievements API
- StickerBloc + 页面改造接入 /diary/sticker-packs API
- TemplateBloc + 页面改造接入 /diary/templates API
- ProfilePage 设置入口改为跳转 /settings
- 添加 /settings 路由

后端改动:
- 扩展 mood_stats_service 测试 (连续天数算法/心情计数/边界场景)
- 新增 class_service 测试 (班级码生成/唯一性/错误映射)
- 新增 achievement_service 测试 (DTO 结构/序列化/map 构建)
- 新增 sticker_service 测试 (DTO 序列化/错误处理)
- 扩展 dto.rs 测试 (achievement/mood_stats/sticker/template/notification)
- 清理 2 个 unused import warning

验证:
- cargo check 0 error 0 warning
- flutter analyze 0 error
This commit is contained in:
iven
2026-06-01 11:19:43 +08:00
parent 860e9e5d22
commit 8331db63ba
19 changed files with 1749 additions and 326 deletions

View File

@@ -430,4 +430,140 @@ mod tests {
let json = serde_json::to_string(&resp).unwrap();
assert!(json.contains("\"is_active\":true"));
}
#[test]
fn achievement_resp_serializes() {
let resp = AchievementResp {
id: uuid::Uuid::nil(),
code: "first_diary".into(),
name: "初次落笔".into(),
description: Some("写下第一篇日记".into()),
icon: Some("✏️".into()),
category: "writing".into(),
is_unlocked: true,
unlocked_at: None,
};
let json = serde_json::to_string(&resp).unwrap();
assert!(json.contains("\"code\":\"first_diary\""));
assert!(json.contains("\"is_unlocked\":true"));
}
#[test]
fn achievement_resp_unlocked_at_present() {
let now = chrono::Utc::now();
let resp = AchievementResp {
id: uuid::Uuid::nil(),
code: "streak_7".into(),
name: "坚持一周".into(),
description: None,
icon: None,
category: "writing".into(),
is_unlocked: true,
unlocked_at: Some(now),
};
let json = serde_json::to_string(&resp).unwrap();
assert!(json.contains("unlocked_at"));
}
#[test]
fn mood_stats_resp_structure() {
let resp = MoodStatsResp {
mood_counts: vec![
MoodCount {
mood: Mood::Happy,
count: 12,
percentage: 60.0,
},
MoodCount {
mood: Mood::Calm,
count: 8,
percentage: 40.0,
},
],
streak_days: 7,
total_journals: 20,
dominant_mood: Some(Mood::Happy),
};
let json = serde_json::to_string(&resp).unwrap();
assert!(json.contains("\"streak_days\":7"));
assert!(json.contains("\"total_journals\":20"));
assert!(json.contains("\"dominant_mood\":\"happy\""));
}
#[test]
fn sticker_resp_fields() {
let resp = StickerResp {
id: uuid::Uuid::nil(),
pack_id: uuid::Uuid::nil(),
name: "笑脸".into(),
image_url: "https://cdn.example.com/smile.png".into(),
category: Some("表情".into()),
};
let json = serde_json::to_string(&resp).unwrap();
assert!(json.contains("\"image_url\""));
assert!(json.contains("\"category\":\"表情\""));
}
#[test]
fn template_resp_with_layout_data() {
let resp = TemplateResp {
id: uuid::Uuid::nil(),
name: "校园日记".into(),
description: Some("在学校的一天".into()),
preview_url: Some("https://cdn.example.com/preview.png".into()),
template_data: Some(serde_json::json!({
"sections": [{"type": "title"}, {"type": "body"}]
})),
category: Some("校园".into()),
is_free: true,
};
let json = serde_json::to_string(&resp).unwrap();
assert!(json.contains("\"sections\""));
assert!(json.contains("\"preview_url\""));
}
#[test]
fn create_journal_req_with_class() {
let json = r#"{
"title": "班级日记",
"date": "2026-06-01",
"mood": "happy",
"weather": "sunny",
"tags": ["校园"],
"is_private": false,
"class_id": "00000000-0000-0000-0000-000000000001"
}"#;
let req: CreateJournalReq = serde_json::from_str(json).unwrap();
assert!(!req.is_private);
assert!(req.class_id.is_some());
}
#[test]
fn join_class_req() {
let json = r#"{"class_code": "ABC123"}"#;
let req: JoinClassReq = serde_json::from_str(json).unwrap();
assert_eq!(req.class_code, "ABC123");
}
#[test]
fn create_class_req() {
let json = r#"{"name": "三年二班", "school_name": "阳光小学"}"#;
let req: CreateClassReq = serde_json::from_str(json).unwrap();
assert_eq!(req.name, "三年二班");
assert_eq!(req.school_name, Some("阳光小学".into()));
}
#[test]
fn notification_payload_structure() {
let payload = NotificationPayload {
notification_type: NotificationType::AchievementUnlocked,
recipient_id: uuid::Uuid::nil(),
title: "成就解锁".into(),
body: "你解锁了「初次落笔」成就".into(),
business_id: Some(uuid::Uuid::nil()),
extra: None,
};
let json = serde_json::to_string(&payload).unwrap();
assert!(json.contains("\"notification_type\":\"achievement_unlocked\""));
}
}