|
|
@@ -0,0 +1,88 @@
|
|
|
+use serde::Deserialize;
|
|
|
+
|
|
|
+/// CFC 统一响应包装 Result<T>(code=200 视为成功)
|
|
|
+#[derive(Debug, Clone, Deserialize)]
|
|
|
+#[serde(rename_all = "camelCase")]
|
|
|
+pub struct ApiResult<T> {
|
|
|
+ pub code: i32,
|
|
|
+ pub message: Option<String>,
|
|
|
+ pub data: Option<T>,
|
|
|
+}
|
|
|
+
|
|
|
+impl<T> ApiResult<T> {
|
|
|
+ pub fn is_success(&self) -> bool {
|
|
|
+ self.code == 200
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+#[derive(Debug, Clone, Deserialize)]
|
|
|
+#[serde(rename_all = "camelCase")]
|
|
|
+pub struct CfcTask {
|
|
|
+ pub id: i64,
|
|
|
+ pub title: Option<String>,
|
|
|
+ pub status: Option<String>, // PENDING / IN_PROGRESS / COMPLETED / REJECTED
|
|
|
+ pub due_date: Option<String>,
|
|
|
+}
|
|
|
+
|
|
|
+#[derive(Debug, Clone, Deserialize)]
|
|
|
+#[serde(rename_all = "camelCase")]
|
|
|
+pub struct DimensionValue {
|
|
|
+ pub dimension: Option<String>,
|
|
|
+ pub value: i32,
|
|
|
+ pub color: Option<String>,
|
|
|
+}
|
|
|
+
|
|
|
+#[derive(Debug, Clone, Deserialize)]
|
|
|
+#[serde(rename_all = "camelCase")]
|
|
|
+pub struct EnergyOverview {
|
|
|
+ #[serde(default)]
|
|
|
+ pub dimensions: Vec<DimensionValue>,
|
|
|
+}
|
|
|
+
|
|
|
+#[derive(Debug, Clone, Deserialize)]
|
|
|
+#[serde(rename_all = "camelCase")]
|
|
|
+pub struct DailyCheckin {
|
|
|
+ pub checked: bool,
|
|
|
+ #[serde(default)]
|
|
|
+ pub items: Vec<String>,
|
|
|
+}
|
|
|
+
|
|
|
+#[derive(Debug, Clone, Deserialize)]
|
|
|
+#[serde(rename_all = "camelCase")]
|
|
|
+pub struct MascotInfo {
|
|
|
+ pub mascot_id: Option<String>,
|
|
|
+ pub name: Option<String>,
|
|
|
+}
|
|
|
+
|
|
|
+#[cfg(test)]
|
|
|
+mod tests {
|
|
|
+ use super::*;
|
|
|
+
|
|
|
+ #[test]
|
|
|
+ fn parses_task_with_camelcase_fields() {
|
|
|
+ let json = r#"{"id":3,"title":"喝水","status":"PENDING","dueDate":"2026-08-15"}"#;
|
|
|
+ let task: CfcTask = serde_json::from_str(json).unwrap();
|
|
|
+ assert_eq!(task.id, 3);
|
|
|
+ assert_eq!(task.status.as_deref(), Some("PENDING"));
|
|
|
+ assert_eq!(task.due_date.as_deref(), Some("2026-08-15"));
|
|
|
+ }
|
|
|
+
|
|
|
+ #[test]
|
|
|
+ fn parses_energy_overview_and_defaults_to_empty() {
|
|
|
+ let json = r##"{"dimensions":[{"dimension":"身","value":65,"color":"#FF8C42"}]}"##;
|
|
|
+ let e: EnergyOverview = serde_json::from_str(json).unwrap();
|
|
|
+ assert_eq!(e.dimensions.len(), 1);
|
|
|
+ assert_eq!(e.dimensions[0].value, 65);
|
|
|
+
|
|
|
+ let empty: EnergyOverview = serde_json::from_str(r#"{}"#).unwrap();
|
|
|
+ assert!(empty.dimensions.is_empty());
|
|
|
+ }
|
|
|
+
|
|
|
+ #[test]
|
|
|
+ fn parses_api_result_envelope() {
|
|
|
+ let json = r#"{"code":200,"message":"ok","data":{"mascotId":"xibao","name":"浠宝"}}"#;
|
|
|
+ let r: ApiResult<MascotInfo> = serde_json::from_str(json).unwrap();
|
|
|
+ assert!(r.is_success());
|
|
|
+ assert_eq!(r.data.unwrap().mascot_id.as_deref(), Some("xibao"));
|
|
|
+ }
|
|
|
+}
|