.Net Core 3.1实现微信公众号发送模板消息,且跳转微信小程序

背景

公司需要将内部产品的审批放到微信小程序上,为广大客户提供更便捷和高效的单据审批体验。目前微信小程序功能及后台逻辑已经实现,需要把审批的通知信息发送到微信公众号(微信小程序无法发送消息给客户)

微信公众号和小程序关联

微信小程序审批之后,根据设置的审批流程,通知给下一个或多个审批人。需要知道同一个人的小程序和公众号的关联关系。微信小程序、公众号对应的每一个客户都分别对应唯一的openId。同一个人要关联,就需要UnionId。
UnionId: 如果开发者拥有多个移动应用、网站应用、和公众帐号(包括小程序),可通过unionid来区分用户的唯一性,因为只要是同一个微信开放平台帐号下的移动应用、网站应用和公众帐号(包括小程序),用户的unionid是唯一的。换句话说,同一用户,对同一个微信开放平台下的不同应用,unionid是相同的。(特别注意加粗部分,特别重要)。需要到微信开放平台关联小程序及公众号,才能调用接口获取UnionId

微信小程序获取UnionId及OpenId

小程序要获取UnionId有两个方法

  • 第一个是通过获取Code,通过Code换取openId和session_key(获取UnionId需要),此时关注了公众号的用户可以直接换取到UnionId。
//前端源码
//小程序app.js
onLaunch() {
    var that = this;
	userLogin()
      .then(res => {
         //获取到的code:res.code
         //用code换取openId和session_key及unionId
        return getOpenid(res.code);
      }).then(res => {
        //openId
        that.globalData.openid = res.data.openid;
        //session_key
        that.globalData.sessionkey = res.data.session_key;
      });
}
//获取code
function userLogin() {
  return new Promise((resolve, reject) => {
    wx.login({
      success(res) {
        if (res.code) {
          resolve(res);
        }
      }
    });
  });
}
//获取openId、session_key、unionId
function getOpenid(code) {
  return new Promise((resolve, reject) => {
    wx.request({
      url: "https://你的服务器地址/api/Login/GetOpenId?js_code="+code,
      method: "GET",
      success: function(res) {
        //得到电话号码
        resolve(res);
      },
      fail(err) {
        reject(err);
      }
    });
  });
}
//后端源码
//appid:小程序appid
//secret:小程序secret
//js_code:上面获取到的code
[HttpGet]
public ActionResult GetOpenId(string js_code)
{
string url = string.Format("https://api.weixin.qq.com/sns/jscode2session?
appid={0}&secret={1}&js_code={2}&grant_type=authorization_code", WXConst.appid,WXConst.secret, js_code);
string html = HttpRestlt.GetHttpResult(url);
return Ok(html);
 }
  • 第二个是通过调用接口 wx.getUserInfo ,从解密数据(encryptedData)中获取 UnionID(推荐使用)。无论如何,还是要获取到session_key,所以绕不过上面的方法。
//Login布局  [open-type]一定要这么写,不然不会跳出授权窗口,默认失败
<button class="userbtn" open-type="getUserInfo" bindgetuserinfo="bindGetUserInfo" >请登录</button>
//单击事件
  bindGetUserInfo: function (e) {
    var that = this;
    if (e.detail.errMsg == "getUserInfo:ok") {
      wx.getUserInfo({
        success:function(res) {
          let url="https://你的服务器地址/api/Login/getUserUnoinID";
          let params={
            session_key: app.globalData.sessionkey,
            encryptedData: res.encryptedData,
            iv: res.iv
          };
          let method="POST";
          return request.getResult(url,params,method).then(res=>{
              if(res.data=="保存成功"){
                  //......other code
              }
          });
        }
      })
    }
  }
//后端源码

[HttpPost]
public async Task<ActionResult> getUserUnoinID(PhoneNumManagement management)
{
//解密获取unionId
UserInfoModel unionId = getUnionIdT<UserInfoModel>(management);
//保存到后台
bool result = await _login.SaveUnionId(unionId);
if (!result)
   return BadRequest();
else
   return Ok("保存成功");
}
//因为需要对encryptedData做对称解密,以下是解密算法
private T getUnionIdT<T>(PhoneNumManagement management)
{
  try
   {
   //非空验证
   if (!string.IsNullOrWhiteSpace(management.encryptedData) && !string.IsNullOrWhiteSpace(management.session_key) && !string.IsNullOrWhiteSpace(management.iv))
    {
     var decryptBytes = Convert.FromBase64String(management.encryptedData.Replace(' ', '+'));
     var keyBytes = Convert.FromBase64String(management.session_key.Replace(' ', '+'));
     var ivBytes = Convert.FromBase64String(management.iv.Replace(' ', '+'));
     var aes = new AesCryptoServiceProvider
        {
         Key = keyBytes,
         IV = ivBytes,
         Mode = CipherMode.CBC,
         Padding = PaddingMode.PKCS7
         };
     var outputBytes = aes.CreateDecryptor().TransformFinalBlock(decryptBytes, 0, decryptBytes.Length);
     var decryptResult = Encoding.UTF8.GetString(outputBytes);
     T decryptData = JsonConvert.DeserializeObject<T>(decryptResult);
     return decryptData;
   }
   else
   {return default;}
  }
  catch (Exception e)
  {return default;}
}
//补充HttpRestlt发送消息类
using AutoUpdate;
using System;
using System.IO;
using System.Net;
using System.Text;

namespace MobileApp.Utils
{
    public static class HttpRestlt
    {
        /// <summary>
        /// 获取消息
        /// </summary>
        /// <param name="url"></param>
        /// <returns></returns>
        public static string GetHttpResult(string url)
        {
            HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url);
            request.Method = "GET";
            request.ContentType = "application/x-www-form-urlencoded;charset=utf-8";
            HttpWebResponse response = (HttpWebResponse)request.GetResponse();
            Stream myResponseStream = response.GetResponseStream();
            StreamReader myStreamReader = new StreamReader(myResponseStream, Encoding.UTF8);
            string retString = myStreamReader.ReadToEnd();
            myStreamReader.Close();
            myResponseStream.Close();
            return retString;
        }
        /// <summary>
        /// 发送消息,并获取结果
        /// </summary>
        /// <param name="url"></param>
        /// <param name="senddata"></param>
        /// <returns></returns>
        public static string GetHttpSendResult(string url, string senddata)
        {
            string result = null;
            byte[] byteData = Encoding.GetEncoding("UTF-8").GetBytes(senddata);
            try
            {
                HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url);
                request.ContentType = "application/x-www-form-urlencoded";
                request.Referer = url;
                request.Accept = "*/*";
                request.Timeout = 30 * 1000;
                request.UserAgent = "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 2.0.50727; .NET CLR 3.0.04506.648; .NET CLR 3.0.4506.2152; .NET CLR 3.5.30729)";
                request.Method = "POST";
                request.ContentLength = byteData.Length;
                Stream stream = request.GetRequestStream();
                stream.Write(byteData, 0, byteData.Length);
                stream.Flush();
                stream.Close();
                HttpWebResponse response = (HttpWebResponse)request.GetResponse();
                Stream backStream = response.GetResponseStream();
                StreamReader sr = new StreamReader(backStream, Encoding.GetEncoding("UTF-8"));
                result = sr.ReadToEnd();
                sr.Close();
                backStream.Close();
                response.Close();
                request.Abort();
            }
            catch(Exception ex)
            {
                LogHelper.WriteException(ex);
                LogHelper.WriteLog("发送微信模板消息:" + ex.Message);
                LogHelper.WriteLog(result);
                return string.Empty;
            }
            return result;
        }

    }
}

