钉钉消息通知要怎么声明 Rust 结构体


今天我们来看下钉钉的消息格式,比起对接接口的返回值,这个业务的结构体声明就烦琐许多了,当然,更多的也是体力活和细心加理解。

它的文档地址是这个:https://open.dingtalk.com/document/orgapp/message-types-and-data-format 由于钉钉分第三方企业应用和企业内部应用,而仅有企业内部应用能够支持到完整丰富的消息格式,第三方应用被限制在三种格式的模版申请上,这里就以企业内部应用的消息通知来进行说明。

先看下它的几个基本格式:

// 文本消息
{
    "msgtype": "text",
    "text": {
        "content": "月会通知"
    }
}
// 图片消息
{
    "msgtype": "image",
    "image": {
        "media_id": "@lADOADmaWMzazQKA"
    }
}
// 语音消息
{
    "msgtype": "voice",
    "voice": {
       "media_id": "@lADOADmaWMzazQKA",
       "duration": "10"
    }
}
// 文件消息
{
    "msgtype": "file",
    "file": {
       "media_id": "MEDIA_ID"
    }
}
// 链接消息
{
    "msgtype": "link",
    "link": {
        "messageUrl": "http://s.dingtalk.com/market/dingtalk/error_code.php",
        "picUrl":"@lALOACZwe2Rk",
        "title": "测试",
        "text": "测试"
    }
}
// OA 消息
{
     "msgtype": "oa",
     "oa": {
        "message_url": "http://dingtalk.com",
        "head": {
            "bgcolor": "FFBBBBBB",
            "text": "头部标题"
        },
        "body": {
            "title": "正文标题",
            "form": [
                {
                    "key": "姓名:",
                    "value": "张三"
                },
                {
                    "key": "年龄:",
                    "value": "20"
                },
                {
                    "key": "身高:",
                    "value": "1.8米"
                },
                {
                    "key": "体重:",
                    "value": "130斤"
                },
                {
                    "key": "学历:",
                    "value": "本科"
                },
                {
                    "key": "爱好:",
                    "value": "打球、听音乐"
                }
            ],
            "rich": {
                "num": "15.6",
                "unit": "元"
            },
            "content": "大段文本大段文本大段文本大段文本大段文本大段文本",
            "image": "@lADOADmaWMzazQKA",
            "file_count": "3",
            "author": "李四 "
        }
    }
}
// markdown 消息
{
    "msgtype": "markdown",
    "markdown": {
        "title": "首屏会话透出的展示内容",
        "text": "# 这是支持markdown的文本   \n   ## 标题2    \n   * 列表1   \n  ![alt 啊](https://img.alicdn.com/tps/TB1XLjqNVXXXXc4XVXXXXXXXXXX-170-64.png)"
    }
}
// 卡片消息
{
    "msgtype": "action_card",
    "action_card": {
        "title": "是透出到会话列表和通知的文案",
        "markdown": "支持markdown格式的正文内容",
        "single_title": "查看详情",
        "single_url": "https://open.dingtalk.com"
    }
}

发现规律没有,就是都有一个共同的字段叫 msgtype ,而它的值,又恰好是它另外一个字段的键,这它另外那个字段的值,才是真正的消息内容结构。

按照 Python 或者其他语言的直觉想法,可能认为这应该有多少种类型就命多少个结构体,msgtype 的值看起来是个枚举,那么就给它命几个枚举值即可。

至于嵌套的结构,在 Python 中可能会也可能不会声明,甚至整个所有消息结构都不声明,但这个毕竟不是长久之计。而在 Golang 里面,直接声明嵌套的 struct 也是很容易的事情,那么直接八个结构体也能搞定,如果是还没有升级到可以用上泛型的版本,那么用到这些消息的方法里面可能就得用 interface {} 来声明参数类型了,这时候的编码体验其实并不比 Python 好多少。

那么在 Rust 里面,也可以按照类似的思路,先实现一波。

