. net core3.1 to send subscription message by wechat applet

Time:2021-1-10

1 appsettings.json Define applet configuration information


"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}"
}

2、 Write general class loading configuration

using System;
using System.Text;
using System.Security.Cryptography;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Configuration.Json;

namespace WXERP.Services
{
  /// <summary>
  ///Project public static class
  /// </summary>
  public class Common
  {
    /// <summary>
    ///Get root directory
    /// </summary>
    public static string AppRoot => Environment.CurrentDirectory;// AppContext.BaseDirectory;
    /// <summary>
    ///Get project configuration
    /// </summary>
    public static IConfiguration Configuration { get; set; }
    /// <summary>
    ///Load project configuration
    /// </summary>
    static Common()
    {
      Configuration = new ConfigurationBuilder()
      .Add(new JsonConfigurationSource
      {
        Path = "appsettings.json",
        Reloadonchange = true // when appsettings.json Reload when modified 
      })
      .Build();
    }

    /// <summary>
    ///SHA1 encryption
    /// </summary>
    ///< param name = "content" > string to be encrypted < / param >
    ///< returns > returns a 40 bit uppercase string < / returns >
    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);
      }
    }

  }
}

3、 Write httphelper request class

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
{
  /// <summary>
  ///HTTP request helper class
  /// </summary>
  public class HttpHelper
  {
    /// <summary>
    ///Post synchronization request
    /// </summary>
    ///< param name = "URL" > address < / param >
    ///< param name = "postData" > Data < / param >
    /// <param name="contentType">application/xml、application/json、application/text、application/x-www-form-urlencoded</param>
    ///< param name = "headers" > request header < / param > 
    /// <returns></returns>
    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;
    }

    /// <summary>
    ///Post asynchronous request
    /// </summary>
    ///< param name = "URL" > address < / param >
    ///< param name = "postData" > Data < / param >
    /// <param name="contentType">application/xml、application/json、application/text、application/x-www-form-urlencoded</param>
    ///< param name = "timeout" > request timeout < / param > 
    ///< param name = "headers" > request header < / param > 
    /// <returns></returns>
    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();
    }

    /// <summary>
    ///Get synchronization request
    /// </summary>
    ///< param name = "URL" > address < / param >
    ///< param name = "headers" > request header < / param >
    /// <returns></returns>
    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;
    }

    /// <summary>
    ///Get asynchronous request
    /// </summary>
    /// <param name="url"></param>
    /// <param name="headers"></param>
    /// <returns></returns>
    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();
    }

  }
}

4、 Store and obtain openid in SQL server, this is mainly because the submission message is not on the wechat applet side. If the subscription message is initiated on the wechat applet, this step can be ignored

//Create database table

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,
)

//The sqlhelper database auxiliary class comes from the communicationoperatedb utility and can be written by yourself

using System.Data;
using System.Text;
using CommunicationOperateDBUtility;

namespace WXERP.Services.CommunicationOperateDAL
{
  /// <summary>
  ///Wechat information
  /// </summary>
  public class WXInforDeal
  {
    private SqlHelper sqlHelper = null;
    /// <summary>
    ///Initializing database helper objects
    /// </summary>
    /// <param name="con"></param>
    public WXInforDeal(object con)
    {
      sqlHelper = new SqlHelper(con);
    }
    /// <summary>
    ///Access to wechat login user information
    /// </summary>
    ///< param name = "staffidlist" > job No. < / param >
    /// <returns></returns>
    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;
    }
  }
}

5、 Write subscription message base class model

using System;
using System.Data;
using Newtonsoft.Json;
using System.Collections.Generic;
using WXERP.Services.CommunicationOperateDAL;