微信公众号获取UnionId及OpenId

using AutoUpdate;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using MobileApp.BLL.Utils;
using MobileApp.Entities;
using MobileApp.IBLL;
using MobileApp.Models;
using MobileApp.Utils;
using Newtonsoft.Json;
using System;
using System.IO;
using System.Linq;
using System.Text;

namespace MobileApp.Controllers
{
    /// <summary>
    /// 微信小程序端,根据点击的ID,获取送审单据内容
    /// </summary>
    [ApiController]
    [Route("api/[controller]/[action]")]
    //[Authorize]
    public class SendMessageController : ControllerBase
    {
        private readonly ILogin<UseEntity> _login;
        public SendMessageController(ILogin<UseEntity> login)
        {
            this._login = login;
        }

		//注:此方法没有写是什么请求。是因为验证为开发者的时候是Get请求。
		//发送其它消息的时候又是Post请求。所以此处不填请求方式。
        public void ProcessRequest()
        {
        	//此处需要用到context,但是.net fw和.net core的api有所不同,下面会给出源码
            Microsoft.AspNetCore.Http.HttpContext context = MyHttpContext.Current;
            string postString = null;
            try
            {
            	//验证的时候调用下面的方法,验证通过后,后面就不用了,可以注释掉
                //ValidDev(context);
                //此处开始为接收微信服务器推送的消息
                if (context.Request.Method.ToUpper() == "POST")
                {
                    context.Request.EnableBuffering();
                    context.Request.Body.Seek(0, 0);
                    context.Request.Body.Position = 0;
                    using (var ms = new MemoryStream())
                    {
                        context.Request.Body.CopyTo(ms);
                        var b = ms.ToArray();
                        postString = Encoding.UTF8.GetString(b);
                        Handle(postString);
                    }
                }
            }
            catch (Exception ex)
            {
                LogHelper.WriteLog("关注公众号:" + ex.Message);
            }
        }
        /// <summary>
        /// 验证成为微信服务号开发者  第一次验证的时候被调用即可
        /// </summary>
        [HttpGet]
        public void ValidDev(Microsoft.AspNetCore.Http.HttpContext context)
        {
            const string token = "hzrgwkjyxgs";
            string echoString = null;
            //string signature = null;
            //string timestamp = null;
            //string nonce = null;

            try
            {
                echoString = context.Request.Query["echoStr"].FirstOrDefault();
                //signature = context.Request.Query["signature"].FirstOrDefault();
                //timestamp = context.Request.Query["timestamp"].FirstOrDefault();
                //nonce = context.Request.Query["nonce"].FirstOrDefault();
                if (!string.IsNullOrEmpty(echoString))
                {
                    context.Response.WriteAsync(echoString);
                }
            }
            catch (Exception ex)
            {
                LogHelper.WriteLog("验证开发者:" + ex.Message);
            }
        }
         /// <summary>
        /// 处理信息得到openId
        /// </summary>
        /// <param name="postStr"></param>
        private void Handle(string postStr)
        {
        	//XmlHelper.GetT将微信发送的xml解析为自定义类,下面会提供源码
            WeChartAcceptMessage message = XmlHelper.GetT<WeChartAcceptMessage>(postStr);
            if (message == null)
                return;
            string openId = message.FromUserName;
            //获取access_token
            string access_token = GetAccessToken.GetAccess_Token();
            GetUnionid(openId, access_token);
        }

