From 7606f08d8ef06ab5f03e76f06eb7555428ace331 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BE=8D=E6=99=A8?= Date: Fri, 26 Aug 2022 19:02:25 +0800 Subject: [PATCH] feat(develop): Add some features 1. Add orc/wifi for wechat work 2. Fixed some bugs. --- Cargo.toml | 4 +- src/wechat/cp/api/external_contact.rs | 5 - src/wechat/miniapp/mod.rs | 2 - src/wechat/mp/api/customservice.rs | 6 +- src/wechat/mp/api/media.rs | 2 +- src/wechat/mp/api/menu.rs | 20 +- src/wechat/mp/api/mod.rs | 4 + src/wechat/mp/api/oauth2.rs | 2 +- src/wechat/mp/api/ocr.rs | 368 ++++++++++++++++++++++++++ src/wechat/mp/api/qrcode.rs | 16 +- src/wechat/mp/api/template_msg.rs | 8 +- src/wechat/mp/api/user.rs | 4 +- src/wechat/mp/api/wifi.rs | 115 ++++++++ src/wechat/mp/constants.rs | 9 + src/wechat/mp/method.rs | 98 +++++-- src/wechat/mp/mod.rs | 163 +++++++++++- 16 files changed, 762 insertions(+), 64 deletions(-) create mode 100644 src/wechat/mp/api/ocr.rs create mode 100644 src/wechat/mp/api/wifi.rs diff --git a/Cargo.toml b/Cargo.toml index ce98cd2..c2ae9db 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,9 +1,9 @@ [package] name = "labrador" -version = "0.1.6" +version = "0.1.7" authors = ["mrpan <1049058427@qq.com>"] edition = "2018" -description = "Labrador - Mini thidpart client for rust." +description = "Labrador - Mini thirdpart client for rust." readme = "README.md" keywords = ["alipay", "wechat", "jd", "taobao", "pdd"] categories = ["api-bindings"] diff --git a/src/wechat/cp/api/external_contact.rs b/src/wechat/cp/api/external_contact.rs index d0c3ab6..f624799 100644 --- a/src/wechat/cp/api/external_contact.rs +++ b/src/wechat/cp/api/external_contact.rs @@ -395,15 +395,12 @@ impl<'a, T: SessionStore> WechatCpExternalContact<'a, T> { /// ///
     /// 注意::
-    /// 

/// 群主离职了的客户群,才可继承 /// 继承给的新群主,必须是配置了客户联系功能的成员 /// 继承给的新群主,必须有设置实名 /// 继承给的新群主,必须有激活企业微信 /// 同一个人的群,限制每天最多分配300个给新群主 - ///

/// 权限说明: - ///

/// 企业需要使用“客户联系”secret或配置到“可调用应用”列表中的自建应用secret所获取的accesstoken来调用(accesstoken如何获取?)。 /// 第三方应用需拥有“企业客户权限->客户联系->分配离职成员的客户群”权限 /// 对于第三方/自建应用,群主必须在应用的可见范围。 @@ -468,9 +465,7 @@ impl<'a, T: SessionStore> WechatCpExternalContact<'a, T> { /// 企业可通过此接口添加企业群发消息的任务并通知客服人员发送给相关客户或客户群。(注:企业微信终端需升级到2.7.5版本及以上) /// 注意:调用该接口并不会直接发送消息给客户/客户群,需要相关的客服人员操作以后才会实际发送(客服人员的企业微信需要升级到2.7.5及以上版本) /// 同一个企业每个自然月内仅可针对一个客户/客户群发送4条消息,超过限制的用户将会被忽略。 - ///

/// 请求方式: POST(HTTP) - ///

/// 请求地址:地址 ///

/// 文档地址:地址 diff --git a/src/wechat/miniapp/mod.rs b/src/wechat/miniapp/mod.rs index 70cf5db..816fc25 100644 --- a/src/wechat/miniapp/mod.rs +++ b/src/wechat/miniapp/mod.rs @@ -81,8 +81,6 @@ impl WeChatMaClient { let expires_in = res.expires_in; // 预留200秒的时间 let expires_at = current_timestamp() + expires_in - 200; - let token_key = format!("{}_access_token", self.appid); - let expires_key = format!("{}_expires_at", self.appid); session.set(&token_key, token.to_owned(), Some(expires_in as usize)); session.set(&expires_key, expires_at, Some(expires_in as usize)); Ok(token) diff --git a/src/wechat/mp/api/customservice.rs b/src/wechat/mp/api/customservice.rs index f89f88b..553063f 100644 --- a/src/wechat/mp/api/customservice.rs +++ b/src/wechat/mp/api/customservice.rs @@ -171,7 +171,7 @@ impl<'a, T: SessionStore> WeChatMpCustomService<'a, T> { //---------------------------------------------------------------------------------------------------------------------------- -#[derive(Debug, Clone, PartialEq, Eq,Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct KFAccount { pub id: String, pub nick: String, @@ -179,7 +179,7 @@ pub struct KFAccount { pub avatar: String, } -#[derive(Debug, Clone, PartialEq, Eq,Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct OnlineKFAccount { pub id: String, pub account: String, @@ -280,7 +280,7 @@ impl SendImageRequest { } -#[derive(Debug, Clone, PartialEq, Eq,Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct SendTextRequest { openid: String, account: Option, diff --git a/src/wechat/mp/api/media.rs b/src/wechat/mp/api/media.rs index 3f4dde6..3d96629 100644 --- a/src/wechat/mp/api/media.rs +++ b/src/wechat/mp/api/media.rs @@ -171,7 +171,7 @@ impl<'a, T: SessionStore> WechatMpMedia<'a, T> { /// 3、素材的格式大小等要求与公众平台官网一致。具体是,图片大小不超过2M,支持bmp/png/jpeg/jpg/gif格式,语音大小不超过5M,长度不超过60秒,支持mp3/wma/wav/amr格式 /// 4、调用该接口需https协议 ///

- pub async fn upload_material(&self, media_type: &str, req: WechatMpMaterialRequest) -> LabradorResult { + pub async fn upload_material(&self, req: WechatMpMaterialRequest) -> LabradorResult { let v = self.client.execute::(req).await?.json::()?; WechatCommonResponse::parse::(v) } diff --git a/src/wechat/mp/api/menu.rs b/src/wechat/mp/api/menu.rs index 8920468..8f5afab 100644 --- a/src/wechat/mp/api/menu.rs +++ b/src/wechat/mp/api/menu.rs @@ -115,14 +115,14 @@ impl<'a, T: SessionStore> WeChatMpMenu<'a, T> { //---------------------------------------------------------------------------------------------------------------------------- -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct MenuButtonsRequest { /// 一级菜单数组,个数应为1~3个 pub button: Vec, } -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct MenuButton { #[serde(rename = "type")] pub button_type: String, @@ -146,7 +146,7 @@ pub struct MenuButton { } -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct SelfMenuInfoResponse { /// 菜单是否开启,0代表未开启,1代表开启 pub is_menu_open: Option, @@ -154,14 +154,14 @@ pub struct SelfMenuInfoResponse { pub selfmenu_info: Option, } -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct SelfMenuInfo { /// 菜单是否开启,0代表未开启,1代表开启 pub button: Option>, } -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct SelfMenuButton { #[serde(rename = "type")] pub button_type: Option, @@ -186,17 +186,17 @@ pub struct SelfMenuButton { } -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct SelfMenuSubButton { pub list: Option>, } -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct SelfMenuNewsButton { pub list: Option>, } -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct SelfMenuNewsInfo { /// 图文消息的标题 pub title: Option, @@ -215,14 +215,14 @@ pub struct SelfMenuNewsInfo { } -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct MenuButtonResponse { pub menu: Option, pub conditionalmenu: Option, } -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct MenuButtonsInner { /// 一级菜单数组,个数应为1~3个 pub button: Option>, diff --git a/src/wechat/mp/api/mod.rs b/src/wechat/mp/api/mod.rs index 94e762b..6712175 100644 --- a/src/wechat/mp/api/mod.rs +++ b/src/wechat/mp/api/mod.rs @@ -6,6 +6,8 @@ mod oauth2; mod media; mod template_msg; mod subscribe_msg; +mod wifi; +mod ocr; pub use self::oauth2::*; pub use self::qrcode::*; @@ -15,5 +17,7 @@ pub use self::template_msg::*; pub use self::subscribe_msg::*; pub use self::media::*; pub use self::menu::*; +pub use self::wifi::*; +pub use self::ocr::*; diff --git a/src/wechat/mp/api/oauth2.rs b/src/wechat/mp/api/oauth2.rs index e4dec48..606239b 100644 --- a/src/wechat/mp/api/oauth2.rs +++ b/src/wechat/mp/api/oauth2.rs @@ -90,7 +90,7 @@ pub struct WechatMpOauth2AccessTokenResponse{ } -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct WechatMpOauth2UserInfo { pub openid: String, pub nickname: String, diff --git a/src/wechat/mp/api/ocr.rs b/src/wechat/mp/api/ocr.rs new file mode 100644 index 0000000..23b024a --- /dev/null +++ b/src/wechat/mp/api/ocr.rs @@ -0,0 +1,368 @@ +use std::fs::File; +use std::io::Read; +use std::path::Path; +use std::vec; + +use serde::{Serialize, Deserialize}; +use serde_json::{Value}; + +use crate::{session::SessionStore, request::{RequestType}, WechatCommonResponse, WeChatMpClient, LabradorResult, WechatRequest, RequestBody}; +use crate::wechat::mp::constants::IMG_URL; +use crate::wechat::mp::method::{MpOcrMethod, WechatMpMethod}; + +/// 微信连接WI-FI接口. +#[derive(Debug, Clone)] +pub struct WeChatMpOcr<'a, T: SessionStore> { + client: &'a WeChatMpClient, +} + +#[allow(unused)] +impl<'a, T: SessionStore> WeChatMpOcr<'a, T> { + + #[inline] + pub fn new(client: &WeChatMpClient) -> WeChatMpOcr { + WeChatMpOcr { + client, + } + } + + /// 身份证OCR识别接口 + pub async fn id_card(&self, img_url: &str) -> LabradorResult { + let img_url = urlencoding::encode(img_url).to_string(); + let v = self.client.post(WechatMpMethod::Ocr(MpOcrMethod::IdCard), vec![(IMG_URL.to_string(), img_url)], Value::Null, RequestType::Json).await?.json::()?; + WechatCommonResponse::parse::(v) + } + + /// 身份证OCR识别接口 + pub async fn id_card_file(&self, file_path: &str) -> LabradorResult { + let path = Path::new(file_path); + let file_name = path.file_name().map(|v| v.to_str().unwrap_or_default()).unwrap_or_default(); + let mut f = File::open(path)?; + let mut content: Vec = Vec::new(); + let _ = f.read_to_end(&mut content)?; + let req = WechatMpOcrRequest { + ocr_type: 1, + filename: "".to_string(), + data: content + }; + let v = self.client.execute::(req).await?.json::()?; + WechatCommonResponse::parse::(v) + } + + /// 银行卡OCR识别接口 + /// 文件大小限制:小于2M + pub async fn back_card(&self, img_url: &str) -> LabradorResult { + let img_url = urlencoding::encode(img_url).to_string(); + let v = self.client.post(WechatMpMethod::Ocr(MpOcrMethod::BankCard), vec![(IMG_URL.to_string(), img_url)], Value::Null, RequestType::Json).await?.json::()?; + WechatCommonResponse::parse::(v) + } + + /// 银行卡OCR识别接口 + /// 文件大小限制:小于2M + pub async fn back_card_file(&self, file_path: &str) -> LabradorResult { + let path = Path::new(file_path); + let file_name = path.file_name().map(|v| v.to_str().unwrap_or_default()).unwrap_or_default(); + let mut f = File::open(path)?; + let mut content: Vec = Vec::new(); + let _ = f.read_to_end(&mut content)?; + let req = WechatMpOcrRequest { + ocr_type: 2, + filename: "".to_string(), + data: content + }; + let v = self.client.execute::(req).await?.json::()?; + WechatCommonResponse::parse::(v) + } + + /// 行驶证OCR识别接口 + /// 文件大小限制:小于2M + pub async fn driving(&self, img_url: &str) -> LabradorResult { + let img_url = urlencoding::encode(img_url).to_string(); + let v = self.client.post(WechatMpMethod::Ocr(MpOcrMethod::Driving), vec![(IMG_URL.to_string(), img_url)], Value::Null, RequestType::Json).await?.json::()?; + WechatCommonResponse::parse::(v) + } + + /// 行驶证OCR识别接口 + /// 文件大小限制:小于2M + pub async fn driving_file(&self, file_path: &str) -> LabradorResult { + let path = Path::new(file_path); + let file_name = path.file_name().map(|v| v.to_str().unwrap_or_default()).unwrap_or_default(); + let mut f = File::open(path)?; + let mut content: Vec = Vec::new(); + let _ = f.read_to_end(&mut content)?; + let req = WechatMpOcrRequest { + ocr_type: 3, + filename: "".to_string(), + data: content + }; + let v = self.client.execute::(req).await?.json::()?; + WechatCommonResponse::parse::(v) + } + + /// 驾驶证OCR识别接口 + /// 文件大小限制:小于2M + pub async fn driving_license(&self, img_url: &str) -> LabradorResult { + let img_url = urlencoding::encode(img_url).to_string(); + let v = self.client.post(WechatMpMethod::Ocr(MpOcrMethod::DrivingLicense), vec![(IMG_URL.to_string(), img_url)], Value::Null, RequestType::Json).await?.json::()?; + WechatCommonResponse::parse::(v) + } + + /// 驾驶证OCR识别接口 + /// 文件大小限制:小于2M + pub async fn driving_license_file(&self, file_path: &str) -> LabradorResult { + let path = Path::new(file_path); + let file_name = path.file_name().map(|v| v.to_str().unwrap_or_default()).unwrap_or_default(); + let mut f = File::open(path)?; + let mut content: Vec = Vec::new(); + let _ = f.read_to_end(&mut content)?; + let req = WechatMpOcrRequest { + ocr_type: 4, + filename: "".to_string(), + data: content + }; + let v = self.client.execute::(req).await?.json::()?; + WechatCommonResponse::parse::(v) + } + + /// 营业执照OCR识别接口 + /// 文件大小限制:小于2M + pub async fn biz_license(&self, img_url: &str) -> LabradorResult { + let img_url = urlencoding::encode(img_url).to_string(); + let v = self.client.post(WechatMpMethod::Ocr(MpOcrMethod::BizLicense), vec![(IMG_URL.to_string(), img_url)], Value::Null, RequestType::Json).await?.json::()?; + WechatCommonResponse::parse::(v) + } + + /// 营业执照OCR识别接口 + /// 文件大小限制:小于2M + pub async fn biz_license_file(&self, file_path: &str) -> LabradorResult { + let path = Path::new(file_path); + let file_name = path.file_name().map(|v| v.to_str().unwrap_or_default()).unwrap_or_default(); + let mut f = File::open(path)?; + let mut content: Vec = Vec::new(); + let _ = f.read_to_end(&mut content)?; + let req = WechatMpOcrRequest { + ocr_type: 5, + filename: "".to_string(), + data: content + }; + let v = self.client.execute::(req).await?.json::()?; + WechatCommonResponse::parse::(v) + } + + /// 通用印刷体OCR识别接口 + /// 文件大小限制:小于2M + /// 适用于屏幕截图、印刷体照片等场景 + pub async fn comm(&self, img_url: &str) -> LabradorResult { + let img_url = urlencoding::encode(img_url).to_string(); + let v = self.client.post(WechatMpMethod::Ocr(MpOcrMethod::Comm), vec![(IMG_URL.to_string(), img_url)], Value::Null, RequestType::Json).await?.json::()?; + WechatCommonResponse::parse::(v) + } + + /// 通用印刷体OCR识别接口 + /// 文件大小限制:小于2M + /// 适用于屏幕截图、印刷体照片等场景 + pub async fn comm_file(&self, file_path: &str) -> LabradorResult { + let path = Path::new(file_path); + let file_name = path.file_name().map(|v| v.to_str().unwrap_or_default()).unwrap_or_default(); + let mut f = File::open(path)?; + let mut content: Vec = Vec::new(); + let _ = f.read_to_end(&mut content)?; + let req = WechatMpOcrRequest { + ocr_type: 6, + filename: "".to_string(), + data: content + }; + let v = self.client.execute::(req).await?.json::()?; + WechatCommonResponse::parse::(v) + } + +} + +//---------------------------------------------------------------------------------------------------------------------------- + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct WechatOcrIdCardResponse { + #[serde(rename="type")] + pub r#type: Option, + pub name: Option, + pub id: Option, + pub valid_date: Option, +} +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct WechatOcrCommResponse { + pub img_size: Option, + pub items: Option>, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct WechatOcrItems { + pub text: Option, + pub pos: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct WechatOcrBankCardResponse { + pub number: Option, +} + + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct WechatOcrDrivingLicenseResponse { + /// 证号 + pub id_num: Option, + /// 姓名 + pub name: Option, + /// 性别 + pub sex: Option, + /// 国籍 + pub nationality: Option, + /// 住址 + pub address: Option, + /// 出生日期 + pub birth_date: Option, + /// 初次领证日期 + pub issue_date: Option, + /// 准驾车型 + pub car_class: Option, + /// 有效期限起始日 + pub valid_from: Option, + /// 有效期限终止日 + pub valid_to: Option, + /// 印章文字 + pub official_seal: Option, +} + + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct WechatOcrBizLicenseResponse { + /// 注册号 + pub reg_num: Option, + /// 编号 + pub serial: Option, + /// 法定代表人姓名 + pub legal_representative: Option, + /// 企业名称 + pub enterprise_name: Option, + /// 组成形式 + pub type_of_organization: Option, + /// 经营场所/企业住所 + pub address: Option, + /// 公司类型 + pub type_of_enterprise: Option, + /// 经营范围 + pub business_scope: Option, + /// 注册资本 + pub registered_capital: Option, + /// 实收资本 + pub paid_in_capital: Option, + /// 营业期限 + pub valid_period: Option, + /// 注册日期/成立日期 + pub registered_date: Option, + /// 营业执照位置 + pub cert_position: Option, + /// 图片大小 + pub img_size: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct WechatOcrDrivingResponse { + /// 车牌号码 + pub plate_num: Option, + /// 车辆类型 + pub vehicle_type: Option, + /// 所有人 + pub owner: Option, + /// 住址 + pub addr: Option, + /// 使用性质 + pub use_character: Option, + /// 品牌型号 + pub model: Option, + /// 车辆识别代码 + pub vin: Option, + /// 发动机号码 + pub engine_num: Option, + /// 注册日期 + pub register_date: Option, + /// 发证日期 + pub issue_date: Option, + /// 车牌号码 + pub plate_num_b: Option, + /// 号牌 + pub record: Option, + /// 核定载人数 + pub passengers_num: Option, + /// 总质量 + pub total_quality: Option, + /// 整备质量 + pub prepare_quality: Option, + /// 外廓尺寸 + pub overall_size: Option, + /// 卡片正面位置(检测到卡片正面才会返回) + pub card_position_front: Option, + /// 卡片反面位置(检测到卡片反面才会返回) + pub card_position_back: Option, + /// 图片大小 + pub img_size: Option, +} + + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct CardPosition { + pub pos: Option, +} + + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct CertPosition { + pub pos: Option, +} + + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct WechatOcrPos { + pub left_top: Option, + pub right_top: Option, + pub right_bottom: Option, + pub left_bottom: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Coordinate { + pub x: Option, + pub y: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct WechatOcrImgSize { + pub w: Option, + pub h: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct WechatMpOcrRequest { + /// 1 身份证 2 银行卡 3 行驶证 4 驾驶证 5 营业执照 6 通用印刷体 + pub ocr_type: u8, + pub filename: String, + pub data: Vec +} + +impl WechatRequest for WechatMpOcrRequest { + fn get_api_method_name(&self) -> String { + match self.ocr_type { + 1 => MpOcrMethod::IdCard.get_method(), + 2 => MpOcrMethod::BankCard.get_method(), + 3 => MpOcrMethod::Driving.get_method(), + 4 => MpOcrMethod::DrivingLicense.get_method(), + 5 => MpOcrMethod::BizLicense.get_method(), + 6 => MpOcrMethod::Comm.get_method(), + _=> "".to_string() + } + } + + fn get_request_body(&self) -> RequestBody { + let form = reqwest::multipart::Form::new().part("media", reqwest::multipart::Part::stream(self.data.to_vec()).file_name(self.filename.to_string())); + form.into() + } +} \ No newline at end of file diff --git a/src/wechat/mp/api/qrcode.rs b/src/wechat/mp/api/qrcode.rs index 66b2536..d769396 100644 --- a/src/wechat/mp/api/qrcode.rs +++ b/src/wechat/mp/api/qrcode.rs @@ -106,33 +106,33 @@ impl<'a, T: SessionStore> WeChatMpQRCode<'a, T> { /// 详情请见: 生成带参数的二维码 /// pub fn get_url(&self, qrcode_ticket: &QRCodeTicket) -> String { - let ticket = &qrcode_ticket.ticket; + let ticket = &qrcode_ticket.ticket.to_owned().unwrap_or_default(); self.get_url_with_ticket(ticket) } } //---------------------------------------------------------------------------------------------------------------------------- -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct QRCodeTicket { - pub ticket: String, - pub expire_seconds: i32, - pub url: String, + pub ticket: Option, + pub expire_seconds: Option, + pub url: Option, } -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct TempQRCodeRequest { scene_id: Option, scene_str: Option, expire_seconds: u32, } -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct PermQRCodeRequest { scene_str: String, } -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct MiniQRCodeRequest { scene: String, page: String, diff --git a/src/wechat/mp/api/template_msg.rs b/src/wechat/mp/api/template_msg.rs index ea974ef..69563a0 100644 --- a/src/wechat/mp/api/template_msg.rs +++ b/src/wechat/mp/api/template_msg.rs @@ -83,7 +83,7 @@ impl<'a, T: SessionStore> WeChatMpTemplateMessage<'a, T> { //---------------------------------------------------------------------------------------------------------------------------- -#[derive(Debug, Clone, PartialEq, Eq,Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct TemplateMessage { pub touser: Option, pub template_id: String, @@ -94,7 +94,7 @@ pub struct TemplateMessage { /// 行业信息 -#[derive(Debug, Clone, PartialEq, Eq,Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct IndustryResponse { primary_industry: Option, secondary_industry: Option, @@ -102,7 +102,7 @@ pub struct IndustryResponse { -#[derive(Debug, Clone, PartialEq, Eq,Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct IndustryClass { first_class: Option, second_class: Option, @@ -110,7 +110,7 @@ pub struct IndustryClass { /// 模版信息 -#[derive(Debug, Clone, PartialEq, Eq,Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct TemplateMessageInfo { /// 模板ID template_id: Option, diff --git a/src/wechat/mp/api/user.rs b/src/wechat/mp/api/user.rs index f0abf39..39fa028 100644 --- a/src/wechat/mp/api/user.rs +++ b/src/wechat/mp/api/user.rs @@ -281,7 +281,7 @@ impl<'a, T: SessionStore> WeChatMpUser<'a, T> { //---------------------------------------------------------------------------------------------------------------------------- -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct WechatUser { pub subscribe: bool, pub openid: String, @@ -299,7 +299,7 @@ pub struct WechatUser { } -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct Followers { pub total: u64, pub count: u64, diff --git a/src/wechat/mp/api/wifi.rs b/src/wechat/mp/api/wifi.rs new file mode 100644 index 0000000..6be6d8c --- /dev/null +++ b/src/wechat/mp/api/wifi.rs @@ -0,0 +1,115 @@ +use std::vec; + +use serde::{Serialize, Deserialize}; +use serde_json::{json, Value}; + +use crate::{session::SessionStore, request::{RequestType}, WechatCommonResponse, WeChatMpClient, LabradorResult}; +use crate::wechat::mp::method::{MpWifiMethod, WechatMpMethod}; + +/// 微信连接WI-FI接口. +#[derive(Debug, Clone)] +pub struct WeChatMpWifi<'a, T: SessionStore> { + client: &'a WeChatMpClient, +} + +#[allow(unused)] +impl<'a, T: SessionStore> WeChatMpWifi<'a, T> { + + #[inline] + pub fn new(client: &WeChatMpClient) -> WeChatMpWifi { + WeChatMpWifi { + client, + } + } + + ///
+    /// 获取Wi-Fi门店列表.
+    /// 通过此接口获取WiFi的门店列表,该列表包括公众平台的门店信息、以及添加设备后的WiFi相关信息。创建门店方法请参考“微信门店接口”。
+    /// 注:微信连Wi-Fi下的所有接口中的shop_id,必需先通过此接口获取。
+    ///
+    /// http请求方式: POST
+    /// 请求URL:地址
+    /// 
+ pub async fn list_shop(&self, page_index: u32, page_size: u32) -> LabradorResult { + let req = json!({ + "pageindex": page_index, + "pagesize": page_size + }); + self.client.post(WechatMpMethod::Wifi(MpWifiMethod::ShopList), vec![], req, RequestType::Json).await?.json::() + } + + ///
+    /// 查询门店Wi-Fi信息
+    /// 通过此接口查询某一门店的详细Wi-Fi信息,包括门店内的设备类型、ssid、密码、设备数量、商家主页URL、顶部常驻入口文案。
+    ///
+    /// http请求方式: POST
+    /// 请求URL:地址
+    /// POST数据格式:JSON
+    /// 
+ pub async fn get_shop(&self, shop_id: u64) -> LabradorResult { + let req = json!({ + "shop_id": shop_id, + }); + let v = self.client.post(WechatMpMethod::Wifi(MpWifiMethod::ShopList), vec![], req, RequestType::Json).await?.json::()?; + WechatCommonResponse::parse::(v) + + } + + ///
+    /// 修改门店网络信息.
+    /// 通过此接口修改门店的网络信息,包括网络名称(ssid)或密码。需注意:
+    /// 只有门店下已添加Wi-Fi网络信息,才能调用此接口修改网络信息;添加方式请参考“添加密码型设备”和"添加portal型设备”接口文档。
+    /// 网络信息修改后,密码型设备需同步修改所有设备的ssid或密码;portal型设备需修改所有设备的ssid,并按照《硬件鉴权协议接口》修改“第二步:改造移动端portal页面”中的ssid参数,否则将无法正常连网。
+    /// 文档地址:地址
+    /// 
+ pub async fn update_shop_wifi(&self, shop_id: u64, old_ssid: &str, ssid: &str, password: Option<&str>) -> LabradorResult { + let req = json!({ + "shop_id": shop_id, + "old_ssid": old_ssid, + "ssid": ssid, + "password": password, + }); + self.client.post(WechatMpMethod::Wifi(MpWifiMethod::UpdateShop), vec![], req, RequestType::Json).await?.json::() + } + +} + +//---------------------------------------------------------------------------------------------------------------------------- + +#[derive(Debug, Clone, PartialEq, Eq,Serialize, Deserialize)] +pub struct WechatMpWifiShopDataResponse { + /// 门店名称 + pub shop_name: Option, + /// 无线网络设备的ssid,未添加设备为空,多个ssid时显示第一个. + pub ssid: Option, + /// 无线网络设备的ssid列表,返回数组格式. + pub ssid_list: Option>, + /// ssid和密码的列表,数组格式。当为密码型设备时,密码才有值. + pub ssid_password_list: Option>, + /// 设备密码,当设备类型为密码型时返回. + pub password: Option, + /// 门店内设备的设备类型,0-未添加设备,4-密码型设备,31-portal型设备. + pub protocol_type: Option, + /// 门店内设备总数 + pub ap_count: Option, + /// 商家主页模板类型 + pub template_id: Option, + /// 商家主页链接 + pub homepage_url: Option, + /// 顶部常驻入口上显示的文本内容:0--欢迎光临+公众号名称;1--欢迎光临+门店名称;2--已连接+公众号名称+WiFi;3--已连接+门店名称+Wi-Fi. + pub bar_type: Option, + /// 连网完成页链接 + pub finishpage_url: Option, + /// 商户自己的id,与门店poi_id对应关系,建议在添加门店时候建立关联关系,具体请参考“微信门店接口”. + pub sid: Option, + /// 门店ID(适用于微信卡券、微信门店业务),具体定义参考微信门店,与shop_id一一对应. + pub poi_id: Option, +} + +#[derive(Debug, Clone, PartialEq, Eq,Serialize, Deserialize)] +pub struct SsidPassword { + /// 无线网络设备的ssid + pub ssid: Option, + /// 无线网络设备的password + pub password: Option, +} \ No newline at end of file diff --git a/src/wechat/mp/constants.rs b/src/wechat/mp/constants.rs index 31da3b5..03ca84f 100644 --- a/src/wechat/mp/constants.rs +++ b/src/wechat/mp/constants.rs @@ -19,3 +19,12 @@ pub static REFRESH_TOKEN: &str = "refresh_token"; pub static QR_SCENE: &str = "QR_SCENE"; pub static QR_LIMIT_SCENE: &str = "QR_LIMIT_SCENE"; +pub static IMG_URL: &str = "img_url"; + +/// ticket类型 +pub static TICKET_TYPE_JSAPI: &str = "jsapi"; +pub static TICKET_TYPE: &str = "type"; +pub static TICKET_TYPE_SDK: &str = "2"; +pub static TICKET_TYPE_WXCARD: &str = "wx_card"; + + diff --git a/src/wechat/mp/method.rs b/src/wechat/mp/method.rs index 870a162..39d37ce 100644 --- a/src/wechat/mp/method.rs +++ b/src/wechat/mp/method.rs @@ -3,18 +3,36 @@ use crate::RequestMethod; #[allow(unused)] #[derive(Debug, PartialEq, Clone)] pub enum WechatMpMethod { + /// 获取access_token AccessToken, + /// 短key托管(生成短key的url) + GenShortenUrl, + GetCallbackIp, + QrConnectUrl, + /// 获得各种类型的ticket + GetTicket, + /// 短key解析(解析短key的url) + FetchShortenUrl, Oauth2(Oauth2Method), /// codesession CodeSession, /// 客户服务 CustomService(MpCustomServiceMethod), + /// ocr + Ocr(MpOcrMethod), + /// wifi服务 + Wifi(MpWifiMethod), + /// 用户服务 User(MpUserMethod), + /// 菜单服务 Menu(MpMenuMethod), - Message(MpMessageMethod), + /// 订阅消息 SubscribeMessage(MpSubscribeMessageMethod), + /// 模板消息 TemplateMessage(MpTemplateMessageMethod), + /// 二维码 QrCode(MpQrCodeMethod), + /// 媒体文件 Media(MpMediaMethod), /// 自定义方法 Custom(String) @@ -110,17 +128,6 @@ pub enum MpQrCodeMethod { ShowQrCode, } - - -#[allow(unused)] -#[derive(Debug, PartialEq, Clone)] -pub enum MpMessageMethod { - - -} - - - #[allow(unused)] #[derive(Debug, PartialEq, Clone)] pub enum MpTemplateMessageMethod { @@ -152,6 +159,56 @@ impl MpTemplateMessageMethod { } +#[allow(unused)] +#[derive(Debug, PartialEq, Clone)] +pub enum MpWifiMethod { + ShopList, + GetShop, + UpdateShop, +} + +#[allow(unused)] +impl MpWifiMethod { + pub fn get_method(&self) -> String { + match *self { + MpWifiMethod::ShopList => String::from("/bizwifi/shop/list"), + MpWifiMethod::GetShop => String::from("/bizwifi/shop/get"), + MpWifiMethod::UpdateShop => String::from("/bizwifi/shop/update"), + } + } +} + + + + + + +#[allow(unused)] +#[derive(Debug, PartialEq, Clone)] +pub enum MpOcrMethod { + IdCard, + BankCard, + Driving, + DrivingLicense, + BizLicense, + Comm +} + +#[allow(unused)] +impl MpOcrMethod { + pub fn get_method(&self) -> String { + match *self { + MpOcrMethod::IdCard => String::from("/cv/ocr/idcard"), + MpOcrMethod::BankCard => String::from("/cv/ocr/bankcard"), + MpOcrMethod::Driving => String::from("/cv/ocr/driving"), + MpOcrMethod::DrivingLicense => String::from("/cv/ocr/drivinglicense"), + MpOcrMethod::BizLicense => String::from("/cv/ocr/bizlicense"), + MpOcrMethod::Comm => String::from("/cv/ocr/comm"), + } + } +} + + @@ -201,16 +258,22 @@ impl RequestMethod for WechatMpMethod { match self { WechatMpMethod::CodeSession => String::from("/sns/jscode2session"), WechatMpMethod::AccessToken => String::from("/cgi-bin/token"), + WechatMpMethod::GenShortenUrl => String::from("/cgi-bin/shorten/gen"), + WechatMpMethod::FetchShortenUrl => String::from("/cgi-bin/shorten/fetch"), + WechatMpMethod::GetTicket => String::from("/cgi-bin/ticket/getticket"), + WechatMpMethod::GetCallbackIp => String::from("/cgi-bin/getcallbackip"), + WechatMpMethod::QrConnectUrl => String::from("/connect/qrconnect"), WechatMpMethod::Oauth2(v) => v.get_method(), WechatMpMethod::CustomService(v) => v.get_method(), WechatMpMethod::User(v) => v.get_method(), WechatMpMethod::Menu(v) => v.get_method(), - WechatMpMethod::Message(v) => v.get_method(), + WechatMpMethod::Wifi(v) => v.get_method(), WechatMpMethod::TemplateMessage(v) => v.get_method(), WechatMpMethod::QrCode(v) => v.get_method(), WechatMpMethod::Media(v) => v.get_method(), WechatMpMethod::Custom(v) => v.to_string(), WechatMpMethod::SubscribeMessage(v) => v.get_method(), + WechatMpMethod::Ocr(v) => v.get_method() } } } @@ -283,15 +346,6 @@ impl MpMenuMethod { } -#[allow(unused)] -impl MpMessageMethod { - pub fn get_method(&self) -> String { - match *self { - - } - } -} - #[allow(unused)] impl MpQrCodeMethod { pub fn get_method(&self) -> String { diff --git a/src/wechat/mp/mod.rs b/src/wechat/mp/mod.rs index efab440..b9d45b1 100644 --- a/src/wechat/mp/mod.rs +++ b/src/wechat/mp/mod.rs @@ -1,5 +1,6 @@ -use crate::{session::SessionStore, client::APIClient, request::{Method, RequestType, LabraResponse, LabraRequest, RequestMethod}, WeChatCrypto, util::current_timestamp, LabradorResult, SimpleStorage, WechatRequest}; +use crate::{session::SessionStore, client::APIClient, request::{Method, RequestType, LabraResponse, LabraRequest, RequestMethod}, WeChatCrypto, util::current_timestamp, LabradorResult, SimpleStorage, WechatRequest, WechatCommonResponse, JsapiSignature, get_timestamp, get_nonce_str}; use serde::{Serialize, Deserialize}; +use serde_json::{json, Value}; use crate::wechat::mp::method::WechatMpMethod; mod api; @@ -11,7 +12,8 @@ pub mod replies; mod constants; pub use api::*; -use crate::wechat::mp::constants::{ACCESS_TOKEN, APPID, CLIENT_CREDENTIAL, GRANT_TYPE, SECRET}; +use crate::wechat::mp::constants::{ACCESS_TOKEN, APPID, CLIENT_CREDENTIAL, GRANT_TYPE, SECRET, TICKET_TYPE, TICKET_TYPE_JSAPI, TICKET_TYPE_SDK, TICKET_TYPE_WXCARD}; +use crate::wechat::mp::method::WechatMpMethod::QrConnectUrl; #[allow(unused)] #[derive(Debug, Clone)] @@ -32,6 +34,36 @@ pub struct AccessTokenResponse{ pub expires_in: i64, } +#[allow(unused)] +#[derive(Serialize, Deserialize)] +pub struct WechatMpShortKeyResponse{ + /// 长信息 + pub long_data: Option, + /// 创建的时间戳 + pub create_time: Option, + /// 剩余的过期秒数 + pub expire_seconds: Option, +} + +pub enum TicketType { + /// jsapi + JSAPI, + /// sdk + SDK, + /// 微信卡券 + WxCard +} + +impl ToString for TicketType { + fn to_string(&self) -> String { + match self { + TicketType::JSAPI => TICKET_TYPE_JSAPI.to_string(), + TicketType::SDK => TICKET_TYPE_SDK.to_string(), + TicketType::WxCard => TICKET_TYPE_WXCARD.to_string(), + } + } +} + #[allow(unused)] impl WeChatMpClient { @@ -92,8 +124,6 @@ impl WeChatMpClient { let expires_in = res.expires_in; // 预留200秒的时间 let expires_at = current_timestamp() + expires_in - 200; - let token_key = format!("{}_access_token", self.appid); - let expires_key = format!("{}_expires_at", self.appid); session.set(&token_key, token.to_owned(), Some(expires_in as usize)); session.set(&expires_key, expires_at, Some(expires_in as usize)); Ok(token) @@ -102,6 +132,121 @@ impl WeChatMpClient { } } + ///
+    /// 短key托管 类似于短链API.
+    /// 详情请见: https://developers.weixin.qq.com/doc/offiaccount/Account_Management/KEY_Shortener.html
+    /// 
+ #[inline] + pub async fn gen_shorten(&self, long_data: &str, expire_seconds: u64) -> LabradorResult { + let res = self.post(WechatMpMethod::GenShortenUrl, vec![], json!({"long_data": long_data, "expire_seconds": expire_seconds}), RequestType::Json).await?.json::()?; + let v = WechatCommonResponse::parse::(res)?; + let short_key = v["short_key"].as_str().unwrap_or_default(); + Ok(short_key.to_string()) + } + + ///
+    /// 短key解析 将短key还原为长信息。
+    /// 详情请见: https://developers.weixin.qq.com/doc/offiaccount/Account_Management/KEY_Shortener.html
+    /// 
+ #[inline] + pub async fn fetch_shorten(&self, short_key: &str) -> LabradorResult { + let res = self.post(WechatMpMethod::GenShortenUrl, vec![], json!({"short_key": short_key}), RequestType::Json).await?.json::()?; + WechatCommonResponse::parse::(res) + } + + ///
+    /// 获得ticket,不强制刷新ticket.
+    /// 链接
+    /// 
+ #[inline] + pub async fn get_ticket(&self, ticket_type: TicketType) -> LabradorResult { + self.get_ticket_force(ticket_type, false).await + } + + ///
+    /// 获得ticket.
+    /// 获得时会检查 Token是否过期,如果过期了,那么就刷新一下,否则就什么都不干
+    /// 链接
+    /// 
+ #[inline] + pub async fn get_ticket_force(&self, ticket_type: TicketType, force_refresh: bool) -> LabradorResult { + let session = self.client.session(); + let key = format!("{}_{}_ticket", self.appid, &ticket_type.to_string()); + let expires_key = format!("{}_{}_ticket_expires_at", self.appid, &ticket_type.to_string()); + let ticket: String = session.get(&key, Some("".to_owned()))?.unwrap_or_default(); + let timestamp = current_timestamp(); + let expires_at: i64 = session.get(&expires_key, Some(timestamp))?.unwrap_or_default(); + if expires_at <= timestamp || force_refresh { + let res = self.get(WechatMpMethod::GetTicket, vec![(TICKET_TYPE, ticket_type.to_string().as_str())], RequestType::Json).await?.json::()?; + let v = WechatCommonResponse::parse::(res)?; + let ticket = v["ticket"].as_str().unwrap_or_default(); + let expires_in = v["expires_in"].as_i64().unwrap_or_default(); + // 预留200秒的时间 + let expires_at = current_timestamp() + expires_in - 200; + session.set(&key, ticket.to_string(), Some(expires_in as usize)); + session.set(&expires_key, expires_at, Some(expires_in as usize)); + Ok(ticket.to_string()) + } else { + Ok(ticket) + } + } + + /// + ///
+    /// 创建调用jsapi时所需要的签名.
+    ///
+    /// 详情请见:链接
+    /// 
+ pub async fn create_jsapi_signature(&self, url: &str) -> LabradorResult { + let timestamp = get_timestamp() / 1000; + let noncestr = get_nonce_str(); + let jsapi_ticket = self.get_jsapi_ticket(false).await?; + let signature = WeChatCrypto::get_sha1_sign(&vec!["jsapi_ticket=".to_string() + &jsapi_ticket, + "noncestr=".to_string() + &noncestr, + "timestamp=".to_string() + ×tamp.to_string(),"url=".to_string() + &url].join("&")); + Ok(JsapiSignature{ + app_id: self.appid.to_string(), + nonce_str: noncestr, + url: url.to_string(), + signature, + timestamp, + }) + } + + /// + ///
+    /// 构造第三方使用网站应用授权登录的url.
+    /// 详情请见: 网站应用微信登录开发指南
+    /// URL格式为https://open.weixin.qq.com/connect/qrconnect?appid=APPID&redirect_uri=REDIRECT_URI&response_type=code&scope=SCOPE&state=STATE#wechat_redirect
+    /// 
+ pub async fn build_qr_connect_url(&self, redirect_url: &str, scope: &str, state: &str, ) -> LabradorResult { + Ok(format!("{}?appid={}&redirect_uri={}&response_type=code&scope={}&state={}#wechat_redirect", QrConnectUrl.get_method(), self.appid.to_string(), urlencoding::encode(redirect_url), scope, state)) + } + + /// + ///
+    /// 获取微信服务器的ip段
+    /// [文档](http://mp.weixin.qq.com/wiki/0/2ad4b6bfd29f30f71d39616c2a0fcedc.html)
+    /// 
+ pub async fn get_callback_ip(&self, force_refresh: bool) -> LabradorResult> { + let v = self.get(WechatMpMethod::GetCallbackIp, vec![], RequestType::Json).await?.json::()?; + let v = WechatCommonResponse::parse::(v)?; + let ip_list = v["ip_list"].as_array().unwrap_or(&vec![]).iter().map(|v| v.as_str().unwrap_or_default().to_string()).collect::>(); + Ok(ip_list) + } + + /// + ///
+    /// 获得jsapi_ticket.
+    /// 获得时会检查jsapiToken是否过期,如果过期了,那么就刷新一下,否则就什么都不干
+    ///
+    /// 详情请见:链接
+    /// 
+ pub async fn get_jsapi_ticket(&self, force_refresh: bool) -> LabradorResult { + self.get_ticket_force(TicketType::JSAPI, force_refresh).await + } + + /// ///
@@ -193,4 +338,14 @@ impl WeChatMpClient {
         WeChatMpSubscribeMessage::new(self)
     }
 
+    /// Wifi服务
+    pub fn wifi(&self) -> WeChatMpWifi {
+        WeChatMpWifi::new(self)
+    }
+
+    /// OCR服务
+    pub fn ocr(&self) -> WeChatMpOcr {
+        WeChatMpOcr::new(self)
+    }
+
 }