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);
}
}