        /// <summary>
        /// 获取用户唯一Unionid
        /// </summary>
        /// <param name="openId"></param>
        private async void GetUnionid(string openId,string accessToken)
        {
            string url = string.Format("https://api.weixin.qq.com/cgi-bin/user/info?access_token={0}&openid={1}&lang=zh_CN ", accessToken, openId);
            string html = HttpRestlt.GetHttpResult(url);
            UseWeChartInfoModel useWeChartInfoModel = JsonConvert.DeserializeObject<UseWeChartInfoModel>(html);
            if (useWeChartInfoModel == null)
                return;
            string unionId = useWeChartInfoModel.unionid;
            UserInfoModel userInfoModel = new UserInfoModel
            {
                openId = openId,
                unionid = unionId
            };
            bool result = await _login.EditAppId(userInfoModel);
            if (!result)
                LogHelper.WriteLog("微信公众号获取UnionId保存失败");
        }
    }

}

public void ConfigureServices(IServiceCollection services )
        {
            //3.0中默认禁用了AllowSynchronousIO,同步读取body的方式需要ConfigureServices中配置允许同步读取IO流,否则可能会抛出异常 Synchronous operations are disallowed. Call ReadAsync or set AllowSynchronousIO to true instead.
            services.Configure<KestrelServerOptions>(x => x.AllowSynchronousIO = true)
                   .Configure<IISServerOptions>(x => x.AllowSynchronousIO = true);


            //利用ASP.NET Core的依赖注入容器系统,通过请求获取IHttpContextAccessor接口,我们拥有模拟使用HttpContext.Current这样API的可能性。但是因为IHttpContextAccessor接口默认不是由依赖注入进行实例管理的。我们先要将它注册到ServiceCollection中
            services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();
            MyHttpContext.ServiceProvider = services.BuildServiceProvider();
     }

//自定义实现context
using System;

namespace MobileApp
{
    public static class MyHttpContext
    {
        public static IServiceProvider ServiceProvider;
        public static Microsoft.AspNetCore.Http.HttpContext Current
        {
            get
            {
                object factory = ServiceProvider.GetService(typeof(Microsoft.AspNetCore.Http.IHttpContextAccessor));
                Microsoft.AspNetCore.Http.HttpContext context = ((Microsoft.AspNetCore.Http.HttpContextAccessor)factory).HttpContext;
                return context;
            }
        }
    }
}
  • xml反序列化得到实体,如有错误可参考
using AutoUpdate;
using System;
using System.IO;
using System.Xml.Serialization;

namespace MobileApp.Utils
{
    public class XmlHelper
    {
        /// <summary>
        /// xml反序列化
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="xml"></param>
        /// <returns></returns>
        public static T GetT<T>(string xml)
        {
            try
            {
                //此处若报错,可参考文章 https://www.cnblogs.com/lionetchen/p/3926846.html
                XmlSerializer serializer = new XmlSerializer(typeof(T), new XmlRootAttribute("xml"));
                StringReader reader = new StringReader(xml.Replace(" ", ""));
                T entity = (T)serializer.Deserialize(reader);
                reader.Close();
                reader.Dispose();
                return entity;
            }
            catch (Exception ex)
            {
                LogHelper.WriteLog("xml序列化为类:" + ex.Message);
                return default(T);
            }
        }
    }
}

  • 获取Access_Token
