.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
- 微信公众号须是服务号
- 微信公众号需要绑定小程序
- 微信公众号需要开通开发者权限,且认证为开发者
- 微信公众号验证开发者,且接收微信服务器推送的消息,获取openId及UnionId
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保存失败");
}
}
}
- context在.net core中的用法解析,可参考文章
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;
}
}
}
}
谢谢打赏!