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:
@@ -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\""));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user