namespace WXERP.Models
{
  /// <summary>
  ///Subscription message request model
  /// </summary>
  public class SubscribeMessageModel
  {
    /// <summary>
    ///Initialize audit subscription message
    /// </summary>
    ///< param name = "dbtransorcnn" > database transaction < / param >
    ///< param name = "nextauditstaffid" > next audit notification user job number < / param >
    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();
      }
    }
    /// <summary>
    ///Openid of message receiver
    /// </summary>
    [JsonProperty("touser")]
    public string Touser { get; set; }
    /// <summary>
    ///Message template ID
    /// </summary>
    [JsonProperty("template_id")]
    public string TemplateId { get; set; }
    /// <summary>
    ///Click the jump page after the template card, which is only limited to the page in this applet. It supports parameters (example index? Foo = bar). If this field is not filled in, it will not jump
    /// </summary>
    [JsonProperty("page")]
    public string Page { get; set; }
    /// <summary>
    ///Jump applet type: developer development version, trial experience version, formal formal version, default to formal version
    /// </summary>
    [JsonProperty("miniprogram_state")]
    public string MiniprogramState { get; set; }
    /// <summary>
    ///Enter the language type of the applet, support zh_ CN (Simplified Chinese), EN_ Us (English), zh_ HK (traditional Chinese), zh_ Tw (traditional Chinese), default to zh_ CN
    /// </summary>
    [JsonProperty("lang")]
    public string Lang { get; set; }
    /// <summary>
    ///Template content
    /// </summary>
    [JsonProperty("data")]
    public Dictionary<string, DataValue> Data { get; set; }
  }
  /// <summary>
  ///Template content關鍵字
  /// </summary>
  public class DataValue
  {
    /// <summary>
    ///Subscription message parameter values
    /// </summary>
    [JsonProperty("value")]
    public string Value { get; set; }
  }

  /// <summary>
  ///Response model of applet subscription message
  /// </summary>
  public class SubscribeMsgResponseModel
  {
    /// <summary>
    ///Error code
    /// </summary>
    public int Errcode { get; set; }
    /// <summary>
    ///Error message
    /// </summary>
    public string Errmsg { get; set; }
  }

  /// <summary>
  ///Response model of getting token by small program
  /// </summary>
  public class AccessTokenResponseModel
  {
    /// <summary>
    ///Applet access token
    /// </summary>
    public string Access_token { get; set; }
    /// <summary>
    ///Token expiration time, in seconds
    /// </summary>
    public int Expires_id { get; set; }
    /// <summary>
    ///Token creation time
    /// </summary>
    public DateTime Create_time { get; set; }
    /// <summary>
    ///Refreshed token
    /// </summary>
    public string Refresh_token { get; set; }
    /// <summary>
    ///If the user does not pay attention to the public number, visiting the public number web page will also produce a unique identity
      /// </summary>
    public string Openid { get; set; }
    /// <summary>
    ///Scope of user authorization, separated by commas
    /// </summary>
    public string Scope { get; set; }
  }

}

6、 To implement the message subscription base class, the following settemplatedata method sets the content of the message to be pushed according to its own situation. If there are other subscription message templates in the future, add a new class to implement the subscribemessage model

using System;
using System.Collections.Generic;
using Newtonsoft.Json;
using BestSoft.Common.Resources;
using BSFWorkFlow.Common.GeneralUtility;
using WXERP.Models;

namespace WXERP.Services.SubscribeMessage
{
  /// <summary>
  ///Audit subscription message
  /// </summary>
  public class AuditSubscribeMessage : SubscribeMessageModel
  {
    private string page;
    private string lang;
    private Dictionary<string, DataValue> data;
    /// <summary>
    ///Set the openid of the applet
    /// </summary>
    ///< param name = "dbtransorcnn" > database transaction < / param >
    ///< param name = "nextauditstaffid" > next audit notification user job number < / param >
    public AuditSubscribeMessage(object dbTransOrCnn, string nextAuditStaffId)
    : base(dbTransOrCnn, nextAuditStaffId)
    {

    }
    /// <summary>
    ///Message template ID
    /// </summary>
    [JsonProperty("template_id")]
    public new string TemplateId => Common.Configuration["WX:Templates:Audit:TemplateId"];

