一、appsettings.json定义小程序配置信息
"WX": { "AppId": "wx88822730803edd44", "AppSecret": "75b269042e8b5026e6ed14aa24ba9353", "Templates": { "Audit": { "TemplateId": "aBaIjTsPBluYtj2tzotzpowsDDBGLhXQkwrScupnQsM", "PageUrl": "/pages/index/formAudit?formId={0}&tableId={1}", "MiniprogramState": "developer", "Lang": "zh_TW", "Data": { "Title": "thing6", "Content": "thing19", "Date": "date9" } } }, "SignatureToken": "aaaaaa", "MessageSendUrl": "https://api.weixin.qq.com/cgi-bin/message/subscribe/send?access_token={0}", "AccessTokenUrl": "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid={0}&secret={1}" }
二、编写通用类加载配置
using System; using System.Text; using System.Security.Cryptography; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration.Json; namespace WXERP.Services { ////// 项目公有静态类 /// public class Common { /// /// 獲取根目錄 /// public static string AppRoot => Environment.CurrentDirectory;// AppContext.BaseDirectory; /// /// 獲取項目配置 /// public static IConfiguration Configuration { get; set; } /// /// 加載項目配置 /// static Common() { Configuration = new ConfigurationBuilder() .Add(new JsonConfigurationSource { Path = "appsettings.json", ReloadOnChange = true //当appsettings.json被修改时重新加载 }) .Build(); } /// /// SHA1加密 /// /// 需要加密的字符串 /// 返回40位大寫字符串 public static string SHA1(string content) { try { SHA1 sha1 = new SHA1CryptoServiceProvider(); byte[] bytes_in = Encoding.UTF8.GetBytes(content); byte[] bytes_out = sha1.ComputeHash(bytes_in); sha1.Dispose(); string result = BitConverter.ToString(bytes_out); result = result.Replace("-", ""); return result; } catch (Exception ex) { throw new Exception("Error in SHA1: " + ex.Message); } } } }
三、编写HttpHelper请求类
using System; using System.Text; using System.Net.Http; using System.Net.Http.Headers; using System.Threading.Tasks; using System.Collections.Generic; namespace WXERP.Services { ////// HTTP請求輔助類 /// public class HttpHelper { /// /// post同步請求 /// /// 地址 /// 數據 /// application/xml、application/json、application/text、application/x-www-form-urlencoded /// 請求頭 /// public static string HttpPost(string url, string postData = null, string contentType = null, Dictionary<string, string> headers = null) { using HttpClient client = new HttpClient(); if (headers != null) { foreach (var header in headers) client.DefaultRequestHeaders.Add(header.Key, header.Value); } postData ??= ""; using HttpContent httpContent = new StringContent(postData, Encoding.UTF8); if (contentType != null) httpContent.Headers.ContentType = new MediaTypeHeaderValue(contentType); HttpResponseMessage response = client.PostAsync(url, httpContent).Result; return response.Content.ReadAsStringAsync().Result; } /// /// post異步請求 /// /// 地址 /// 數據 /// application/xml、application/json、application/text、application/x-www-form-urlencoded /// 請求超時時間 /// 請求頭 /// public static async Task<string> HttpPostAsync(string url, string postData = null, string contentType = null, int timeOut = 30, Dictionary<string, string> headers = null) { using HttpClient client = new HttpClient(); client.Timeout = new TimeSpan(0, 0, timeOut); if (headers != null) { foreach (var header in headers) client.DefaultRequestHeaders.Add(header.Key, header.Value); } postData ??= ""; using HttpContent httpContent = new StringContent(postData, Encoding.UTF8); if (contentType != null) httpContent.Headers.ContentType = new MediaTypeHeaderValue(contentType); HttpResponseMessage response = await client.PostAsync(url, httpContent); return await response.Content.ReadAsStringAsync(); } /// /// get同步請求 /// /// 地址 /// 請求頭 /// public static string HttpGet(string url, Dictionary<string, string> headers = null) { using HttpClient client = new HttpClient(); if (headers != null) { foreach (var header in headers) client.DefaultRequestHeaders.Add(header.Key, header.Value); } HttpResponseMessage response = client.GetAsync(url).Result; return response.Content.ReadAsStringAsync().Result; } /// /// get異步請求 /// /// /// /// public static async Task<string> HttpGetAsync(string url, Dictionary<string, string> headers = null) { using HttpClient client = new HttpClient(); if (headers != null) { foreach (var header in headers) client.DefaultRequestHeaders.Add(header.Key, header.Value); } HttpResponseMessage response = await client.GetAsync(url); return await response.Content.ReadAsStringAsync(); } } }
四、在sqlserver下存储并获取openid,这个主要是因为提交消息并不是在微信小程序端,如果是在微信小程序上发起订阅消息,可以忽略这个步骤
// 创建数据库表 create table TBSF_Conmmunicate_WXUser ( ID int identity(1,1) primary key, Staff_ID varchar(10), OpenId varchar(50), SessionKey varchar(50), UnionId varchar(50), IsValid bit, ) // SqlHelper数据库辅助类来自于CommunicationOperateDBUtility,可以自己编写 using System.Data; using System.Text; using CommunicationOperateDBUtility; namespace WXERP.Services.CommunicationOperateDAL { ////// 微信信息 /// public class WXInforDeal { private SqlHelper sqlHelper = null; /// /// 初始化數據庫輔助對象 /// /// public WXInforDeal(object con) { sqlHelper = new SqlHelper(con); } /// /// 獲取微信登陸用戶信息 /// /// 工號 /// public DataSet GetLoginUserInfo(string staffIdList) { DataSet ds = new DataSet(); StringBuilder stringBuilder = new StringBuilder(); stringBuilder.Append(" SELECT distinct OpenId FROM "); stringBuilder.Append(" TBSF_Conmmunicate_WXUser WHERE Staff_ID IN ("); stringBuilder.Append(staffIdList); stringBuilder.Append(")"); string strSql = stringBuilder.ToString(); sqlHelper.DBRunSql(strSql, ref ds); return ds; } } }
五、编写订阅消息基类模型
using System; using System.Data; using Newtonsoft.Json; using System.Collections.Generic; using WXERP.Services.CommunicationOperateDAL; namespace WXERP.Models { ////// 訂閲消息請求模型 /// public class SubscribeMessageModel { /// /// 初始化審核訂閲消息 /// /// 數據庫事務 /// 下一個審核通知用戶工號 public SubscribeMessageModel(object dbTransOrCnn, string nextAuditStaffId) { WXInforDeal wxInfoDeal = new WXInforDeal(dbTransOrCnn); DataSet wxUserInfo = wxInfoDeal.GetLoginUserInfo(nextAuditStaffId); if (wxUserInfo != null && wxUserInfo.Tables.Count > 0 && wxUserInfo.Tables[0].Rows.Count > 0) { Touser = wxUserInfo.Tables[0].Rows[0]["OpenId"].ToString(); } } /// /// 消息接收者的openid /// [JsonProperty("touser")] public string Touser { get; set; } /// /// 消息模板ID /// [JsonProperty("template_id")] public string TemplateId { get; set; } /// /// 點擊模板卡片后的跳轉頁面,僅限本小程序内的頁面,支持帶參數(示例index?foo=bar),該字段不填則不跳轉 /// [JsonProperty("page")] public string Page { get; set; } /// /// 跳轉小程序類型:developer開發版、trial體驗版、formal正式版,默认为正式版 /// [JsonProperty("miniprogram_state")] public string MiniprogramState { get; set; } /// /// 進入小程序查看的語言類型,支持zh_CN(簡體中文)、en_US(英文)、zh_HK(繁體中文)、zh_TW(繁體中文),默認為zh_CN /// [JsonProperty("lang")] public string Lang { get; set; } /// /// 模板内容 /// [JsonProperty("data")] public Dictionary<string, DataValue> Data { get; set; } } /// /// 模板内容關鍵字 /// public class DataValue { /// /// 訂閲消息參數值 /// [JsonProperty("value")] public string Value { get; set; } } /// /// 小程序訂閲消息響應模型 /// public class SubscribeMsgResponseModel { /// /// 錯誤代碼 /// public int Errcode { get; set; } /// /// 錯誤信息 /// public string Errmsg { get; set; } } /// /// 小程序獲取token響應模型 /// public class AccessTokenResponseModel { /// /// 小程序訪問token /// public string Access_token { get; set; } /// /// Token過期時間,單位秒 /// public int Expires_id { get; set; } /// /// Token創建時間 /// public DateTime Create_time { get; set; } /// /// 刷新以後的Token /// public string Refresh_token { get; set; } /// /// 小程序用戶唯一標識,如果用戶未關注公衆號,訪問公衆號網頁也會產生 /// public string Openid { get; set; } /// /// 用戶授權的作用域,使用逗號分隔 /// public string Scope { get; set; } } }
六、实现消息订阅基类,下面的SetTemplateData方法根据自己的情况设置需要推送消息的内容,如果以后有其他订阅消息模板,新增一个类实现SubscribeMessageModel
using System; using System.Collections.Generic; using Newtonsoft.Json; using BestSoft.Common.Resources; using BSFWorkFlow.Common.GeneralUtility; using WXERP.Models; namespace WXERP.Services.SubscribeMessage { ////// 審核訂閲消息 /// public class AuditSubscribeMessage : SubscribeMessageModel { private string page; private string lang; private Dictionary<string, DataValue> data; /// /// 設置小程序OpenId /// /// 數據庫事務 /// 下一個審核通知用戶工號 public AuditSubscribeMessage(object dbTransOrCnn, string nextAuditStaffId) : base(dbTransOrCnn, nextAuditStaffId) { } /// /// 消息模板ID /// [JsonProperty("template_id")] public new string TemplateId => Common.Configuration["WX:Templates:Audit:TemplateId"]; /// /// 設置小程序訂閲消息跳轉頁面 /// /// /// public void SetPageUrl(string formId, string tableId) { Page = string.Format(Common.Configuration["WX:Templates:Audit:PageUrl"], formId, tableId); } /// /// 點擊模板卡片后的跳轉頁面 /// [JsonProperty("page")] public new string Page { get { return page; } set { page = value; return; } } /// /// 跳轉小程序類型 /// [JsonProperty("miniprogram_state")] public new string MiniprogramState => Common.Configuration["WX:Templates:Audit:MiniprogramState"]; /// /// 進入小程序查看的語言類型,支持zh_CN(簡體中文)、en_US(英文)、zh_HK(繁體中文)、zh_TW(繁體中文),默認為zh_CN /// [JsonProperty("lang")] public new string Lang { get { lang = Common.Configuration["WX:Templates:Audit:Lang"]; if (!string.IsNullOrEmpty(MyHttpContext.Current.Request.Headers["bsLanKind"])) lang = MyHttpContext.Current.Request.Headers["bsLanKind"]; return lang; } set { lang = value; return; } } /// /// 設置審核訂閲消息數據 /// /// 審核動作:通過、否決、作廢、退回 /// 審核狀態:1代表審核完畢 /// 審核標題 public void SetTemplateData(WFAuditOperation operation, WFAuditItemStatus itemAuditStatus, string currentWorkflowName) { string tip_msg = ""; switch (operation) { case WFAuditOperation.AuditPassAndAgree: if (itemAuditStatus == WFAuditItemStatus.SuccessfulToFinishAllAudits) tip_msg = GeneralFunction.ReplaceNullOrEmptyStr(SourcesWarehouse.GetStringSources("WFEngine_FinishAuditTip"), "您的單據已審核完成!"); else tip_msg = GeneralFunction.ReplaceNullOrEmptyStr(SourcesWarehouse.GetStringSources("WFEngine_AuditAgreeTip"), "您有一筆新單據待審核!"); break; case WFAuditOperation.AuditPassButDegree: tip_msg = GeneralFunction.ReplaceNullOrEmptyStr(SourcesWarehouse.GetStringSources("WFEngine_AuditDegreeTip"), "您提交的單據等待異議!"); break; case WFAuditOperation.AuditAbort: tip_msg = GeneralFunction.ReplaceNullOrEmptyStr(SourcesWarehouse.GetStringSources("WFEngine_AuditAbortTip"), "您提交的單據已被作廢!"); break; case WFAuditOperation.AuditBack: tip_msg = GeneralFunction.ReplaceNullOrEmptyStr(SourcesWarehouse.GetStringSources("WFEngine_AuditBackTip"), "您提交的單據已被退回修正!"); break; } string title = Common.Configuration["WX:Templates:Audit:Data:Title"]; string content = Common.Configuration["WX:Templates:Audit:Data:Content"]; string date = Common.Configuration["WX:Templates:Audit:Data:Date"]; Dictionary<string, DataValue> data = new Dictionary<string, DataValue>() { {title, new DataValue{ Value= currentWorkflowName }}, {content, new DataValue{ Value= tip_msg }}, {date, new DataValue{ Value= DateTime.Now.ToShortDateString() }} }; Data = data; } /// /// 審核訂閲消息數據 /// [JsonProperty("data")] public new Dictionary<string, DataValue> Data { get { return data; } set { data = value; return; } } } }
七、编写发送订阅消息,消息推送配置签名认证
using System; using System.Threading.Tasks; using System.Collections.Generic; using Newtonsoft.Json; using WXERP.Models; namespace WXERP.Services { ////// 系統消息上下文 /// public class MessageContext { /// /// 獲取AccessToken的全局鎖 /// private readonly static object SyncLock = new object(); private static Dictionary<string, AccessTokenResponseModel> tokenCache = new Dictionary<string, AccessTokenResponseModel>(); /// /// 發送訂閲消息 /// /// 消息内容 /// 可能由於獲取的token錯誤 /// public static bool SendSubscribeMsg(SubscribeMessageModel msg, out string errMsg) { errMsg = ""; try { string token = GetAccessToken(); if (token.Length < 20) { errMsg = "Failed to send subscription message, Access token error!"; return false; } string url = string.Format(Common.Configuration["WX:MessageSendUrl"], token); string requestJson = JsonConvert.SerializeObject(msg); string responseJson = HttpHelper.HttpPost(url, requestJson, "application/json", null); var msgResponse = JsonConvert.DeserializeObject (responseJson); if (msgResponse.Errcode != 0) { errMsg = string.Format("Failed to send subscription message, {0}", msgResponse.Errmsg); return false; } } catch (Exception exp) { throw new Exception("SendSubscribeMsg: " + exp.Message); } return true; } /// /// 獲取小程序訪問token /// /// private static string GetAccessToken() { lock (SyncLock) { string appid = Common.Configuration["WX:AppId"]; string appsecret = Common.Configuration["WX:AppSecret"]; string accessTokenUrl = string.Format(Common.Configuration["WX:AccessTokenUrl"], appid, appsecret); AccessTokenResponseModel result = null; if (tokenCache.ContainsKey(appid)) result = tokenCache[appid]; if (result == null) { string responseJson = HttpHelper.HttpGet(accessTokenUrl, null); result = JsonConvert.DeserializeObject (responseJson); result.Create_time = DateTime.Now; tokenCache.Add(appid, result); } else if (DateTime.Compare(result.Create_time.AddSeconds(result.Expires_id), DateTime.Now) < 1) { string responseJson = HttpHelper.HttpGet(accessTokenUrl, null); result = JsonConvert.DeserializeObject (responseJson); result.Create_time = DateTime.Now; tokenCache[appid] = result; } return result.Access_token; } } /// /// 驗證消息來自於微信服務器 /// /// 微信加密簽名,signature結合了開發者填寫的token、timestamp、nonce /// 時間戳 /// 隨機數 /// public async Task<bool> CheckSignature(string signature, string timestamp, string nonce) { string token = Common.Configuration["WX:SignatureToken"]; string[] tmpArr = { token, timestamp, nonce }; Array.Sort(tmpArr); string tmpStr = string.Join("", tmpArr); tmpStr = Common.SHA1(tmpStr); if (!tmpStr.Equals(signature, StringComparison.OrdinalIgnoreCase)) return false; await Task.CompletedTask; return true; } } }
八、编写消息推送配置签名认证控制器
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using WXERP.Services; namespace WXERP.Controllers { ////// 消息控制器 /// [Route("api/[controller]")] [ApiController] public class MessageController : ControllerBase { private readonly MessageContext _context; /// /// 初始化消息 /// public MessageController() { _context = new MessageContext(); } /// 微信消息 /// 驗證消息來自於微信服務器 /// 微信加密簽名,signature結合了開發者填寫的token、timestamp、nonce /// 時間戳 /// 隨機數 /// 隨機字符串 /// [HttpGet("checkSignature")] [AllowAnonymous] public async void CheckSignature(string signature,string timestamp,string nonce,string echostr) { bool result = await _context.CheckSignature(signature, timestamp, nonce); if (result) { HttpContext.Response.ContentType = "text/plain; charset=utf-8"; await HttpContext.Response.WriteAsync(echostr); } else { HttpContext.Response.StatusCode = 409; HttpContext.Response.ContentType = "text/plain; charset=utf-8"; await HttpContext.Response.WriteAsync("error"); } } } }
九、调用小程序订阅消息,需要自己实现其他逻辑
//@iFormSaveDAL.GetTran 数据库链接事务,如果发送消息失败,应该回滚提交的表单数据 //@wFControl.NextAuditNotifyStaffIDStr 下一个审核用户的工号 //@auditPageData.FormID 表单编号 //@auditPageData.MainRecordID 表单数据ID //@operationByCode 一个枚举类型,前端传递的:审核通过、作废、退回等 //@wFControl.ItemAuditStatus 一个枚举类型,如果全部审核完毕为1,否则为0 //@wFControl.CurrentWorkflowName 当前流程的名称,例如:请假单审核 //@SaveAfterInfo 全局字符变量,用于保存结果信息 AuditSubscribeMessage auditMsg = new AuditSubscribeMessage(iFormSaveDAL.GetTran, wFControl.NextAuditNotifyStaffIDStr); auditMsg.SetPageUrl(auditPageData.FormID, auditPageData.MainRecordID); auditMsg.SetTemplateData(operationByCode, wFControl.ItemAuditStatus, wFControl.CurrentWorkflowName); if (!string.IsNullOrEmpty(auditMsg.Touser)) { if (!MessageContext.SendSubscribeMsg(auditMsg, out messageStr)) { SaveAfterInfo = messageStr; return false; } }
有不懂或需要改正的欢迎留言!