与 Golang 不同,Rust 是不能直接声明嵌套结构体的,而是要声明新的结构体。


/// 钉钉企业内部应用消息(工作通知)消息类型
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, Ord, PartialOrd, Eq, PartialEq, Hash)]
pub enum DingtalkOrgMsgType {
    /// 文本消息
    #[serde(rename = "text")]
    Text,
    /// 图片消息
    #[serde(rename = "image")]
    Image,
    /// 语音消息
    #[serde(rename = "voice")]
    Voice,
    /// 文件消息
    #[serde(rename = "file")]
    File,
    /// 链接消息
    #[serde(rename = "link")]
    Link,
    /// OA消息
    #[serde(rename = "oa")]
    Oa,
    /// Markdown 消息
    #[serde(rename = "markdown")]
    Markdown,
    /// 卡片消息
    #[serde(rename = "action_card")]
    ActionCard,
}

/// 文本消息
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
pub struct DingtalkOrgTextMsg {
    pub msgtype: DingtalkOrgMsgType,
    pub text: DingtalkOrgTextMsgBody,
}

/// 文本消息体
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
pub struct DingtalkOrgTextMsgBody {
    /// 消息内容,建议500字符以内。
    pub content: String,
}

/// 图片消息
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
pub struct DingtalkOrgImageMsg {
    pub msgtype: DingtalkOrgMsgType,
    pub image: DingtalkOrgImageMsgBody,
}

/// 图片消息体
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
pub struct DingtalkOrgImageMsgBody {
    /// 媒体文件mediaid,建议宽600像素 x 400像素,宽高比3 : 2。
    pub media_id: String,
}

/// 语音消息
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
pub struct DingtalkOrgVoiceMsg {
    pub msgtype: DingtalkOrgMsgType,
    pub voice: DingtalkOrgVoiceMsgBody,
}

/// 语音消息体
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
pub struct DingtalkOrgVoiceMsgBody {
    /// 媒体文件ID。
    pub media_id: String,
    /// 正整数,小于60,表示音频时长。
    pub duration: String,
}

/// 文件消息
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
pub struct DingtalkOrgFileMsg {
    pub msgtype: DingtalkOrgMsgType,
    pub file: DingtalkOrgFileMsgBody,
}

/// 文件消息体
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
pub struct DingtalkOrgFileMsgBody {
    /// 媒体文件ID,引用的媒体文件最大10MB。
    pub media_id: String,
}

/// 链接消息
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
pub struct DingtalkOrgLinkMsg {
    pub msgtype: DingtalkOrgMsgType,
    pub link: DingtalkOrgLinkMsgBody,
}

/// 链接消息体
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
pub struct DingtalkOrgLinkMsgBody {
    /// 消息点击链接地址,当发送消息为小程序时支持小程序跳转链接。
    #[serde(rename = "messageUrl")]
    pub message_url: String,
    /// 媒体文件ID
    #[serde(rename = "picUrl")]
    pub pic_url: String,
    /// 消息标题,建议100字符以内。
    pub title: String,
    /// 消息描述,建议500字符以内。
    pub text: String,
}

/// OA 消息
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
pub struct DingtalkOrgOaMsg {
    pub msgtype: DingtalkOrgMsgType,
    pub oa: DingtalkOrgOaMsgBody,
}

/// OA 消息体
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
pub struct DingtalkOrgOaMsgBody {
    /// 消息点击链接地址,当发送消息为小程序时支持小程序跳转链接。
    pub message_url: Option<String>,
    /// PC端点击消息时跳转到的地址。
    pub pc_message_url: Option<String>,
    /// 消息头部内容。
    pub head: DingtalkOrgOaMsgBodyHead,
    /// 消息状态栏,只支持接收者的userid列表,userid最多不能超过5个人。
    pub status_bar: Option<DingtalkOrgOaMsgBodyStatusBar>,
    /// 消息体 body
    pub body: DingtalkOrgOaMsgBodyBody,
}

