Skip to main content

wx_rust_open/bean/message/
wx_open_xml_message.rs

1//! 第三方平台推送消息(xml 格式)。
2//!
3//! 对应 Java `me.chanjar.weixin.open.bean.message.WxOpenXmlMessage`。Java 用
4//! XStream 反射映射(`@XStreamAlias` 元素名 + CDATA);Rust 以 quick-xml
5//! serde 派生(`quick_xml::de::from_str`)表达同一线格式:根元素 `xml`、
6//! 元素名精确映射、缺失元素为 `None`/`Default`。
7//!
8//! 解析入口(对应 Java 同名静态方法):
9//! - [`WxOpenXmlMessage::from_xml`]:明文 XML 解析;
10//! - [`WxOpenXmlMessage::from_encrypted_xml`]:加密回调解密(复用 Wave 0
11//!   的 [`crate::util::crypto::WxOpenCryptUtils`],SHA1 验签 + AES-256-CBC)
12//!   后解析,并回填原始明文到 `context`;
13//! - [`WxOpenXmlMessage::from_encrypted_mp_xml`]:解密公众号消息回调
14//!   (ADAPTED,见方法文档)。
15//!
16//! 出站(被动回复)侧:第三方平台回复公众号消息与 mp 模块同线格式,
17//! 加密打包入口为 [`wx_mp_out_xml_message_to_encrypted_xml`]
18//! (对应 Java `wxMpOutXmlMessageToEncryptedXml`,ADAPTED 以明文 XML 入参)。
19
20use crate::config::WxOpenConfigStorage;
21use crate::util::crypto::WxOpenCryptUtils;
22
23/// 第三方平台推送消息。
24///
25/// 元素名与 Java `@XStreamAlias` 一一对应;`context` 为原始明文
26/// (`from_encrypted_xml` 回填,非 XML 元素)。
27#[derive(Debug, Clone, Default, PartialEq, serde::Deserialize)]
28#[serde(rename = "xml")]
29pub struct WxOpenXmlMessage {
30    /// 第三方平台的APPID。
31    #[serde(rename = "AppId", default)]
32    pub app_id: Option<String>,
33    /// 消息创建时间。
34    #[serde(rename = "CreateTime", default)]
35    pub create_time: Option<i64>,
36    /// 消息类型(verify_ticket / authorized / unauthorized / updateauthorized /
37    /// notify_third_fasteregister / notify_3rd_wxa_auth 等)。
38    #[serde(rename = "InfoType", default)]
39    pub info_type: Option<String>,
40    /// 第三方平台 component_verify_ticket(票据,5 分钟推送一次)。
41    #[serde(rename = "ComponentVerifyTicket", default)]
42    pub component_verify_ticket: Option<String>,
43    /// 授权方 appid(authorized/updateauthorized/unauthorized 事件)。
44    #[serde(rename = "AuthorizerAppid", default)]
45    pub authorizer_appid: Option<String>,
46    /// 授权码(authorized 事件,换取授权信息)。
47    #[serde(rename = "AuthorizationCode", default)]
48    pub authorization_code: Option<String>,
49    /// 授权码过期时间(秒)。
50    #[serde(rename = "AuthorizationCodeExpiredTime", default)]
51    pub authorization_code_expired_time: Option<i64>,
52    /// 预授权码。
53    #[serde(rename = "PreAuthCode", default)]
54    pub pre_auth_code: Option<String>,
55    /// 子平台APPID(公众号/小程序的APPID)——快速创建小程序、小程序认证中。
56    #[serde(rename = "appid", default)]
57    pub sub_app_id: Option<String>,
58    /// 快速创建小程序接口推送的状态(对应 Java `status`)。
59    #[serde(rename = "status", default)]
60    pub status: Option<i32>,
61    /// 快速创建小程序的 auth_code。
62    #[serde(rename = "auth_code", default)]
63    pub auth_code: Option<String>,
64    /// 快速创建小程序推送的消息。
65    #[serde(rename = "msg", default)]
66    pub msg: Option<String>,
67    /// 快速创建小程序推送的信息。
68    #[serde(rename = "info", default)]
69    pub info: Info,
70    /// 小程序认证(年审)任务ID。
71    #[serde(rename = "taskid", default)]
72    pub task_id: Option<String>,
73    /// 认证任务状态:0初始 1超24小时 2用户拒绝 3用户同意 4发起人脸
74    /// 5人脸失败 6人脸ok 7人脸认证后手机验证码 8手机验证失败 9手机验证成功
75    /// 11创建审核单失败 12创建审核单成功 14验证失败 15等待支付。
76    #[serde(rename = "task_status", default)]
77    pub task_status: Option<i32>,
78    /// 审核单状态,创建审核单成功后有效:0审核单不存在 1待支付 2审核中
79    /// 3打回重填 4认证通过 5认证最终失败(不能再修改)。
80    #[serde(rename = "apply_status", default)]
81    pub apply_status: Option<i32>,
82    /// 审核消息或失败原因。
83    #[serde(rename = "message", default)]
84    pub message: Option<String>,
85    /// 审核提供商分配信息。
86    #[serde(rename = "dispatch_info", default)]
87    pub dispatch_info: Option<DispatchInfo>,
88    /// 小程序认证(年审)即将到期通知的过期时间戳(秒数)。
89    #[serde(rename = "expired", default)]
90    pub expired: Option<i64>,
91    /// 人脸核验任务id(infoType=notify_icpfiling_verify_result)。
92    #[serde(rename = "task_id", default)]
93    pub icp_verify_task_id: Option<String>,
94    /// 小程序唯一id。
95    #[serde(rename = "verify_appid", default)]
96    pub verify_app_id: Option<String>,
97    /// 人脸核验结果:2-核验失败;3-核验成功。
98    #[serde(rename = "result", default)]
99    pub result: Option<i32>,
100    /// 发起时 along_with_auth 填 true 时有效:9. 认证短信核验通过。
101    #[serde(rename = "along_with_auth_result", default)]
102    pub along_with_auth_result: Option<i32>,
103    /// 小程序唯一id(备案审核被驳回/通过事件 notify_apply_icpfiling_result)。
104    #[serde(rename = "authorizer_appid", default)]
105    pub beian_authorizer_app_id: Option<String>,
106    /// 备案状态,参考“获取小程序备案状态及驳回原因”接口的备案状态枚举。
107    #[serde(rename = "beian_status", default)]
108    pub beian_status: Option<i32>,
109    /// 小程序认证及备案任务流程id(notify_3rd_wxa_auth_and_icp)。
110    #[serde(rename = "procedure_id", default)]
111    pub procedure_id: Option<String>,
112    /// 任务流程状态:9手机验证成功 15等待支付 16支付成功 17认证审核中
113    /// 18认证审核驳回 19认证审核通过 20认证最终失败 21创建备案审核单失败
114    /// 22备案平台审核中 23备案平台审核驳回 24备案管局审核中 25管局审核驳回
115    /// 26认证及备案完成 27流程已过期 28流程已终止 29备案已撤回。
116    #[serde(rename = "procedure_status", default)]
117    pub procedure_status: Option<i32>,
118    /// 原始通知内容(由 [`from_encrypted_xml`](Self::from_encrypted_xml) 回填,
119    /// 非 XML 元素)。
120    #[serde(default, skip)]
121    pub context: Option<String>,
122}
123
124/// 快速创建小程序推送的信息(对应 Java 内嵌类
125/// `WxOpenXmlMessage.Info`,`@XStreamAlias("info")`)。
126#[derive(Debug, Clone, Default, PartialEq, serde::Deserialize)]
127pub struct Info {
128    #[serde(rename = "name", default)]
129    pub name: Option<String>,
130    #[serde(rename = "code", default)]
131    pub code: Option<String>,
132    #[serde(rename = "code_type", default)]
133    pub code_type: Option<i32>,
134    #[serde(rename = "legal_persona_wechat", default)]
135    pub legal_persona_wechat: Option<String>,
136    #[serde(rename = "legal_persona_name", default)]
137    pub legal_persona_name: Option<String>,
138    #[serde(rename = "component_phone", default)]
139    pub component_phone: Option<String>,
140    /// 创建个人小程序审核通知数据。
141    #[serde(rename = "wxuser", default)]
142    pub wxuser: Option<String>,
143    #[serde(rename = "idname", default)]
144    pub idname: Option<String>,
145    /// 创建试用小程序成功/失败的通知数据。
146    #[serde(rename = "unique_id", default)]
147    pub unique_id: Option<String>,
148}
149
150/// 审核提供商分配信息(对应 Java 内嵌类 `WxOpenXmlMessage.DispatchInfo`)。
151#[derive(Debug, Clone, Default, PartialEq, serde::Deserialize)]
152pub struct DispatchInfo {
153    /// 提供商,如:上海倍通企业信用征信有限公司。
154    #[serde(rename = "provider", default)]
155    pub provider: Option<String>,
156    /// 联系方式,如:咨询电话:0411-84947888。
157    #[serde(rename = "contact", default)]
158    pub contact: Option<String>,
159    /// 派遣时间戳(秒),如:1704952913。
160    #[serde(rename = "dispatch_time", default)]
161    pub dispatch_time: Option<i64>,
162}
163
164impl WxOpenXmlMessage {
165    /// 从明文 xml 解析消息。
166    ///
167    /// 对应 Java `fromXml(String)`:先修正微信变态的消息内容格式
168    /// (`</PicList><PicList>` 相邻闭合/开启,open 消息中为无操作),
169    /// 再以 quick-xml serde 按 `@XStreamAlias` 元素名映射字段。
170    pub fn from_xml(xml: &str) -> Result<Self, String> {
171        let xml = xml.replace("</PicList><PicList>", "");
172        quick_xml::de::from_str(&xml).map_err(|e| format!("WxOpenXmlMessage 解析失败: {e}"))
173    }
174
175    /// 从加密字符串转换(第三方平台回调消息解密入口)。
176    ///
177    /// 对应 Java `fromEncryptedXml(String, WxOpenConfigStorage, String, String,
178    /// String)`:`WxOpenCryptUtils` 验签并 AES 解密(Wave 0 已实现),
179    /// 解密为空时返回错误,解析后回填原始明文到 `context`。
180    pub fn from_encrypted_xml(
181        encrypted_xml: &str,
182        config: &dyn WxOpenConfigStorage,
183        timestamp: &str,
184        nonce: &str,
185        msg_signature: &str,
186    ) -> Result<Self, String> {
187        let crypt_util = WxOpenCryptUtils::new(config)?;
188        let plain_text = crypt_util.decrypt_xml(msg_signature, timestamp, nonce, encrypted_xml)?;
189        if plain_text.trim().is_empty() {
190            return Err("解密后的xml消息内容为空,请检查加密参数是否正确".to_string());
191        }
192        let mut message = Self::from_xml(&plain_text)?;
193        message.context = Some(plain_text);
194        Ok(message)
195    }
196
197    /// 解密第三方平台推送的公众号消息回调。
198    ///
199    /// 对应 Java `fromEncryptedMpXml(...)`(返回 `WxMpXmlMessage`)。
200    ///
201    /// ADAPTED:wx-rust-open 尚未依赖 wx-rust-mp,此处返回解密后的明文
202    /// XML 字符串,由调用方交给 `wx_rust_mp::bean::message::WxMpXmlMessage::
203    /// from_xml` 解析;Wave 2 引入跨 crate 依赖后换为返回 `WxMpXmlMessage`。
204    pub fn from_encrypted_mp_xml(
205        encrypted_xml: &str,
206        config: &dyn WxOpenConfigStorage,
207        timestamp: &str,
208        nonce: &str,
209        msg_signature: &str,
210    ) -> Result<String, String> {
211        let crypt_util = WxOpenCryptUtils::new(config)?;
212        crypt_util.decrypt_xml(msg_signature, timestamp, nonce, encrypted_xml)
213    }
214}
215
216/// 将待回复的公众号消息加密打包为回调 xml。
217///
218/// 对应 Java `wxMpOutXmlMessageToEncryptedXml(WxMpXmlOutMessage,
219/// WxOpenConfigStorage)`:Java 先 `message.toXml()` 得明文 XML 再加密;
220/// ADAPTED:Rust 侧以明文 XML 字符串入参(由调用方用
221/// `wx_rust_mp` 的 `WxMpXmlOutMessage::to_xml` 生成),加密流程
222/// (`WxOpenCryptUtils::encrypt`)与 Java 逐字一致。
223pub fn wx_mp_out_xml_message_to_encrypted_xml(
224    plain_xml: &str,
225    config: &dyn WxOpenConfigStorage,
226) -> Result<String, String> {
227    let crypt_util = WxOpenCryptUtils::new(config)?;
228    crypt_util.encrypt(plain_xml)
229}
230
231#[cfg(test)]
232mod tests {
233    use super::*;
234
235    /// verify_ticket 推送(Java 测试 golden 线格式)。
236    #[test]
237    fn parse_verify_ticket() {
238        let xml = r#"<xml>
239  <AppId><![CDATA[wxb1234567890abcdef]]></AppId>
240  <CreateTime>1413192605</CreateTime>
241  <InfoType><![CDATA[verify_ticket]]></InfoType>
242  <ComponentVerifyTicket><![CDATA[ticket@@@6iJtQHC1]]></ComponentVerifyTicket>
243</xml>"#;
244        let m = WxOpenXmlMessage::from_xml(xml).unwrap();
245        assert_eq!(m.app_id.as_deref(), Some("wxb1234567890abcdef"));
246        assert_eq!(m.create_time, Some(1413192605));
247        assert_eq!(m.info_type.as_deref(), Some("verify_ticket"));
248        assert_eq!(
249            m.component_verify_ticket.as_deref(),
250            Some("ticket@@@6iJtQHC1")
251        );
252        assert_eq!(m.context, None);
253    }
254
255    /// 快速创建小程序(notify_third_fasteregister)推送:info 嵌套 + 小写键。
256    #[test]
257    fn parse_fasteregister() {
258        let xml = r#"<xml>
259<AppId><![CDATA[wx1234567890abcdef]]></AppId>
260<CreateTime>1492747804</CreateTime>
261<InfoType><![CDATA[notify_third_fasteregister]]></InfoType>
262<appid><![CDATA[wx1234567890abcdef]]></appid>
263<status>0</status>
264<auth_code><![CDATA[queryauthcode@@@123]]></auth_code>
265<msg><![CDATA[]]></msg>
266<info><name><![CDATA[test]]></name><code><![CDATA[123456]]></code><code_type>1</code_type><legal_persona_wechat><![CDATA[wx123]]></legal_persona_wechat><legal_persona_name><![CDATA[张三]]></legal_persona_name><component_phone><![CDATA[13800000000]]></component_phone></info>
267</xml>"#;
268        let m = WxOpenXmlMessage::from_xml(xml).unwrap();
269        assert_eq!(m.info_type.as_deref(), Some("notify_third_fasteregister"));
270        assert_eq!(m.sub_app_id.as_deref(), Some("wx1234567890abcdef"));
271        assert_eq!(m.status, Some(0));
272        assert_eq!(m.auth_code.as_deref(), Some("queryauthcode@@@123"));
273        assert_eq!(m.info.name.as_deref(), Some("test"));
274        assert_eq!(m.info.code.as_deref(), Some("123456"));
275        assert_eq!(m.info.code_type, Some(1));
276        assert_eq!(m.info.legal_persona_name.as_deref(), Some("张三"));
277    }
278}