using AutoUpdate;
using Microsoft.Extensions.Caching.Memory;
using MobileApp.Models;
using MobileApp.Utils;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Linq;

namespace MobileApp.BLL.Utils
{
    public static class GetAccessToken
    {
        public static IMemoryCache _memoryCache = new MemoryCache(new MemoryCacheOptions());
        public static string GetAccess_Token()
        {
            string access_token = null;
            Dictionary<string, DateTime> accesstoken = new Dictionary<string, DateTime>();
            var item = _memoryCache.Get("accesstoken");
            if (item == null)
            {
                access_token = Get_Access();
                if (access_token == string.Empty)
                    return string.Empty;
                accesstoken.Add(access_token, DateTime.Now);
                _memoryCache.Set("accesstoken", accesstoken);
                return access_token;
            }
            else
            {
                accesstoken = (Dictionary<string, DateTime>)item;
                DateTime timePre = accesstoken.FirstOrDefault().Value;
                //若过期,重新申请
                if ((timePre - DateTime.Now).Hours < -2)
                {
                    access_token = Get_Access();
                    if (access_token == string.Empty)
                        return string.Empty;
                    accesstoken.Add(access_token, DateTime.Now);
                    _memoryCache.Remove("accesstoken");
                    _memoryCache.Set("accesstoken", accesstoken);
                    return access_token;
                }
                else
                    return accesstoken.First().Key;
            }

        }

        public static string Get_Access()
        {
            //此处的WXConst.appid_fwh  WXConst.secret_fwh是公众号对应的内容,不是小程序对应的内容,不要混淆
            string url = string.Format("https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid={0}&secret={1}", WXConst.appid_fwh, WXConst.secret_fwh);
            string result = HttpRestlt.GetHttpResult(url);
            try
            {
                AccessToeknModel toeknModel = JsonConvert.DeserializeObject<AccessToeknModel>(result);
                return toeknModel.access_token;
            }
            catch (Exception ex)
            {
                LogHelper.WriteException(ex);
                LogHelper.WriteLog("获取accesstoken:" + ex.Message);
                LogHelper.WriteLog(result);
                return string.Empty;
            }
        }
    }
}

谢谢打赏!

概述 本平台主要功能是针对微信商家公众号提供与众不同的、有针对性的营销推广服务。通过沐雪微信平台,用户可以轻松管理自己的微信各类信息,对微信公众账号进行维护、开展智能机器人、在线发优惠劵、抽奖、刮奖、派发会员卡、打造微官网、开启微团购等多种活动,对微信营销实现有效监控,极大扩展潜在客户群和实现企业的运营目标。沐雪微信平台很好的弥补了微信公众平台本身功能不足、针对性不强、交互不便利的问题,为商家公众账号提供更为贴心的、且是核心需求的功能和服务。在线优惠劵、转盘抽奖、微信会员卡等推广服务更是让微信成为商家推广的利器。智能客服的可调教功能让用户真正从微信繁琐的日常客服工作中解脱出来,真正成为商家便利的新营销渠道。 二、源码特点 1、微信公众平台开发,对于公众平台所有功能进行完整开发。 2、多用户:可同时进行多公众号的管理和配置。 3、直接性:购买者可直接购买细微修改即是成品的平台商品。 4、开发语言:asp.net,C# ,webform,数据库sqlserver 2005 三、功能介绍 1、菜单回复:关注时回复、默认回复、文本回复、图文回复、语音回复、请求回复、LBS回复。 2、自定义菜单:公众号自定义菜单设置 3、用户列表管理:获取关注公众号的账户信息进行管理 4、微官网:各种微官网模板可自行配置,头部幻灯片,底部,分类模块配置,内容配置 5、微商城:商城模板配置、产品分类管理、商品信息管理、商品录入、客户资源管理、订单管 理、物流配送方式设置 6、支付方式:微信支付,货到付款。 7、微汽车[大模块]、微酒店、点餐系统 8、微会员卡:会员卡商家、会员卡商城、会员卡头部广告设计 9、用户管理:微信用户管理、系统角色管理、系统操作日志 10、后台菜单:后台导航菜单管理 11、系统设置:网站基本信息设置、功能权限设置、短信平台设置、邮件发送设置、文件上传设 置 12、代理商管理:代理商发展下线。 四、环境要求 开发环境: 操作系统:windows7,8 软件:vs2012+sqlserver2005 发布环境: 操作系统:windows server 2003,2008 软件:.netframework4.0,iis6+,sqlserver 2005 后续会更新vs2017+sql2012 ,新功能的开发
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值