/// OA 消息体 head
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
pub struct DingtalkOrgOaMsgBodyHead {
    /// 消息头部的背景颜色。长度限制为8个英文字符,其中前2为表示透明度,后6位表示颜色值。不要添加0x。
    pub bgcolor: String,
    /// 消息的头部标题。
    pub text: String,
}

/// OA 消息体 status_bar
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
pub struct DingtalkOrgOaMsgBodyStatusBar {
    /// 状态栏文案。
    pub status_value: String,
    /// 状态栏背景色,默认为黑色,推荐0xFF加六位颜色值。
    pub status_bg: String,
}

/// OA 消息体 body
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
pub struct DingtalkOrgOaMsgBodyBody {
    /// 消息体的标题,建议50个字符以内。
    pub title: Option<String>,
    /// 消息体的表单,最多显示6个,超过会被隐藏。
    pub form: Option<Vec<DingtalkOrgOaMsgBodyBodyFormItem>>,
    /// 单行富文本信息。
    pub rich: Option<DingtalkOrgOaMsgBodyBodyRich>,
    /// 消息体的内容,最多显示3行。
    pub content: Option<String>,
    /// 消息体中的图片,支持图片资源@mediaId。建议宽600像素 x 400像素,宽高比3 : 2。
    pub image: Option<String>,
    /// 自定义的附件数目。此数字仅供显示,钉钉不作验证。
    pub file_count: Option<String>,
    /// 自定义的作者名字。
    pub author: Option<String>,
}


/// OA 消息体 body.form
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
pub struct DingtalkOrgOaMsgBodyBodyFormItem {
    /// 消息体的关键字。
    pub key: String,
    /// 消息体的关键字对应的值。
    pub value: String,
}

/// OA 消息体 body.rich
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
pub struct DingtalkOrgOaMsgBodyBodyRich {
    /// 单行富文本信息的数目。
    pub num: String,
    /// 单行富文本信息的单位。
    pub unit: String,
}

/// Markdown 消息
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
pub struct DingtalkOrgMarkdownMsg {
    pub msgtype: DingtalkOrgMsgType,
    pub markdown: DingtalkOrgMarkdownMsgBody,
}

/// Markdown 消息体
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
pub struct DingtalkOrgMarkdownMsgBody {
    /// 首屏会话透出的展示内容。注意:消息列表不显示,如需在消息列表显示请组装到text里面
    pub title: String,
    /// markdown格式的消息,最大不超过5000字符。
    pub text: String,
}

/// 卡片消息
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
pub struct DingtalkOrgActionCardMsg {
    pub msgtype: DingtalkOrgMsgType,
    pub action_card: DingtalkOrgActionCardMsgBody,
}

/// 卡片消息体
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
pub struct DingtalkOrgActionCardMsgBody {
    /// 透出到会话列表和通知的文案。注意:消息列表不显示,如需在消息列表显示请组装到markdown里面
    pub title: Option<String>,
    /// 消息内容,支持markdown,语法参考标准markdown语法。建议1000个字符以内。
    pub markdown: String,
    /// 使用整体跳转ActionCard样式时的标题。必须与single_url同时设置,最长20个字符。
    pub single_title: Option<String>,
    /// 消息点击链接地址,当发送消息为小程序时支持小程序跳转链接,最长500个字符。
    pub single_url: Option<String>,
    /// 使用独立跳转ActionCard样式时的按钮排列方式:0 竖直排列 1 横向排列,必须与btn_json_list同时设置。
    pub btn_orientation: Option<String>,
    /// 使用独立跳转ActionCard样式时的按钮列表;必须与btn_orientation同时设置,且长度不超过1000字符。
    pub btn_json_list: Option<Vec<DingtalkOrgActionCardMsgBodyBtnJsonListItem>>

}

/// 卡片消息体 btn_json_list
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
pub struct DingtalkOrgActionCardMsgBodyBtnJsonListItem {
    /// 使用独立跳转ActionCard样式时的按钮的标题,最长20个字符。
    pub title: String,
    /// 使用独立跳转ActionCard样式时的跳转链接,最长700个字符。
    pub action_url: String,
}