    /// <summary>
    ///Set applet subscription message jump page
    /// </summary>
    /// <param name="formId"></param>
    /// <param name="tableId"></param>
    public void SetPageUrl(string formId, string tableId)
    {
      Page = string.Format(Common.Configuration["WX:Templates:Audit:PageUrl"],
      formId, tableId);
    }
    /// <summary>
    ///Click the jump page after the template card
    /// </summary>
    [JsonProperty("page")]
    public new string Page
    {
      get
      {
        return page;
      }
      set
      {
        page = value;
        return;
      }
    }
    /// <summary>
    ///Jump applet type
    /// </summary>
    [JsonProperty("miniprogram_state")]
    public new string MiniprogramState => Common.Configuration["WX:Templates:Audit:MiniprogramState"];
    /// <summary>
    ///Enter the language type of the applet, support zh_ CN (Simplified Chinese), EN_ Us (English), zh_ HK (traditional Chinese), zh_ Tw (traditional Chinese), default to zh_ CN
    /// </summary>
    [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;
      }
    }
    /// <summary>
    ///Set audit subscription message data
    /// </summary>
    ///Approval actions: pass, reject, void and return
    ///< param name = "itemauditstatus" > audit status: 1 means audit completed < / param >
    ///< param name = "currentworkflowname" > audit Title < / param >
    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_ Your document has been approved;
          else
            tip_ msg =  GeneralFunction.ReplaceNullOrEmptyStr ( SourcesWarehouse.GetStringSources ("WFEngine_ "You have a new document to review!";
        break;
        case WFAuditOperation.AuditPassButDegree:
          tip_ msg =  GeneralFunction.ReplaceNullOrEmptyStr ( SourcesWarehouse.GetStringSources ("WFEngine_ "Auditdegreetip", "the document you submitted is waiting for objection!";
        break;
        case WFAuditOperation.AuditAbort:
          tip_ msg =  GeneralFunction.ReplaceNullOrEmptyStr ( SourcesWarehouse.GetStringSources ("WFEngine_ The document you submitted has been voided;
        break;
        case WFAuditOperation.AuditBack:
          tip_ msg =  GeneralFunction.ReplaceNullOrEmptyStr ( SourcesWarehouse.GetStringSources ("WFEngine_ The document you submitted has been returned for correction;
        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;
    }
    /// <summary>
    ///Audit subscription message數據
    /// </summary>
    [JsonProperty("data")]
    public new Dictionary<string, DataValue> Data
    {
      get
      {
        return data;
      }
      set
      {
        data = value;
        return;
      }
    }

  }
}

7、 Write send subscription message, push message, configure signature authentication

using System;
using System.Threading.Tasks;
using System.Collections.Generic;
using Newtonsoft.Json;
using WXERP.Models;

namespace WXERP.Services
{
  /// <summary>
  ///System message context
  /// </summary>
  public class MessageContext
  {
    /// <summary>
    ///Get the global lock of accesstoken
    /// </summary>
    private readonly static object SyncLock = new object();

    private static Dictionary<string, AccessTokenResponseModel> tokenCache = new Dictionary<string, AccessTokenResponseModel>();

    /// <summary>
    ///Send subscription message
    /// </summary>
    ///< param name = "MSG" > message content < / param >
    ///< param name = "errmsg" > it may be due to the wrong token obtained < / param >
    /// <returns></returns>
    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<SubscribeMsgResponseModel>(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;
    }

    /// <summary>
    ///Get applet access token
    /// </summary>
    /// <returns></returns>
    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<AccessTokenResponseModel>(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<AccessTokenResponseModel>(responseJson);
          result.Create_time = DateTime.Now;
          tokenCache[appid] = result;
        }
        return result.Access_token;
      }
    }

    /// <summary>
    ///The verification message comes from wechat server
    /// </summary>
    ///< param name = "signature" > wechat encrypted signature, which combines the token, timestamp and nonce filled in by the developer < / param >
    ///< param name = "timestamp" > timestamp < / param >
    ///< param name = "nonce" > random number < / param >
    /// <returns></returns>
    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;
    }

  }
}

8、 Write message push to configure signature authentication controller

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using WXERP.Services;

namespace WXERP.Controllers
{
  /// <summary>
  ///Message controller
  /// </summary>
  [Route("api/[controller]")]
  [ApiController]
  public class MessageController : ControllerBase
  {
    private readonly MessageContext _context;
    /// <summary>
    ///Initialization message
    /// </summary>
    public MessageController()
    {
      _context = new MessageContext();
    }

    ///< summary > wechat news
    ///< remarks > the verification message comes from the wechat server < / remarks >
    ///< param name = "signature" > wechat encrypted signature, which combines the token, timestamp and nonce filled in by the developer < / param >
    ///< param name = "timestamp" > timestamp < / param >
    ///< param name = "nonce" > random number < / param >
    ///< param name = "echostr" > random string < / param >
    /// <returns></returns>
    [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");
      }
    }

  }
}

9、 Call small program subscription message, need to implement other logic

// iFormSaveDAL.GetTran  Database link transaction. If sending message fails, the submitted form data should be rolled back
// wFControl.NextAuditNotifyStaffIDStr  Job number of the next audit user
// auditPageData.FormID  Form number
// auditPageData.MainRecordID  Form data ID
//@Operationbycode is an enumeration type, which is passed by the front end: approved, voided, returned, etc
// wFControl.ItemAuditStatus  An enumeration type. If all auditing is completed, it is 1; otherwise, it is 0
// wFControl.CurrentWorkflowName  The name of the current process, for example: leave form approval
//@Saveaferinfo global character variable, used to save the result information

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;
  }
}

Here is the article about. Net core3.1 to send subscription message to wechat applet. For more related. Net core applet sending subscription content, please search previous articles of developer or continue to browse the following related articles. I hope you can support developer more in the future!