企业微信消息通知要怎么声明 Rust 结构体
是的,今天来看企业微信的消息通知了,跟各大平台的消息通知杠上了,实际上是跟 Rust 的结构体杠上了。
毕竟,数据结构的设计和声明是编程工作中非常重要的部分,数据结构和结构体声明好了,代码其实就已经走好了很大一段路了。
而跟 Python 接触这些年,发现大多数情况下可能趋于只关注逻辑,却忘记了去关注数据结构的声明,宁愿很多时候在代码里面做很多奇奇怪怪的一些临时数据操作,也很难认真去真真切切地看一下到底有哪些数据主体,他们的关系是什么样的。谁和谁是同一个层面的,却被分在了不同的地方;谁又和谁本质上不是同一个东西,却又在代码里面绕啊绕,绕成了一个东西。
企业微信的消息通知,比钉钉的消息通知要稍微复杂一点,除了基本的几种消息结构外,它还有“模版卡片”这种复杂的消息结构,它的数据结构,其实等于在它的内层又声明了好几种更细分的格式定义,这里我们参考的文档是这个 https://developer.work.weixin.qq.com/document/path/90372
惯例,先看它的 json 声明
// 文本消息
{
"touser" : "UserID1|UserID2|UserID3",
"toparty" : "PartyID1|PartyID2",
"totag" : "TagID1 | TagID2",
"msgtype" : "text",
"agentid" : 1,
"text" : {
"content" : "你的快递已到,请携带工卡前往邮件中心领取。\n出发前可查看<a href=\"http://work.weixin.qq.com\">邮件中心视频实况</a>,聪明避开排队。"
},
"safe":0,
"enable_id_trans": 0,
"enable_duplicate_check": 0,
"duplicate_check_interval": 1800
}
// 图片消息
{
"touser" : "UserID1|UserID2|UserID3",
"toparty" : "PartyID1|PartyID2",
"totag" : "TagID1 | TagID2",
"msgtype" : "image",
"agentid" : 1,
"image" : {
"media_id" : "MEDIA_ID"
},
"safe":0,
"enable_duplicate_check": 0,
"duplicate_check_interval": 1800
}
// 语音消息
{
"touser" : "UserID1|UserID2|UserID3",
"toparty" : "PartyID1|PartyID2",
"totag" : "TagID1 | TagID2",
"msgtype" : "voice",
"agentid" : 1,
"voice" : {
"media_id" : "MEDIA_ID"
},
"enable_duplicate_check": 0,
"duplicate_check_interval": 1800
}
// 视频消息
{
"touser" : "UserID1|UserID2|UserID3",
"toparty" : "PartyID1|PartyID2",
"totag" : "TagID1 | TagID2",
"msgtype" : "video",
"agentid" : 1,
"video" : {
"media_id" : "MEDIA_ID",
"title" : "Title",
"description" : "Description"
},
"safe":0,
"enable_duplicate_check": 0,
"duplicate_check_interval": 1800
}
// 文件消息
{
"touser" : "UserID1|UserID2|UserID3",
"toparty" : "PartyID1|PartyID2",
"totag" : "TagID1 | TagID2",
"msgtype" : "file",
"agentid" : 1,
"file" : {
"media_id" : "1Yv-zXfHjSjU-7LH-GwtYqDGS-zz6w22KmWAT5COgP7o"
},
"safe":0,
"enable_duplicate_check": 0,
"duplicate_check_interval": 1800
}
// 文本卡片消息
{
"touser" : "UserID1|UserID2|UserID3",
"toparty" : "PartyID1 | PartyID2",
"totag" : "TagID1 | TagID2",
"msgtype" : "textcard",
"agentid" : 1,
"textcard" : {
"title" : "领奖通知",
"description" : "<div class=\"gray\">2016年9月26日</div> <div class=\"normal\">恭喜你抽中iPhone 7一台,领奖码:xxxx</div><div class=\"highlight\">请于2016年10月10日前联系行政同事领取</div>",
"url" : "URL",
"btntxt":"更多"
},
"enable_id_trans": 0,
"enable_duplicate_check": 0,
"duplicate_check_interval": 1800
}
// 图文消息
{
"touser" : "UserID1|UserID2|UserID3",
"toparty" : "PartyID1 | PartyID2",
"totag" : "TagID1 | TagID2",
"msgtype" : "news",
"agentid" : 1,
"news" : {
"articles" : [
{
"title" : "中秋节礼品领取",
"description" : "今年中秋节公司有豪礼相送",
"url" : "URL",
"picurl" : "http://res.mail.qq.com/node/ww/wwopenmng/images/independent/doc/test_pic_msg1.png",
"appid": "wx123123123123123",
"pagepath": "pages/index?userid=zhangsan&orderid=123123123"
}
]
},
"enable_id_trans": 0,
"enable_duplicate_check": 0,
"duplicate_check_interval": 1800
}
// 图文消息(mpnews)
{
"touser" : "UserID1|UserID2|UserID3",
"toparty" : "PartyID1 | PartyID2",
"totag": "TagID1 | TagID2",
"msgtype" : "mpnews",
"agentid" : 1,
"mpnews" : {
"articles":[
{
"title": "Title",
"thumb_media_id": "MEDIA_ID",
"author": "Author",
"content_source_url": "URL",
"content": "Content",
"digest": "Digest description"
}
]
},
"safe":0,
"enable_id_trans": 0,
"enable_duplicate_check": 0,
"duplicate_check_interval": 1800
}
// markdown消息
{
"touser" : "UserID1|UserID2|UserID3",
"toparty" : "PartyID1|PartyID2",
"totag" : "TagID1 | TagID2",
"msgtype": "markdown",
"agentid" : 1,
"markdown": {
"content": "您的会议室已经预定,稍后会同步到`邮箱`
>**事项详情**
>事 项:<font color=\"info\">开会</font>
>组织者:@miglioguan
>参与者:@miglioguan、@kunliu、@jamdeezhou、@kanexiong、@kisonwang
>
>会议室:<font color=\"info\">广州TIT 1楼 301</font>
>日 期:<font color=\"warning\">2018年5月18日</font>
>时 间:<font color=\"comment\">上午9:00-11:00</font>
>
>请准时参加会议。
>
>如需修改会议信息,请点击:[修改会议信息](https://work.weixin.qq.com)"
},
"enable_duplicate_check": 0,
"duplicate_check_interval": 1800
}
// 小程序通知消息
{
"touser" : "zhangsan|lisi",
"toparty": "1|2",
"totag": "1|2",
"msgtype" : "miniprogram_notice",
"miniprogram_notice" : {
"appid": "wx123123123123123",
"page": "pages/index?userid=zhangsan&orderid=123123123",
"title": "会议室预订成功通知",
"description": "4月27日 16:16",
"emphasis_first_item": true,
"content_item": [
{
"key": "会议室",
"value": "402"
},
{
"key": "会议地点",
"value": "广州TIT-402会议室"
},
{
"key": "会议时间",
"value": "2018年8月1日 09:00-09:30"
},
{
"key": "参与人员",
"value": "周剑轩"
}
]
},
"enable_id_trans": 0,
"enable_duplicate_check": 0,
"duplicate_check_interval": 1800
}
看到这里,我们分析一下啊,touser
toparty
totag
enable_id_trans
enable_duplicate_check
duplicate_check_interval
这些字段虽然每个里面都有,但是它们是业务逻辑里面的字段,不是消息本身的字段数据,真正消息本身的字段就只有 msg type
和那个动态根据消息类型不同而不同的键,所以其实你发现又与钉钉的消息结构思路就一致了。
然后,就是更为复杂的模版卡片消息,不怕麻烦,json 我们也一样仔细看一下。
// 模版卡片消息 - 文本通知型
{
"touser" : "UserID1|UserID2|UserID3",
"toparty" : "PartyID1 | PartyID2",
"totag" : "TagID1 | TagID2",
"msgtype" : "template_card",
"agentid" : 1,
"template_card" : {
"card_type" : "text_notice",
"source" : {
"icon_url": "图片的url",
"desc": "企业微信",
"desc_color": 1
},
"action_menu": {
"desc": "卡片副交互辅助文本说明",
"action_list": [
{"text": "接受推送", "key": "A"},
{"text": "不再推送", "key": "B"}
]
},
"task_id": "task_id",
"main_title" : {
"title" : "欢迎使用企业微信",
"desc" : "您的好友正在邀请您加入企业微信"
},
"quote_area": {
"type": 1,
"url": "https://work.weixin.qq.com",
"title": "企业微信的引用样式",
"quote_text": "企业微信真好用呀真好用"
},
"emphasis_content": {
"title": "100",
"desc": "核心数据"
},
"sub_title_text" : "下载企业微信还能抢红包!",
"horizontal_content_list" : [
{
"keyname": "邀请人",
"value": "张三"
},
{
"type": 1,
"keyname": "企业微信官网",
"value": "点击访问",
"url": "https://work.weixin.qq.com"
},
{
"type": 2,
"keyname": "企业微信下载",
"value": "企业微信.apk",
"media_id": "文件的media_id"
},
{
"type": 3,
"keyname": "员工信息",
"value": "点击查看",
"userid": "zhangsan"
}
],
"jump_list" : [
{
"type": 1,
"title": "企业微信官网",
"url": "https://work.weixin.qq.com"
},
{
"type": 2,
"title": "跳转小程序",
"appid": "小程序的appid",
"pagepath": "/index.html"
}
],
"card_action": {
"type": 2,
"url": "https://work.weixin.qq.com",
"appid": "小程序的appid",
"pagepath": "/index.html"
}
},
"enable_id_trans": 0,
"enable_duplicate_check": 0,
"duplicate_check_interval": 1800
}
// 模版卡片消息 - 图文展示型
{
"touser" : "UserID1|UserID2|UserID3",
"toparty" : "PartyID1 | PartyID2",
"totag" : "TagID1 | TagID2",
"msgtype" : "template_card",
"agentid" : 1,
"template_card" : {
"card_type" : "news_notice",
"source" : {
"icon_url": "图片的url",
"desc": "企业微信",
"desc_color": 1
},
"action_menu": {
"desc": "卡片副交互辅助文本说明",
"action_list": [
{"text": "接受推送", "key": "A"},
{"text": "不再推送", "key": "B"}
]
},
"task_id": "task_id",
"main_title" : {
"title" : "欢迎使用企业微信",
"desc" : "您的好友正在邀请您加入企业微信"
},
"quote_area": {
"type": 1,
"url": "https://work.weixin.qq.com",
"title": "企业微信的引用样式",
"quote_text": "企业微信真好用呀真好用"
},
"image_text_area": {
"type": 1,
"url": "https://work.weixin.qq.com",
"title": "企业微信的左图右文样式",
"desc": "企业微信真好用呀真好用",
"image_url": "https://img.iplaysoft.com/wp-content/uploads/2019/free-images/free_stock_photo_2x.jpg"
},
"card_image": {
"url": "图片的url",
"aspect_ratio": 1.3
},
"vertical_content_list": [
{
"title": "惊喜红包等你来拿",
"desc": "下载企业微信还能抢红包!"
}
],
"horizontal_content_list" : [
{
"keyname": "邀请人",
"value": "张三"
},
{
"type": 1,
"keyname": "企业微信官网",
"value": "点击访问",
"url": "https://work.weixin.qq.com"
},
{
"type": 2,
"keyname": "企业微信下载",
"value": "企业微信.apk",
"media_id": "文件的media_id"
},
{
"type": 3,
"keyname": "员工信息",
"value": "点击查看",
"userid": "zhangsan"
}
],
"jump_list" : [
{
"type": 1,
"title": "企业微信官网",
"url": "https://work.weixin.qq.com"
},
{
"type": 2,
"title": "跳转小程序",
"appid": "小程序的appid",
"pagepath": "/index.html"
}
],
"card_action": {
"type": 2,
"url": "https://work.weixin.qq.com",
"appid": "小程序的appid",
"pagepath": "/index.html"
}
},
"enable_id_trans": 0,
"enable_duplicate_check": 0,
"duplicate_check_interval": 1800
}
// 模版卡片消息 - 按钮交互型
{
"touser" : "UserID1|UserID2|UserID3",
"toparty" : "PartyID1 | PartyID2",
"totag" : "TagID1 | TagID2",
"msgtype" : "template_card",
"agentid" : 1,
"template_card" : {
"card_type" : "button_interaction",
"source" : {
"icon_url": "图片的url",
"desc": "企业微信",
"desc_color": 1
},
"action_menu": {
"desc": "卡片副交互辅助文本说明",
"action_list": [
{"text": "接受推送", "key": "A"},
{"text": "不再推送", "key": "B"}
]
},
"main_title" : {
"title" : "欢迎使用企业微信",
"desc" : "您的好友正在邀请您加入企业微信"
},
"quote_area": {
"type": 1,
"url": "https://work.weixin.qq.com",
"title": "企业微信的引用样式",
"quote_text": "企业微信真好用呀真好用"
},
"sub_title_text" : "下载企业微信还能抢红包!",
"horizontal_content_list" : [
{
"keyname": "邀请人",
"value": "张三"
},
{
"type": 1,
"keyname": "企业微信官网",
"value": "点击访问",
"url": "https://work.weixin.qq.com"
},
{
"type": 2,
"keyname": "企业微信下载",
"value": "企业微信.apk",
"media_id": "文件的media_id"
},
{
"type": 3,
"keyname": "员工信息",
"value": "点击查看",
"userid": "zhangsan"
}
],
"card_action": {
"type": 2,
"url": "https://work.weixin.qq.com",
"appid": "小程序的appid",
"pagepath": "/index.html"
},
"task_id": "task_id",
"button_selection": {
"question_key": "btn_question_key1",
"title": "企业微信评分",
"option_list": [
{
"id": "btn_selection_id1",
"text": "100分"
},
{
"id": "btn_selection_id2",
"text": "101分"
}
],
"selected_id": "btn_selection_id1"
},
"button_list": [
{
"text": "按钮1",
"style": 1,
"key": "button_key_1"
},
{
"text": "按钮2",
"style": 2,
"key": "button_key_2"
}
]
},
"enable_id_trans": 0,
"enable_duplicate_check": 0,
"duplicate_check_interval": 1800
}
// 模版卡片消息 - 投票选择型
{
"touser" : "UserID1|UserID2|UserID3",
"toparty" : "PartyID1 | PartyID2",
"totag" : "TagID1 | TagID2",
"msgtype" : "template_card",
"agentid" : 1,
"template_card" : {
"card_type" : "vote_interaction",
"source" : {
"icon_url": "图片的url",
"desc": "企业微信"
},
"main_title" : {
"title" : "欢迎使用企业微信",
"desc" : "您的好友正在邀请您加入企业微信"
},
"task_id": "task_id",
"checkbox": {
"question_key": "question_key1",
"option_list": [
{
"id": "option_id1",
"text": "选择题选项1",
"is_checked": true
},
{
"id": "option_id2",
"text": "选择题选项2",
"is_checked": false
}
],
"mode": 1
},
"submit_button": {
"text": "提交",
"key": "key"
}
},
"enable_id_trans": 0,
"enable_duplicate_check": 0,
"duplicate_check_interval": 1800
}
// 模版卡片消息 - 多项选择型
{
"touser" : "UserID1|UserID2|UserID3",
"toparty" : "PartyID1 | PartyID2",
"totag" : "TagID1 | TagID2",
"msgtype" : "template_card",
"agentid" : 1,
"template_card" : {
"card_type" : "multiple_interaction",
"source" : {
"icon_url": "图片的url",
"desc": "企业微信"
},
"main_title" : {
"title" : "欢迎使用企业微信",
"desc" : "您的好友正在邀请您加入企业微信"
},
"task_id": "task_id",
"select_list": [
{
"question_key": "question_key1",
"title": "选择器标签1",
"selected_id": "selection_id1",
"option_list": [
{
"id": "selection_id1",
"text": "选择器选项1"
},
{
"id": "selection_id2",
"text": "选择器选项2"
}
]
},
{
"question_key": "question_key2",
"title": "选择器标签2",
"selected_id": "selection_id3",
"option_list": [
{
"id": "selection_id3",
"text": "选择器选项3"
},
{
"id": "selection_id4",
"text": "选择器选项4"
}
]
}
],
"submit_button": {
"text": "提交",
"key": "key"
}
},
"enable_id_trans": 0,
"enable_duplicate_check": 0,
"duplicate_check_interval": 1800
}
可以看到,这模版卡片消息的复杂度可比基本消息类型复杂多了。
我们先把基本消息类型来实现一下,先看看效果。
这次很自然地,你会发现同样也用到了枚举类型进行综合性声明,并用到了 serde
的 tag
和 rename
属性,这基本和之前的枚举声明类似的套路,就不用再细说了吧。
model/msg/wework/mod.rs
pub mod template_card;
use crate::model::msg::wework::template_card::WeworkTemplateCardMsg;
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, Ord, PartialOrd, Eq, PartialEq, Hash)]
pub enum WeworkMsgType {
/// 文本消息
#[serde(rename = "text")]
Text,
/// 图片消息
#[serde(rename = "image")]
Image,
/// 语音消息
#[serde(rename = "voice")]
Voice,
/// 视频消息
#[serde(rename = "video")]
Video,
/// 文件消息
#[serde(rename = "file")]
File,
/// 文本卡片消息
#[serde(rename = "textcard")]
TextCard,
/// 图文消息
#[serde(rename = "news")]
News,
/// 图文消息(mpnews)
#[serde(rename = "mpnews")]
MpNews,
/// markdown 消息
#[serde(rename = "markdown")]
Markdown,
/// 小程序通知消息
#[serde(rename = "miniprogram_notice")]
MiniProgramNotice,
/// 模版卡片消息
#[serde(rename = "template_card")]
TemplateCard,
}
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, Ord, PartialOrd, Eq, PartialEq, Hash)]
pub enum WeworkMsgTemplateCardType {
/// 文本通知型
#[serde(rename = "text_notice")]
TextNotice,
/// 图文展示型
#[serde(rename = "news_notice")]
NewsNotice,
/// 按钮交互型
#[serde(rename = "button_interaction")]
ButtonInteraction,
/// 投票选择型
#[serde(rename = "vote_interaction")]
VoteInteraction,
/// 多项选择型
#[serde(rename = "multiple_interaction")]
MultipleInteraction,
}
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
#[serde(tag = "msgtype")]
pub enum WeworkMsg {
/// 文本消息
#[serde(rename = "text")]
Text(WeworkTextMsg),
/// 图片消息
#[serde(rename = "image")]
Image(WeworkImageMsg),
/// 语音消息
#[serde(rename = "voice")]
Voice(WeworkVoiceMsg),
/// 视频消息
#[serde(rename = "video")]
Video(WeworkVideoMsg),
/// 文件消息
#[serde(rename = "file")]
File(WeworkFileMsg),
/// 文本卡片消息
#[serde(rename = "textcard")]
TextCard(WeworkTextCardMsg),
/// 图文消息
#[serde(rename = "news")]
News(WeworkNewsMsg),
/// 图文消息(mpnews)
#[serde(rename = "mpnews")]
MpNews(WeworkMpNewsMsg),
/// markdown 消息
#[serde(rename = "markdown")]
Markdown(WeworkMarkdownMsg),
/// 小程序通知消息
#[serde(rename = "miniprogram_notice")]
MiniProgramNotice(WeworkMiniProgramNoticeMsg),
/// 模版卡片消息
#[serde(rename = "template_card")]
TemplateCard(WeworkTemplateCardMsg),
}
/// 文本消息 text
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
pub struct WeworkTextMsg {
pub text: WeworkTextMsgBody,
}
/// 文本消息体
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
pub struct WeworkTextMsgBody {
/// 消息内容,最长不超过2048个字节,超过将截断(支持id转译)
pub content: String,
}
/// 图片消息 image
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
pub struct WeworkImageMsg {
pub image: WeworkImageMsgBody,
}
/// 图片消息体
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
pub struct WeworkImageMsgBody {
/// 图片媒体文件id,可以调用上传临时素材接口获取
pub media_id: String,
}
/// 语音消息 voice
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
pub struct WeworkVoiceMsg {
pub voice: WeworkVoiceMsgBody,
}
/// 语音消息体
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
pub struct WeworkVoiceMsgBody {
/// 语音文件id,可以调用上传临时素材接口获取
pub media_id: String,
}
/// 视频消息 video
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
pub struct WeworkVideoMsg {
pub video: WeworkVideoMsgBody,
}
/// 视频消息体
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
pub struct WeworkVideoMsgBody {
/// 视频媒体文件id,可以调用上传临时素材接口获取
pub media_id: String,
/// 视频消息的标题,不超过128个字节,超过会自动截断
pub title: Option<String>,
/// 视频消息的描述,不超过512个字节,超过会自动截断
pub description: Option<String>,
}
/// 文件消息 file
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
pub struct WeworkFileMsg {
pub file: WeworkFileMsgBody,
}
/// 文件消息体
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
pub struct WeworkFileMsgBody {
/// 文件id,可以调用上传临时素材接口获取
pub media_id: String,
}
/// 文本卡片消息 textcard
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
pub struct WeworkTextCardMsg {
pub textcard: WeworkTextCardMsgBody,
}
/// 文本卡片消息体
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
pub struct WeworkTextCardMsgBody {
/// 标题,不超过128个字节,超过会自动截断(支持id转译)
pub title: String,
/// 描述,不超过512个字节,超过会自动截断(支持id转译)
pub description: String,
/// 点击后跳转的链接。最长2048字节,请确保包含了协议头(http/https)
pub url: String,
/// 按钮文字。 默认为“详情”, 不超过4个文字,超过自动截断。
pub btntxt: Option<String>,
}
/// 图文消息 news
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
pub struct WeworkNewsMsg {
pub news: WeworkNewsMsgBody,
}
/// 图文消息体
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
pub struct WeworkNewsMsgBody {
/// 图文消息,一个图文消息支持1到8条图文
pub articles: Vec<WeworkNewsMsgBodyArticle>,
}
/// 一条图文
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
pub struct WeworkNewsMsgBodyArticle {
/// 标题,不超过128个字节,超过会自动截断(支持id转译)
pub title: String,
/// 描述,不超过512个字节,超过会自动截断(支持id转译)
pub description: Option<String>,
/// 点击后跳转的链接。 最长2048字节,请确保包含了协议头(http/https),小程序或者url必须填写一个
pub url: Option<String>,
/// 图文消息的图片链接,最长2048字节,支持JPG、PNG格式,较好的效果为大图 1068*455,小图150*150。
pub picurl: Option<String>,
/// 小程序appid,必须是与当前应用关联的小程序,appid和pagepath必须同时填写,填写后会忽略url字段
pub appid: Option<String>,
/// 点击消息卡片后的小程序页面,最长128字节,仅限本小程序内的页面。appid和pagepath必须同时填写,填写后会忽略url字段
pub pagepath: Option<String>,
}
/// 图文消息 mpnews(图文内容存储在企业微信)
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
pub struct WeworkMpNewsMsg {
pub mpnews: WeworkMpNewsMsgBody,
}
/// 图文消息体 mpnews
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
pub struct WeworkMpNewsMsgBody {
/// 图文消息,一个图文消息支持1到8条图文
pub articles: Vec<WeworkMpNewsMsgBodyArticle>,
}
/// 一条图文消息 mpnews
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
pub struct WeworkMpNewsMsgBodyArticle {
/// 标题,不超过128个字节,超过会自动截断(支持id转译)
pub title: String,
/// 图文消息缩略图的media_id, 可以通过素材管理接口获得。此处thumb_media_id即上传接口返回的media_id
pub thumb_media_id: String,
/// 图文消息的作者,不超过64个字节
pub author: Option<String>,
/// 图文消息点击“阅读原文”之后的页面链接
pub content_source_url: Option<String>,
/// 图文消息的内容,支持html标签,不超过666 K个字节(支持id转译)
pub content: String,
/// 图文消息的描述,不超过512个字节,超过会自动截断(支持id转译)
pub digest: Option<String>,
}
/// Markdown 消息
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
pub struct WeworkMarkdownMsg {
pub markdown: WeworkMarkdownMsgBody,
}
/// Markdown 消息体
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
pub struct WeworkMarkdownMsgBody {
/// markdown内容,最长不超过2048个字节,必须是utf8编码
pub content: String,
}
/// 小程序消息
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
pub struct WeworkMiniProgramNoticeMsg {
pub miniprogram_notice: WeworkMiniProgramNoticeMsgBody,
}
/// 小程序消息体
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
pub struct WeworkMiniProgramNoticeMsgBody {
/// 小程序appid,必须是与当前应用关联的小程序
pub appid: String,
/// 点击消息卡片后的小程序页面,最长1024个字节,仅限本小程序内的页面。该字段不填则消息点击后不跳转。
pub page: Option<String>,
/// 消息标题,长度限制4-12个汉字(支持id转译)
pub title: String,
/// 消息描述,长度限制4-12个汉字(支持id转译)
pub description: Option<String>,
/// 是否放大第一个content_item
pub emphasis_first_item: Option<bool>,
/// 消息内容键值对,最多允许10个item
pub content_item: Option<Vec<WeworkMiniProgramNoticeMsgBodyContentItem>>,
}
/// 小程序消息体内容
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
pub struct WeworkMiniProgramNoticeMsgBodyContentItem {
/// 长度10个汉字以内
pub key: String,
/// 长度30个汉字以内(支持id转译)
pub value: String,
}
虽然“模版卡片”的消息复杂,但是无论是外层结构还是内层结构,都一样还是那个套路,那这事就清晰明朗了。
由于“模版卡片”的消息结构毕竟复杂,这写到差不多的时候,自然就会想要把它独立出去为一个单独的文件,这样在代码查看和整体结构上会有一个更好的体验和设计,这时候就要用到 Rust 的 ”模块“ mod
这个概念了。
先说在前面,如果你用过 Python 的话,你会发现 Rust 的模块 mod.rs
与 Python 中的模块 __init__.py
它俩非常相似,可以说几乎雷同了。
model/msg/wework/template_card.rs
/// 模版卡片消息
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
pub struct WeworkTemplateCardMsg {
/// 模版卡片消息体
pub template_card: TemplateCard,
}
/// 模版卡片消息体
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
#[serde(tag = "card_type")]
pub enum TemplateCard {
/// 文本通知型
#[serde(rename = "text_notice")]
TextNotice(TextNotice),
/// 图文展示型
#[serde(rename = "news_notice")]
NewsNotice(NewsNotice),
/// 按钮交互型
#[serde(rename = "button_interaction")]
ButtonInteraction(ButtonInteraction),
/// 投票选择型
#[serde(rename = "vote_interaction")]
VoteInteraction(VoteInteraction),
/// 多项选择型
#[serde(rename = "multiple_interaction")]
MultipleInteraction(MultipleInteraction),
}
/// 文本通知型
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
pub struct TextNotice {
/// 卡片来源样式信息,不需要来源样式可不填写
pub source: Option<Source>,
/// 卡片右上角更多操作按钮
pub action_menu: Option<ActionMenu>,
/// 任务id,同一个应用任务id不能重复,只能由数字、字母和“_-@”组成,最长128字节,填了action_menu字段的话本字段必填
pub task_id: Option<String>,
/// 一级标题
pub main_title: Option<MainTile>,
/// 引用文献样式
pub quote_area: Option<QuoteArea>,
/// 关键数据样式
pub emphasis_content: Option<EmphasisContent>,
/// 二级普通文本,建议不超过160个字,(支持id转译)
pub sub_title_text: Option<String>,
/// 二级标题+文本列表,该字段可为空数组,但有数据的话需确认对应字段是否必填,列表长度不超过6
pub horizontal_content_list: Option<Vec<HorizontalContentItem>>,
/// 跳转指引样式的列表,该字段可为空数组,但有数据的话需确认对应字段是否必填,列表长度不超过3
pub jump_list: Option<Vec<JumpItem>>,
/// 整体卡片的点击跳转事件,text_notice必填本字段
pub card_action: CardAction,
}
/// 图文展示型
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
pub struct NewsNotice {
/// 卡片来源样式信息,不需要来源样式可不填写
pub source: Option<Source>,
/// 卡片右上角更多操作按钮
pub action_menu: Option<ActionMenu>,
/// 任务id,同一个应用任务id不能重复,只能由数字、字母和“_-@”组成,最长128字节
pub task_id: String,
/// 一级标题
pub main_title: MainTile,
/// 引用文献样式
pub quote_area: Option<QuoteArea>,
/// 左图右文样式,news_notice类型的卡片,card_image和image_text_area两者必填一个字段,不可都不填
pub image_text_area: Option<ImageTextArea>,
///图片样式,news_notice类型的卡片,card_image和image_text_area两者必填一个字段,不可都不填
pub card_image: Option<CardImage>,
/// 卡片二级垂直内容,该字段可为空数组,但有数据的话需确认对应字段是否必填,列表长度不超过4
pub vertical_content_list: Option<Vec<VerticalContentItem>>,
/// 二级标题+文本列表,该字段可为空数组,但有数据的话需确认对应字段是否必填,列表长度不超过6
pub horizontal_content_list: Option<Vec<HorizontalContentItem>>,
/// 跳转指引样式的列表,该字段可为空数组,但有数据的话需确认对应字段是否必填,列表长度不超过3
pub jump_list: Vec<JumpItem>,
/// 整体卡片的点击跳转事件,news_notice必填本字段
pub card_action: CardAction,
}
/// 按钮交互型
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
pub struct ButtonInteraction {
/// 卡片来源样式信息,不需要来源样式可不填写
pub source: Option<Source>,
/// 卡片右上角更多操作按钮
pub action_menu: Option<ActionMenu>,
/// 一级标题
pub main_title: MainTile,
/// 引用文献样式
pub quote_area: Option<QuoteArea>,
/// 二级普通文本,建议不超过160个字,(支持id转译)
pub sub_title_text: Option<String>,
/// 二级标题+文本列表,该字段可为空数组,但有数据的话需确认对应字段是否必填,列表长度不超过6
pub horizontal_content_list: Option<Vec<HorizontalContentItem>>,
/// 整体卡片的点击跳转事件
pub card_action: Option<CardAction>,
/// 任务id,同一个应用任务id不能重复,只能由数字、字母和“_-@”组成,最长128字节
pub task_id: String,
/// 下拉式的选择器
pub button_selection: ButtonSelection,
/// 按钮列表,列表长度不超过6
pub button_list: Vec<ButtonItem>,
}
/// 投票选择型
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
pub struct VoteInteraction {
/// 卡片来源样式信息,不需要来源样式可不填写
pub source: Option<Source>,
/// 一级标题
pub main_title: MainTile,
/// 任务id,同一个应用任务id不能重复,只能由数字、字母和“_-@”组成,最长128字节
pub task_id: String,
/// 选择题样式
pub checkbox: Option<Checkbox>,
/// 提交按钮样式
pub submit_button: Option<SubmitButton>,
}
/// 多项选择型
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
pub struct MultipleInteraction {
/// 卡片来源样式信息,不需要来源样式可不填写
pub source: Option<Source>,
/// 一级标题
pub main_title: MainTile,
/// 任务id,同一个应用任务id不能重复,只能由数字、字母和“_-@”组成,最长128字节
pub task_id: String,
/// 下拉式的选择器列表,multiple_interaction类型的卡片该字段不可为空,一个消息最多支持 3 个选择器
pub select_list: Vec<SelectItem>,
/// 提交按钮样式
pub submit_button: SubmitButton,
}
/// 卡片来源样式信息
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
pub struct Source {
/// 来源图片的url,来源图片的尺寸建议为72*72
pub icon_url: Option<String>,
/// 来源图片的描述,建议不超过20个字,(支持id转译)
pub desc: Option<String>,
/// 来源文字的颜色,目前支持:0(默认) 灰色,1 黑色,2 红色,3 绿色
pub desc_color: Option<String>,
}
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
pub struct ActionMenu {
/// 更多操作界面的描述
pub desc: Option<String>,
/// 操作列表,列表长度取值范围为 [1, 3]
pub action_list: Vec<ActionMenuItem>,
}
/// 操作
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
pub struct ActionMenuItem {
/// 操作的描述文案
pub text: String,
/// 操作key值,用户点击后,会产生回调事件将本参数作为EventKey返回,回调事件会带上该key值,最长支持1024字节,不可重复
pub key: String,
}
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
pub struct MainTile {
/// 一级标题,建议不超过36个字,文本通知型卡片本字段非必填,但不可本字段和sub_title_text都不填,(支持id转译)
pub title: Option<String>,
/// 标题辅助信息,建议不超过44个字,(支持id转译)
pub desc: Option<String>,
}
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
pub struct QuoteArea {
/// 引用文献样式区域点击事件,0或不填代表没有点击事件,1 代表跳转url,2 代表跳转小程序
#[serde(rename = "type")]
pub type_: Option<i32>,
/// 点击跳转的url,quote_area.type是1时必填
pub url: Option<String>,
/// 点击跳转的小程序的appid,必须是与当前应用关联的小程序,quote_area.type是2时必填
pub appid: Option<String>,
/// 点击跳转的小程序的pagepath,quote_area.type是2时选填
pub pagepath: Option<String>,
/// 引用文献样式的标题
pub title: Option<String>,
/// 引用文献样式的引用文案
pub quote_text: Option<String>,
}
/// 左图右文样式
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
pub struct ImageTextArea {
/// 左图右文样式区域点击事件,0或不填代表没有点击事件,1 代表跳转url,2 代表跳转小程序
#[serde(rename = "type")]
pub type_: Option<i32>,
/// 点击跳转的url,image_text_area.type是1时必填
pub url: Option<String>,
/// 点击跳转的小程序的appid,必须是与当前应用关联的小程序,image_text_area.type是2时必填
pub appid: Option<String>,
/// 点击跳转的小程序的pagepath,image_text_area.type是2时选填
pub pagepath: Option<String>,
/// 左图右文样式的标题
pub title: Option<String>,
/// 左图右文样式的描述
pub desc: Option<String>,
/// 左图右文样式的图片url
pub image_url: String,
}
/// 图片样式
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
pub struct CardImage {
/// 图片的url
pub url: String,
/// 图片的宽高比,宽高比要小于2.25,大于1.3,不填该参数默认1.3
pub aspect_ratio: Option<f64>,
}
/// 卡片二级垂直内容
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
pub struct VerticalContentItem {
/// 卡片二级标题,建议不超过38个字
pub title: String,
/// 二级普通文本,建议不超过160个字
pub desc: Option<String>,
}
/// 关键数据样式
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
pub struct EmphasisContent {
/// 关键数据样式的数据内容,建议不超过14个字
pub title: Option<String>,
/// 关键数据样式的数据描述内容,建议不超过22个字
pub desc: Option<String>,
}
/// 二级标题+文本列表
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
pub struct HorizontalContentItem {
/// 链接类型,0或不填代表不是链接,1 代表跳转url,2 代表下载附件,3 代表点击跳转成员详情
#[serde(rename = "type")]
pub type_: Option<String>,
/// 二级标题,建议不超过5个字
pub keyname: String,
/// 二级文本,如果horizontal_content_list.type是2,该字段代表文件名称(要包含文件类型),建议不超过30个字,(支持id转译)
pub value: Option<String>,
/// 链接跳转的url,horizontal_content_list.type是1时必填
pub url: Option<String>,
/// 附件的media_id,horizontal_content_list.type是2时必填
pub media_id: Option<String>,
/// 成员详情的userid,horizontal_content_list.type是3时必填
pub userid: Option<String>,
}
/// 跳转指引样式的列表
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
pub struct JumpItem {
/// 跳转链接类型,0或不填代表不是链接,1 代表跳转url,2 代表跳转小程序
#[serde(rename = "type")]
pub type_: Option<String>,
/// 跳转链接样式的文案内容,建议不超过18个字
pub title: String,
/// 跳转链接的url,jump_list.type是1时必填
pub url: Option<String>,
/// 跳转链接的小程序的appid,必须是与当前应用关联的小程序,jump_list.type是2时必填
pub appid: Option<String>,
/// 跳转链接的小程序的pagepath,jump_list.type是2时选填
pub pagepath: Option<String>,
}
/// 整体卡片的点击跳转事件
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
pub struct CardAction {
/// 跳转事件类型,1 代表跳转url,2 代表打开小程序。text_notice卡片模版中该字段取值范围为[1,2]
#[serde(rename = "type")]
pub type_: String,
/// 跳转事件的url,card_action.type是1时必填
pub url: String,
/// 跳转事件的小程序的appid,必须是与当前应用关联的小程序,card_action.type是2时必填
pub appid: String,
/// 跳转事件的小程序的pagepath,card_action.type是2时选填
pub pagepath: String,
}
/// 下拉式的选择器
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
pub struct ButtonSelection {
/// 下拉式的选择器的key,用户提交选项后,会产生回调事件,回调事件会带上该key值表示该题,最长支持1024字节
pub questioin_key: String,
/// 下拉式的选择器左边的标题
pub title: Option<String>,
/// 选项列表,下拉选项不超过 10 个,最少1个
pub option_list: Vec<ButtonSelectionOptionItem>,
/// 默认选定的id,不填或错填默认第一个
pub selected_id: Option<String>,
}
/// 下拉式的选择器 选项列表
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
pub struct ButtonSelectionOptionItem {
/// 下拉式的选择器选项的id,用户提交后,会产生回调事件,回调事件会带上该id值表示该选项,最长支持128字节,不可重复
pub id: String,
/// 下拉式的选择器选项的文案,建议不超过16个字
pub text: String,
}
/// 按钮
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
pub struct ButtonItem {
/// 按钮点击事件类型,0 或不填代表回调点击事件,1 代表跳转url
#[serde(rename = "type")]
pub type_: Option<String>,
/// 按钮文案,建议不超过10个字
pub text: String,
/// 按钮样式,目前可填1~4,不填或错填默认1
pub style: Option<i32>,
/// 按钮key值,用户点击后,会产生回调事件将本参数作为EventKey返回,回调事件会带上该key值,最长支持1024字节,不可重复,button_list.type是0时必填
pub key: Option<String>,
/// 跳转事件的url,button_list.type是1时必填
pub url: Option<String>,
}
/// 选择题样式
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
pub struct Checkbox {
/// 选择题key值,用户提交选项后,会产生回调事件,回调事件会带上该key值表示该题,最长支持1024字节
pub question_key: String,
/// 选择题模式,单选:0,多选:1,不填默认0
pub mode: Option<i32>,
/// 选项list,选项个数不超过 20 个,最少1个
pub option_list: Vec<CheckboxOptionItem>,
}
/// 选项
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
pub struct CheckboxOptionItem {
/// 选项id,用户提交选项后,会产生回调事件,回调事件会带上该id值表示该选项,最长支持128字节,不可重复
pub id: String,
/// 选项文案描述,建议不超过17个字
pub text: String,
/// 该选项是否要默认选中
pub is_checked: bool,
}
/// 提交按钮
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
pub struct SubmitButton {
/// 按钮文案,建议不超过10个字,不填默认为提交
pub text: String,
/// 提交按钮的key,会产生回调事件将本参数作为EventKey返回,最长支持1024字节
pub key: String,
}
/// 下拉式的选择器
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
pub struct SelectItem {
/// 下拉式的选择器题目的key,用户提交选项后,会产生回调事件,回调事件会带上该key值表示该题,最长支持1024字节,不可重复
pub question_key: String,
/// 下拉式的选择器上面的title
pub title: Option<String>,
/// 默认选定的id,不填或错填默认第一个
pub selected_id: Option<String>,
/// 选项列表,下拉选项不超过 10 个,最少1个
pub option_list: Vec<SelectItemOptionItem>,
}
/// 下拉式的选择器 选项列表
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
pub struct SelectItemOptionItem {
/// 下拉式的选择器选项的id,用户提交选项后,会产生回调事件,回调事件会带上该id值表示该选项,最长支持128字节,不可重复
pub id: String,
/// 下拉式的选择器选项的文案,建议不超过16个字
pub text: String,
}
这次又是类似,无论多么复杂的结构,层层分解,每次声明局部的一个小部分的时候,就关注局部的这个小部分就可以了,取个合理的名称,如果名称太长,考虑合理简化一些,字段名称是什么,序列化和反序列化时候的名称要不要变化,它是一个可选字段还是必选字段,它的数据类型又是什么,如果是一个对象,那么就又再给它命一个结构出来即可,循环往复这个动作,最终完成所有结构的声明即可。
这次除了新增了关于 “模块“ mod.rs
的知识之外,其实就没有更多的要细说的了,编程,基本知识学到位之后,到最后,其实就是一个需要细心和耐心的体力活,只要是自己手写出来的代码,你都始终会有印象的,那时候才算知识的真正学会了。
顺便提一嘴,你想想,如果不是去声明这些结构体,而是面对着层层嵌套的 json 直接进行操作,你觉得会有什么样的体验?
还有哦,结构体的文档说明一定要有,否则过一段时间你就忘记了它是什么意思了。同时呢,这些结构体上的文档说明,也会被其他工具所用来自动化地去展示非常详尽的接口文档,包括字段值的类型,例如 OpenAPI 或 Swagger 等,这是我在 Python 或者 Golang 从未得到过完善解决甚至有时根本无法解决,而在 Rust 这里,被做到了极致。