这样,我们第一版的钉钉企业内部应用的消息通知的结构体就全部声明完成了,而后要在此基础上做任何业务逻辑,只要结构体声明没啥问题,写起代码来基本不用太再去操心里面的数据结构的细节问题,IDE 编辑器能给出来提示的,肯定就是正常可用的。

由于这里声明了八个大结构体,在函数参数的使用时候,一个方案是用泛型 T 来处理。但是这个方案有个问题,其实八种类型就是已经限定了范围的,这时候使用泛型其实是不太符合业务语义的。例如这时候去自动生成 OpenAPI 或 Swagger 的文档,就无法清晰地表达出接口的要求。

另外一个方案可能是用我们上一篇提到的多种结构的枚举类型进行简单包装一层,也是可以用的,这样就可以不用泛型来处理了。这种方案,就很好地解决了业务语义的问题,自动生成的接口文档也是清晰明确的。

不过,这里依然还有一个小问题:虽然这八个结构体里面都声明了 msgtype 为枚举类型,能限定传入的参数是八个枚举当中的任意一个,但是如果我这时候给 DingtalkOrgImageMsgmsgtype 传入一个 DingtalkOrgMsgType::Text 是允许的么?

发现问题了,这种声明方式,没有办法严格限制传入的 msgtype 的枚举值是对应类型所要求的值。

那么怎么解决?

首先想到的肯定是去写校验方法,这在 Python 的场景里面很常见,拿到任何的参数,虽然都能正常初始化为类实例或者都能正常解析为一个字典,但是然后要对这个数据进行各种校验逻辑,通过了所有校验环节,然后才能进入后续的业务流程。

这个思路在 Golang 里面依然如此,以及还得考虑默认的初始值对值校验的影响。

而在 Rust 这里,其实有更清晰和简单的方案:枚举声明,与方案二的枚举不同的是,这次的枚举声明有那么些不一样,具体请看代码,并与上一版的进行对比查看:


/// 钉钉企业内部应用消息(工作通知)消息类型
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, Ord, PartialOrd, Eq, PartialEq, Hash)]
pub enum DingtalkOrgMsgType {
    /// 文本消息
    #[serde(rename = "text")]
    Text,
    /// 图片消息
    #[serde(rename = "image")]
    Image,
    /// 语音消息
    #[serde(rename = "voice")]
    Voice,
    /// 文件消息
    #[serde(rename = "file")]
    File,
    /// 链接消息
    #[serde(rename = "link")]
    Link,
    /// OA消息
    #[serde(rename = "oa")]
    Oa,
    /// Markdown 消息
    #[serde(rename = "markdown")]
    Markdown,
    /// 卡片消息
    #[serde(rename = "action_card")]
    ActionCard,
}

/// 钉钉企业内部应用消息(工作通知)
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
#[serde(tag = "msgtype")]
pub enum DingtalkOrgMsg {
    /// 文本消息
    #[serde(rename = "text")]
    Text(DingtalkOrgTextMsg),
    /// 图片消息
    #[serde(rename = "image")]
    Image(DingtalkOrgImageMsg),
    /// 语音消息
    #[serde(rename = "voice")]
    Voice(DingtalkOrgVoiceMsg),
    /// 文件消息
    #[serde(rename = "file")]
    File(DingtalkOrgFileMsg),
    /// 链接消息
    #[serde(rename = "link")]
    Link(DingtalkOrgLinkMsg),
    /// OA消息
    #[serde(rename = "oa")]
    Oa(DingtalkOrgOaMsg),
    /// Markdown 消息
    #[serde(rename = "markdown")]
    Markdown(DingtalkOrgMarkdownMsg),
    /// 卡片消息
    #[serde(rename = "action_card")]
    ActionCard(DingtalkOrgActionCardMsg),
}

/// 文本消息
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
pub struct DingtalkOrgTextMsg {
    pub text: DingtalkOrgTextMsgBody,
}

