fache 0.1.351

发车工具箱
Documentation
use {Deserialize, Serialize};

// 消息类型枚举
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum MsgType {
    Text,
    Markdown,
    Image,
    News,
    File,
    Voice,
    TemplateCard,
}

// 文本消息内容
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct TextContent {
    pub content: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub mentioned_list: Option<Vec<String>>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub mentioned_mobile_list: Option<Vec<String>>,
}

// Markdown消息内容
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct MarkdownContent {
    pub content: String,
}

// 图片消息内容
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ImageContent {
    pub base64: String,
    pub md5: String,
}

// 图文消息内容
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct NewsContent {
    pub articles: Vec<Article>,
}

// 单条图文
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Article {
    pub title: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub description: Option<String>,
    pub url: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub picurl: Option<String>,
}

// 媒体ID内容(用于文件和语音)
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct MediaIdContent {
    pub media_id: String,
}

// 模板卡片消息 - 各部分定义
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Source {
    pub icon_url: String,
    pub desc: String,
    pub desc_color: i32,
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct MainTitle {
    pub title: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub desc: Option<String>,
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct EmphasisContent {
    pub title: String,
    pub desc: String,
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct QuoteArea {
    #[serde(skip_serializing_if = "Option::is_none")]
    pub r#type: Option<i32>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub url: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub appid: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub pagepath: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub title: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub quote_text: Option<String>,
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct HorizontalContentList {
    pub keyname: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub value: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub r#type: Option<i32>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub url: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub media_id: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub userid: Option<String>,
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct JumpList {
    #[serde(skip_serializing_if = "Option::is_none")]
    pub r#type: Option<i32>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub url: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub appid: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub pagepath: Option<String>,
    pub title: String,
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct CardAction {
    pub r#type: i32,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub url: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub appid: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub pagepath: Option<String>,
}

// 模板卡片内容
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct TemplateCardContent {
    pub card_type: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub source: Option<Source>,
    pub main_title: MainTitle,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub emphasis_content: Option<EmphasisContent>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub quote_area: Option<QuoteArea>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub sub_title_text: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub horizontal_content_list: Option<Vec<HorizontalContentList>>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub jump_list: Option<Vec<JumpList>>,
    pub card_action: CardAction,
}

// 统一消息结构
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "msgtype")]
pub enum Message {
    #[serde(rename = "text")]
    Text { text: TextContent }, // 添加text字段包装

    #[serde(rename = "markdown")]
    Markdown { markdown: MarkdownContent }, // 添加markdown字段包装

    #[serde(rename = "image")]
    Image { image: ImageContent }, // 添加image字段包装

    #[serde(rename = "news")]
    News { news: NewsContent }, // 添加news字段包装

    #[serde(rename = "file")]
    File { file: MediaIdContent }, // 添加file字段包装

    #[serde(rename = "voice")]
    Voice { voice: MediaIdContent }, // 添加voice字段包装

    #[serde(rename = "template_card")]
    TemplateCard { template_card: TemplateCardContent }, // 添加template_card字段包装
}
// 文件上传响应
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct UploadFileResp {
    pub errcode: i32,
    pub errmsg: String,
    #[serde(rename = "type")]
    pub file_type: String,
    pub media_id: String,
    pub created_at: u64,
}

// 发送消息响应
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct SendMessageResp {
    pub errcode: Option<u16>,
    pub errmsg: Option<String>,
}

// 在lib.rs中添加测试模块
#[cfg(test)]
mod tests {
    use super::*;
    use crate::json;
    /// 验证文本消息序列化格式是否符合企业微信API要求
    #[test]
    fn test_text_message_serialization() {
        let text_msg = Message::Text {
            text: TextContent {
                content: "测试消息".to_string(),
                mentioned_list: Some(vec!["user1".to_string(), "user2".to_string()]),
                mentioned_mobile_list: None,
            },
        };
        // 序列化消息
        let jsons = json::to_string(&text_msg).unwrap();

        // 预期JSON(符合企业微信API规范)
        let expected = r#"{"msgtype":"text","text":{"content":"测试消息","mentioned_list":["user1","user2"]}}"#;
        assert_eq!(jsons, expected);

        println!("文本消息序列化验证通过: {}", jsons);
    }

    /// 验证Markdown消息序列化
    #[test]
    fn test_markdown_message_serialization() {
        let markdown_msg = Message::Markdown {
            markdown: MarkdownContent {
                content: "# 标题\n这是一条Markdown消息".to_string(),
            },
        };

        // 使用serde_json进行序列化(确保已导入serde_json)
        let json = serde_json::to_string(&markdown_msg).unwrap();
        let expected =
            r##"{"msgtype":"markdown","markdown":{"content":"# 标题\n这是一条Markdown消息"}}"##;
        assert_eq!(json, expected);
    }

    /// 验证图片消息序列化
    #[test]
    fn test_image_message_serialization() {
        let image_msg = Message::Image{
            image: ImageContent {
                base64: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=".to_string(),
                md5: "d41d8cd98f00b204e9800998ecf8427e".to_string(),
            }
        };
        let json = json::to_string(&image_msg).unwrap();
        let expected = r#"{"msgtype":"image","image":{"base64":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=","md5":"d41d8cd98f00b204e9800998ecf8427e"}}"#;
        assert_eq!(json, expected);
    }

    /// 验证图文消息序列化
    #[test]
    fn test_news_message_serialization() {
        let news_msg = Message::News {
            news: NewsContent {
                articles: vec![
                    Article {
                        title: "文章标题1".to_string(),
                        description: Some("文章描述1".to_string()),
                        url: "https://example.com/article1".to_string(),
                        picurl: Some("https://example.com/image1.jpg".to_string()),
                    },
                    Article {
                        title: "文章标题2".to_string(),
                        description: None,
                        url: "https://example.com/article2".to_string(),
                        picurl: None,
                    },
                ],
            },
        };

        let json = json::to_string(&news_msg).unwrap();
        let expected = r#"{"msgtype":"news","news":{"articles":[{"title":"文章标题1","description":"文章描述1","url":"https://example.com/article1","picurl":"https://example.com/image1.jpg"},{"title":"文章标题2","url":"https://example.com/article2"}]}}"#;
        assert_eq!(json, expected);
    }

    /// 验证文件消息序列化
    #[test]
    fn test_file_message_serialization() {
        let file_msg = Message::File {
            file: MediaIdContent {
                media_id: "FILE_MEDIA_ID_123456".to_string(),
            },
        };

        // 假设使用serde_json进行序列化
        let json = serde_json::to_string(&file_msg).unwrap();
        let expected = r#"{"msgtype":"file","file":{"media_id":"FILE_MEDIA_ID_123456"}}"#;
        assert_eq!(json, expected);
    }

    /// 验证语音消息序列化
    #[test]
    fn test_voice_message_serialization() {
        let voice_msg = Message::Voice {
            voice: MediaIdContent {
                media_id: "VOICE_MEDIA_ID_123456".to_string(),
            },
        };

        // 使用serde_json进行序列化(确保已导入serde_json)
        let json = serde_json::to_string(&voice_msg).unwrap();
        let expected = r#"{"msgtype":"voice","voice":{"media_id":"VOICE_MEDIA_ID_123456"}}"#;
        assert_eq!(json, expected);
    }

    /// 验证模板卡片消息序列化
    #[test]
    fn test_template_card_message_serialization() {
        let template_card_msg = Message::TemplateCard {
            template_card: TemplateCardContent {
                card_type: "text_notice".to_string(),
                source: Some(Source {
                    icon_url: "https://example.com/icon.png".to_string(),
                    desc: "来源描述".to_string(),
                    desc_color: 0,
                }),
                main_title: MainTitle {
                    title: "卡片标题".to_string(),
                    desc: Some("卡片副标题".to_string()),
                },
                emphasis_content: Some(EmphasisContent {
                    title: "重点标题".to_string(),
                    desc: "重点描述".to_string(),
                }),
                quote_area: Some(QuoteArea {
                    r#type: Some(1),
                    url: Some("https://example.com/quote".to_string()),
                    appid: None,
                    pagepath: None,
                    title: Some("引用标题".to_string()),
                    quote_text: Some("引用文本".to_string()),
                }),
                sub_title_text: Some("子标题文本".to_string()),
                horizontal_content_list: Some(vec![
                    HorizontalContentList {
                        keyname: "字段1".to_string(),
                        value: Some("值1".to_string()),
                        r#type: None,
                        url: None,
                        media_id: None,
                        userid: None,
                    },
                    HorizontalContentList {
                        keyname: "链接字段".to_string(),
                        value: None,
                        r#type: Some(1),
                        url: Some("https://example.com/link".to_string()),
                        media_id: None,
                        userid: None,
                    },
                ]),
                jump_list: Some(vec![
                    JumpList {
                        r#type: Some(1),
                        url: Some("https://example.com/jump1".to_string()),
                        appid: None,
                        pagepath: None,
                        title: "跳转链接1".to_string(),
                    },
                    JumpList {
                        r#type: Some(2),
                        url: None,
                        appid: Some("APP_ID_123".to_string()),
                        pagepath: Some("/pages/detail".to_string()),
                        title: "小程序跳转".to_string(),
                    },
                ]),
                card_action: CardAction {
                    r#type: 1,
                    url: Some("https://example.com/card_action".to_string()),
                    appid: None,
                    pagepath: None,
                },
            },
        };

        // 使用serde_json进行序列化(确保已导入serde_json)
        let json = serde_json::to_string(&template_card_msg).unwrap();

        // 预期JSON(符合企业微信API规范)
        let expected = r#"{"msgtype":"template_card","template_card":{"card_type":"text_notice","source":{"icon_url":"https://example.com/icon.png","desc":"来源描述","desc_color":0},"main_title":{"title":"卡片标题","desc":"卡片副标题"},"emphasis_content":{"title":"重点标题","desc":"重点描述"},"quote_area":{"type":1,"url":"https://example.com/quote","title":"引用标题","quote_text":"引用文本"},"sub_title_text":"子标题文本","horizontal_content_list":[{"keyname":"字段1","value":"值1"},{"keyname":"链接字段","type":1,"url":"https://example.com/link"}],"jump_list":[{"type":1,"url":"https://example.com/jump1","title":"跳转链接1"},{"type":2,"appid":"APP_ID_123","pagepath":"/pages/detail","title":"小程序跳转"}],"card_action":{"type":1,"url":"https://example.com/card_action"}}}"#;

        assert_eq!(json, expected);
    }
}