/// 文本消息体
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
pub struct DingtalkOrgTextMsgBody {
    /// 消息内容,建议500字符以内。
    pub content: String,
}

/// 图片消息
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
pub struct DingtalkOrgImageMsg {
    pub image: DingtalkOrgImageMsgBody,
}

/// 图片消息体
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
pub struct DingtalkOrgImageMsgBody {
    /// 媒体文件mediaid,建议宽600像素 x 400像素,宽高比3 : 2。
    pub media_id: String,
}

/// 语音消息
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
pub struct DingtalkOrgVoiceMsg {
    pub voice: DingtalkOrgVoiceMsgBody,
}

/// 语音消息体
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
pub struct DingtalkOrgVoiceMsgBody {
    /// 媒体文件ID。
    pub media_id: String,
    /// 正整数,小于60,表示音频时长。
    pub duration: String,
}

/// 文件消息
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
pub struct DingtalkOrgFileMsg {
    pub file: DingtalkOrgFileMsgBody,
}

/// 文件消息体
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
pub struct DingtalkOrgFileMsgBody {
    /// 媒体文件ID,引用的媒体文件最大10MB。
    pub media_id: String,
}

/// 链接消息
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
pub struct DingtalkOrgLinkMsg {
    pub link: DingtalkOrgLinkMsgBody,
}

/// 链接消息体
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
pub struct DingtalkOrgLinkMsgBody {
    /// 消息点击链接地址,当发送消息为小程序时支持小程序跳转链接。
    #[serde(rename = "messageUrl")]
    pub message_url: String,
    /// 媒体文件ID
    #[serde(rename = "picUrl")]
    pub pic_url: String,
    /// 消息标题,建议100字符以内。
    pub title: String,
    /// 消息描述,建议500字符以内。
    pub text: String,
}

/// OA 消息
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
pub struct DingtalkOrgOaMsg {
    pub oa: DingtalkOrgOaMsgBody,
}

/// OA 消息体
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
pub struct DingtalkOrgOaMsgBody {
    /// 消息点击链接地址,当发送消息为小程序时支持小程序跳转链接。
    pub message_url: Option<String>,
    /// PC端点击消息时跳转到的地址。
    pub pc_message_url: Option<String>,
    /// 消息头部内容。
    pub head: DingtalkOrgOaMsgBodyHead,
    /// 消息状态栏,只支持接收者的userid列表,userid最多不能超过5个人。
    pub status_bar: Option<DingtalkOrgOaMsgBodyStatusBar>,
    /// 消息体 body
    pub body: DingtalkOrgOaMsgBodyBody,
}

/// OA 消息体 head
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
pub struct DingtalkOrgOaMsgBodyHead {
    /// 消息头部的背景颜色。长度限制为8个英文字符,其中前2为表示透明度,后6位表示颜色值。不要添加0x。
    pub bgcolor: String,
    /// 消息的头部标题。
    pub text: String,
}

/// OA 消息体 status_bar
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
pub struct DingtalkOrgOaMsgBodyStatusBar {
    /// 状态栏文案。
    pub status_value: String,
    /// 状态栏背景色,默认为黑色,推荐0xFF加六位颜色值。
    pub status_bg: String,
}

/// OA 消息体 body
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
pub struct DingtalkOrgOaMsgBodyBody {
    /// 消息体的标题,建议50个字符以内。
    pub title: Option<String>,
    /// 消息体的表单,最多显示6个,超过会被隐藏。
    pub form: Option<Vec<DingtalkOrgOaMsgBodyBodyFormItem>>,
    /// 单行富文本信息。
    pub rich: Option<DingtalkOrgOaMsgBodyBodyRich>,
    /// 消息体的内容,最多显示3行。
    pub content: Option<String>,
    /// 消息体中的图片,支持图片资源@mediaId。建议宽600像素 x 400像素,宽高比3 : 2。
    pub image: Option<String>,
    /// 自定义的附件数目。此数字仅供显示,钉钉不作验证。
    pub file_count: Option<String>,
    /// 自定义的作者名字。
    pub author: Option<String>,
}


/// OA 消息体 body.form
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
pub struct DingtalkOrgOaMsgBodyBodyFormItem {
    /// 消息体的关键字。
    pub key: String,
    /// 消息体的关键字对应的值。
    pub value: String,
}

/// OA 消息体 body.rich
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
pub struct DingtalkOrgOaMsgBodyBodyRich {
    /// 单行富文本信息的数目。
    pub num: String,
    /// 单行富文本信息的单位。
    pub unit: String,
}

/// Markdown 消息
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
pub struct DingtalkOrgMarkdownMsg {
    pub markdown: DingtalkOrgMarkdownMsgBody,
}

/// Markdown 消息体
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
pub struct DingtalkOrgMarkdownMsgBody {
    /// 首屏会话透出的展示内容。注意:消息列表不显示,如需在消息列表显示请组装到text里面
    pub title: String,
    /// markdown格式的消息,最大不超过5000字符。
    pub text: String,
}

/// 卡片消息
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
pub struct DingtalkOrgActionCardMsg {
    pub action_card: DingtalkOrgActionCardMsgBody,
}

/// 卡片消息体
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
pub struct DingtalkOrgActionCardMsgBody {
    /// 透出到会话列表和通知的文案。注意:消息列表不显示,如需在消息列表显示请组装到markdown里面
    pub title: Option<String>,
    /// 消息内容,支持markdown,语法参考标准markdown语法。建议1000个字符以内。
    pub markdown: String,
    /// 使用整体跳转ActionCard样式时的标题。必须与single_url同时设置,最长20个字符。
    pub single_title: Option<String>,
    /// 消息点击链接地址,当发送消息为小程序时支持小程序跳转链接,最长500个字符。
    pub single_url: Option<String>,
    /// 使用独立跳转ActionCard样式时的按钮排列方式:0 竖直排列 1 横向排列,必须与btn_json_list同时设置。
    pub btn_orientation: Option<String>,
    /// 使用独立跳转ActionCard样式时的按钮列表;必须与btn_orientation同时设置,且长度不超过1000字符。
    pub btn_json_list: Option<Vec<DingtalkOrgActionCardMsgBodyBtnJsonListItem>>

}

/// 卡片消息体 btn_json_list
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
pub struct DingtalkOrgActionCardMsgBodyBtnJsonListItem {
    /// 使用独立跳转ActionCard样式时的按钮的标题,最长20个字符。
    pub title: String,
    /// 使用独立跳转ActionCard样式时的跳转链接,最长700个字符。
    pub action_url: String,
}

粗看上去 DingtalkOrgMsg 非常像是方案二的声明一个综合性的多结构枚举,但是你发现,msgtype 字段从每个消息结构体中抽走了,然后使用 #[serde(tag = "msgtype")] 进行统一地声明,再结合 #[serde(rename = "text")] 这种声明方式,它就能完美地与文档中的 Json 结构进行一一对应。

同时在自动生成 OpenAPI 或 Swagger 文档的时候,它的语义是 OneOf 而不是方案二中的 AnyOf 了,完美符合业务语义。

以及此时,DingtalkOrgMsgType 这个枚举的存在价值可能不大了,实际应用中可能就清理掉了,看具体情况吧。

不知道你看明白了么?

总结一下知识点:

  • 对于可枚举的多种结构,可以使用 enum 进行综合性声明为一个枚举结构
  • 对于每种结构中可能存在有一个共同字段,可以使用 #[serde(tag = "msgtype")] 进行表示
  • 对于每种子结构,有一个动态的键需要声明,则可以用 #[serde(rename = "text")] 进行表示
  • 对于每个子级,无论有多深,都可以层层分解,最后得到一个个逻辑非常清晰和层次分明的结构体,这在业务流程中逐级组合数据时,有非常好的代码正确性和质量保证。