asp.net core微信小程序和公众号打通,实现用户关注公众号送优惠券

前提

小程序
公众号
微信开放平台
小程序和公众号都需要绑定到同一个微信开放平台,因为要获取Unionid,unionid是什么

如果开发者拥有多个移动应用、网站应用、和公众账号(包括小程序),可通过 UnionID 来区分用户的唯一性,因为只要是同一个微信开放平台账号下的移动应用、网站应用和公众账号(包括小程序),用户的 UnionID 是唯一的。换句话说,同一用户,对同一个微信开放平台下的不同应用,UnionID是相同的。

微信公众号获取unionid
在这里插入图片描述

小程序获取unionid
在这里插入图片描述

获取参数

公众号appid和appsecret
在这里插入图片描述
小程序appid和appsecret
在这里插入图片描述

微信小程序关注公众号组件

https://developers.weixin.qq.com/miniprogram/dev/component/official-account.html
在这里插入图片描述
在这里插入图片描述
小程序测试需要设置场景值
在这里插入图片描述
在这里插入图片描述
这个展示有条件,不过基本能满足,如果没有关注就是显示关注,因为关注了所以显示的是查看,模拟器上没法点击,需要发布体验版去真机上使用,如果场景下不支持,则可以模拟写一个,不过功能没法实现,最终方案是通过webview跳转公众号文件引导关注

<template>
	<view class="offical-account-container">
		<view class="top-text">
			xxx小程序关联的公众号
		</view>
		<view class="content">
			<view class="logo">
				<image src="../../static/images/officalaccount/logo.png"></image>
			</view>
			<view class="info">
				<view class="name">公众号名字</view>
				<view class="detial">
					公众号简介。
				</view>
			</view>
			<view class="action">
				<!--未关注-->
				<view class="action-item">关注</view>
				<!--已关注-->
				<!-- <view class="action-item">查看</view> -->
			</view>
		</view>
	</view>
</template>

<script>
	export default {
		name:"follow-official-account",
		data() {
			return {
				
			};
		}
	}
</script>

<style lang="scss" scoped>
.offical-account-container{
	padding: 16rpx;
	width: 750rpx;
	height: 168rpx;
	overflow: hidden;
	background-color: #fff;
	
	.top-text{
		color: #b2b2b2;
		font-size: 24rpx;
		height: 24rpx;
		line-height: 24rpx;
	}
	.content{
		display: flex;
		flex-direction: row;
		justify-content: space-between;
		align-items: center;
		width: 100%;
		margin-top:32rpx;
		height: 72rpx;
		.info{
			// padding-left: 20rpx;
			// padding-right: 20rpx;
			height: 80rpx;
			max-width: 474rpx;
			.name{
				font-size: 32rpx;
				color: #303133;
				overflow: hidden;
				white-space: nowrap;
				text-overflow: ellipsis;
			}
			.detial{
				font-size: 24rpx;
				color: #7f7f7f;
				overflow: hidden;
				white-space: nowrap;
				text-overflow: ellipsis;
			}
		}
		.logo{
			width: 80rpx;
			height: 80rpx;
			min-width: 80rpx;
			image{
				width: 100%;
				height: 100%;
			}
		}
		.action{
			// margin-right: 24rpx;
			padding-right: 8rpx;
			display: flex;
			align-items: center;
			justify-content: center;
			width: 130rpx;
			height: 80rpx;
			.action-item{
				width: 130rpx;
				height:56rpx;
				border:2rpx solid #1aad19;
				color: #1aad19;
				text-align: center;
				line-height: 56rpx;
				font-size: 28rpx;
				border-radius: 6rpx;
			}
		}
	}
}
</style>

后端需要安装SKIT.FlurlHttpClient.Wechat.Api组件
在这里插入图片描述

后端初始化client

小程序

/// <summary>
/// 客户端
/// </summary>
private WechatApiClient client;

public WeChatMiniProgramClient(IOptions<xxx> config)
{
    var options = new WechatApiClientOptions()
    {
        AppId = config.Value.AppId,
        AppSecret = config.Value.AppSecret,
        ImmeDeliveryAppKey = "",
        ImmeDeliveryAppSecret = "",
        VirtualPaymentAppKey = "",
        MidasOfferId = "",
        MidasAppKey = "",
        MidasOfferIdV2 = "",
        MidasAppKeyV2 = ""
    };
    client = WechatApiClientBuilder.Create(options).Build();
}

公众号

/// <summary>
/// 客户端
/// </summary>
private WechatApiClient client;

/// <summary>
/// 构造函数注入
/// </summary>
public OfficalAccountClient(IOptions<xxxx> config)
{
    var options = new WechatApiClientOptions()
    {
        AppId = config.Value.AppId,
        AppSecret = config.Value.AppSecret,
        ImmeDeliveryAppKey = "",
        ImmeDeliveryAppSecret = "",
        VirtualPaymentAppKey = "",
        MidasOfferId = "",
        MidasAppKey = "",
        MidasOfferIdV2 = "",
        MidasAppKeyV2 = "",
        PushToken = config.Value.Token,
        PushEncodingAESKey  =config.Value.EncodingAesKey
    };
    client = WechatApiClientBuilder.Create(options).Build();
}

小程序获取unionid

uni-app和小程序获取wx.login下的code

uni.login({
	provider:'weixin',
	success: function(res) {
		console.log(res);
		if (res.code) {
			console.log(res.code);
		} else {
			//login成功,但是没有取到code
			_this.$refs.uToast.show({
				title: '未取得code,请重试',
				type: 'error',
			})
		}
	},
	fail: function(res) {
		_this.$refs.uToast.show({
			title: '获取wx.login失败,请重试',
			type: 'error',
		})
	}
})
wx.login({
	success(res) {
		if (res.code) {
		//TODO 根据code获取openid以及unionid
		} else {
			wx.showToast({
				icon: "none",
				title: '获取信息失败',
			})
		}
	}
})

后端解析

/// <summary>
/// jscode获取openid
/// </summary>
/// <param name="jsCode"></param>
/// <returns></returns>
public async Task<(string openId,string unionId)> WeChatLogin(string jsCode)
{
    try
    {
        var userLoginRequest = new SnsJsCode2SessionRequest();
        userLoginRequest.JsCode = jsCode;
        var loginInfo = await client.ExecuteSnsJsCode2SessionAsync(userLoginRequest);
        if (loginInfo.IsSuccessful())
        {
            return (loginInfo.OpenId,loginInfo.UnionId ?? "");
        }
        return (string.Empty,string.Empty);
    }
    catch (Exception ex)
    {
        return (string.Empty,string.Empty);
    }
}

公众号获取unionid

根据openid获取unionid

/// <summary>
/// 获取关注公众号的用户详情
/// </summary>
/// <param name="openId"></param>
public async Task<(bool isSub,string errMsg,string openId,string unionId,string remark)> GetOfficalAccountFollowUserInfo(string openId)
{
    var accessToken = await WeChatGetOfficalAccountToken();
    var response = await _officalAccountClient.GetOfficalFllowAccountUserInfo(accessToken, openId);
    if (!response.IsSuccessful())
    {
        if (response.ErrorCode == 42001)
        {
            accessToken = await WeChatGetOfficalAccountToken(true);
            response = await _officalAccountClient.GetOfficalFllowAccountUserInfo(accessToken, openId);
            if (!response.IsSuccessful())
            {
                return (false,response.ErrorMessage, string.Empty, string.Empty,string.Empty);
            }
        }
        else
        {
            return (false,response.ErrorMessage, string.Empty, string.Empty,string.Empty);
        }
    }
    return (response.IsSubscribed, string.Empty, response.OpenId,response.UnionId,response.Remark);
}

/// <summary>
/// 获取关注公众号的多个用户详情
/// </summary>
/// <param name="openIds"></param>
public async Task<(bool isSuccess,string errMsg,List<CgibinUserInfoBatchGetResponse.Types.User> list)> GetOfficalAccountFollowUsersInfo(List<string> openIds)
{
    var accessToken = await WeChatGetOfficalAccountToken();
    var response = await _officalAccountClient.GetOfficalFllowAccountUsersInfo(accessToken, openIds);
    if (!response.IsSuccessful())
    {
        if (response.ErrorCode == 42001)
        {
            accessToken = await WeChatGetOfficalAccountToken(true);
            response = await _officalAccountClient.GetOfficalFllowAccountUsersInfo(accessToken, openIds);
            if (!response.IsSuccessful())
            {
                return (false,response.ErrorMessage,null);
            }
        }
        else
        {
            return (false,response.ErrorMessage,null);
        }
    }
    return (true,response.ErrorMessage,response.UserList.ToList());
}

在这里插入图片描述
在这里插入图片描述

公众号菜单处理

增加

如果遇到invalid menu api user增加之前先删除

/// <summary>
/// 创建菜单
/// </summary>
/// <param name="token"></param>
/// <returns></returns>
public async Task<CgibinMenuCreateResponse> CreateMenuInfo(string token,List<CgibinMenuCreateRequest.Types.Button> buttons)
{
    var request = new CgibinMenuCreateRequest();
    request.AccessToken = token;
    request.ButtonList = buttons;
    var userGetResponse = await client.ExecuteCgibinMenuCreateAsync(request);
    return userGetResponse;
}

示例

{
	"buttons": [
		{
			"name": "发现",
			"subbuttonlist": [
				{
					"type": "click",
					"name": "联系我们",
					"key": "contact_us"
				}
			]
		},
		{
			"name": "官方商城",
			"subbuttonlist": [
				{
					"type": "miniprogram",
					"name": "微信小程序",
					"appid": "",
					"pagepath": ""
				}
			]
		}
	]
}

删除

/// <summary>
/// 删除菜单
/// </summary>
/// <param name="token"></param>
/// <returns></returns>
public async Task<CgibinMenuDeleteResponse> MenuDelete(string token)
{
    var request = new CgibinMenuDeleteRequest();
    request.AccessToken = token;
    var userGetResponse = await client.ExecuteCgibinMenuDeleteAsync(request);
    return userGetResponse;
}

查询

/// <summary>
/// 获取菜单列表
/// </summary>
/// <param name="token"></param>
/// <returns></returns>
public async Task<CgibinGetCurrentSelfMenuInfoResponse> GetCurrentSelfmenuInfo(string token)
{
    var request = new CgibinGetCurrentSelfMenuInfoRequest();
    request.AccessToken = token;
    var userGetResponse = await client.ExecuteCgibinGetCurrentSelfMenuInfoAsync(request);
    return userGetResponse;
}

公众号自动回复文字没有这个类型,需要自己通过click方案实现

/// <summary>
/// 解密文本的xml数据
/// </summary>
/// <param name="xmlData"></param>
/// <returns></returns>
public SKIT.FlurlHttpClient.Wechat.Api.Events.ClickPushEvent DecryptEventClickXmlData(string xmlData)
{
    var webhookModel =
        client.DeserializeEventFromXml<SKIT.FlurlHttpClient.Wechat.Api.Events.ClickPushEvent>(xmlData);
    return webhookModel;
}

//微信公众号按钮点击事件
case EventType.Click:
	var eventClickXmlData = _officalAccountWechatHelper.DecryptEventClickXmlData(bodyData);
	if (eventClickXmlData.EventKey == "contact_us")
	{
	    responseData =_officalAccountWechatHelper.SendTxtMsg(welcome, openId, officalAccountOpenId);
	}
break;

用户关注/取消关注公众号监听

申请测试号,主要是提前写好接口
在这里插入图片描述
接入指南
在这里插入图片描述

/// <summary>
/// 微信公众号服务器传递过来的消息验证
/// </summary>
/// <returns></returns>
public bool ValidateMsg(string timestamp,string nonce,string signature)
{
    /* 验证微信服务器 */
    bool ret = client.VerifyEventSignatureForEcho(
        webhookTimestamp: timestamp,
        webhookNonce: nonce,
        webhookSignature: signature
    );
    return ret;
}

class XXXParamModel 
{
    /// <summary>
    /// 微信加密签名,signature
    /// 结合了开发者填写的token参数和请求中的timestamp参数、nonce参数。
    /// </summary>
    public string Signature { get; set; }
    
    /// <summary>
    /// 时间戳
    /// </summary>
    public string Timestamp { get; set; }

    /// <summary>
    /// 随机数
    /// </summary>
    public string Nonce { get; set; }
    
    /// <summary>
    /// 随机字符串
    /// </summary>
    public string Echostr { get; set; }
}

/// <summary>
/// 微信公众号验证
/// </summary>
/// <param name="param"></param>
/// <returns></returns>
[HttpGet]
public IActionResult OfficalAccountValidate([FromQuery]XXXParamModel param)
{
    var isValid = _officalAccountWechatHelper.ValidateMsg(param.Timestamp, param.Nonce, param.Nonce);
    if (isValid)
    {
        //返回随机字符串则表示验证通过
        return Content(param.Echostr);
    }
    return Content("");
}

本地测试做好内网穿透

点击提交即可,验证通过即可
在这里插入图片描述
通过之后可以点击启用
在这里插入图片描述
需要处理菜单
在这里插入图片描述
在这里插入图片描述

//允许多次读取
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILoggerFactory loggerFactory)
{
    //允许body重用
    app.Use(next => context =>
    {
        context.Request.EnableBuffering();
        return next(context);
    })
}

/// <summary>
/// 微信公众号消息处理
/// </summary>
/// <param name="param"></param>
/// <returns></returns>
[HttpPost]
public async Task<IActionResult> OfficalAccountMessageHandler([FromQuery]XXXParamModel param)
{
    var isValid = _officalAccountWechatHelper.ValidateMsg(param.Timestamp, param.Nonce, param.Signature);
    if (!isValid)
    {
        return Content("验证失败");
    }

    //过滤器或者中间件中调用
    var body = Request.Body;
    if (body.CanSeek)
    {
        body.Seek(0L, SeekOrigin.Begin);
    }
    string bodyData = string.Empty;
    bodyData =await new StreamReader(body, Encoding.UTF8).ReadToEndAsync();
    Console.WriteLine(bodyData);
    return Content("");
}

在这里插入图片描述
解密

/// <summary>
/// 解密xml数据
/// </summary>
/// <param name="xmlData"></param>
/// <returns></returns>
public WechatApiEvent DecryptXmlData(string xmlData)
{
    var xml = client.DeserializeEventFromXml(xmlData);
    return xml;
}

消息处理的完整的代码

/// <summary>
/// 微信公众号消息处理
/// </summary>
/// <param name="param"></param>
/// <returns></returns>
[HttpPost]
public async Task<IActionResult> OfficalAccountMessageHandler(
[FromQuery] OfficalAccountMessageHandlerParamModel param)
{
string responseData = string.Empty;
var isValid = _officalAccountWechatHelper.ValidateMsg(param.Timestamp, param.Nonce, param.Signature);
if (!isValid)
{
    return Content("验证失败");
}

//过滤器或者中间件中调用
var body = Request.Body;
if (body.CanSeek)
{
    body.Seek(0L, SeekOrigin.Begin);
}
string bodyData = string.Empty;
bodyData = await new StreamReader(body, Encoding.UTF8).ReadToEndAsync();
var decryptXmlData = _officalAccountWechatHelper.DecryptXmlData(bodyData);
switch (decryptXmlData.MessageType.ToLower())
{
    case RequestMsgType.Text:
        break;
    case RequestMsgType.Location:
        break;
    case RequestMsgType.Image:
        break;
    case RequestMsgType.Voice:
        break;
    case RequestMsgType.Video:
        break;
    case RequestMsgType.ShortVideo:
        break;
    case RequestMsgType.Link:
        break;
    case RequestMsgType.MessageEvent:
        var eventType = decryptXmlData.Event;
        var openId = decryptXmlData.FromUserName;
        if (!string.IsNullOrEmpty(eventType))
        {
            switch (eventType)
            {
                //关注
                case EventType.Subscribe:
                    //TODO 关注之后的处理
                    //发送被动回复消息
                    responseData =_officalAccountWechatHelper.SendTxtMsg("欢迎关注~~", openId, decryptXmlData.ToUserName);
                    Console.WriteLine(responseData);
                    break;
                //取消关注
                case EventType.Unsubscribe:
                    //TODO 取消关注之后的处理
                    break;
                case EventType.Localtion:
                    break;
                 //微信公众号点击点击事件
                 case EventType.Click:
                 //后续处理微信公众号菜单点击事件
                  break;
                default:
                    break;
            }
        }
        break;
    default:
        break;
}
return Content(responseData);
}

已有数据如何处理

公众号有获取关注的列表,根据列表中的openid获取unionid,小程序也有保存unionid,根据unionid关联起来就能实现之前数据的处理,不过没有登陆过的客户应该没法处理了,需要再客户登陆的时候获取unionid然后进行判定

公众号获取关注的列表

/// <summary>
/// 获取关注的用户列表
/// </summary>
/// <param name="token"></param>
/// <param name="nextOpenid"></param>
public async Task<CgibinUserGetResponse> GetOfficalAccountFollowUserList(string token,string nextOpenid="")
{
    var request = new CgibinUserGetRequest();
    request.AccessToken = token;
    if (!string.IsNullOrEmpty(nextOpenid))
    {
        request.NextOpenId = nextOpenid;
    }
    var userGetResponse = await client.ExecuteCgibinUserGetAsync(request);
    return userGetResponse;
}

关注公众号之后给用户发送消息

在这里插入图片描述
进行xml拼接加密处理,然后在微信调用关注的事件通知的时候返回即可

/// <summary>
/// 获取unix时间戳,扩展方法
/// </summary>
/// <param name="dt"></param>
/// <returns></returns>
public static long GetUnixTimeStampSeconds(this DateTime dt)
{
    long unixTime = ((DateTimeOffset)dt).ToUnixTimeSeconds();
    return unixTime;
}

/// <summary>
/// 解密文本的xml数据
/// </summary>
/// <param name="xmlData"></param>
/// <returns></returns>
public string SendTxtMsg(string content, string toOpenId, string fromOpenId)
{
    var replyModel = new SKIT.FlurlHttpClient.Wechat.Api.Events.TextMessageReply()
    {
        ToUserName = toOpenId,
        FromUserName = fromOpenId,
        MessageType = "text",
        Content = content,
        CreateTimestamp = DateTime.Now.GetUnixTimeStampSeconds()
    };
    string replyXml = client.SerializeEventToXml(replyModel);
    return replyXml;
}

小程序显示公众号链接

在这里插入图片描述
获取文章链接之后编写代码

switchToWebView(){
let pageUrl = 'https://mp.weixin.qq.com/s?__biz=MzkxODsdfsdf5MQ==&mid=2247483682&idx=1&sn=72sdfbsdff000ddb38bf207e04497b6c7f3&chksm=c1af76f2f6d8ffe4c1e90572ed4b5535a7f40eecd415e7d51bfdda2dcdf157844541960b4791#rd'
let linkUrl = pageUrl.split('?')  // 将域名和参数分隔开,把参数使用encodeURIComponent编码
uni.navigateTo({
    url: '/pages/urlview/urlview?url=' + linkUrl[0] + '&params=' + encodeURIComponent(linkUrl[1])
});
},

页面代码如下

<template>
	<view class="webview-container">
		<web-view :src="url"></web-view>
	</view>
</template>

<script>
	export default {
		data() {
			return {
				url:""
			}
		},
		onLoad(option){
			let link = option.url + '?'+ decodeURIComponent(option.params)
			this.url = link;
		},
		methods: {
			
		}
	}
</script>

<style lang="scss" scoped>
</style>

在这里插入图片描述

常见类型

/// <summary>
/// 常见消息类型
/// </summary>
public class EventType
{
   /// <summary>
   /// 关注
   /// </summary>
   public const string Subscribe = "subscribe";
   /// <summary>
   /// 取消订阅
   /// </summary>
   public const string Unsubscribe = "unsubscribe";
   /// <summary>
   /// 上报地理位置事件
   /// 用户同意上报地理位置后,每次进入公众号会话时,都会在进入时上报地理位置,或在进入会话后每5秒上报一次地理位置,公众号可以在公众平台网站中修改以上设置。上报地理位置时,微信会将上报地理位置事件推送到开发者填写的URL。
   /// </summary>
   public const string Localtion = "LOCATION";

   /// <summary>
   /// 自定义菜单事件-用户点击自定义菜单后,微信会把点击事件推送给开发者,请注意,点击菜单弹出子菜单,不会产生上报。
   /// </summary>
   public const string Click = "CLICK";
}

/// <summary>
///  常用常量配置
/// </summary>
public class RequestMsgType
{
    // 各种消息类型,除了扫带二维码事件
    /// <summary>
    /// 文本消息
    /// </summary>
    public const string Text = "text";

    /// <summary>
    /// 图片消息
    /// </summary>
    public const string Image = "image";

    /// <summary>
    /// 语音消息
    /// </summary>
    public const string Voice = "voice";

    /// <summary>
    /// 视频消息
    /// </summary>
    public const string Video = "video";

    /// <summary>
    /// 小视频消息
    /// </summary>
    public const string ShortVideo = "shortvideo";

    /// <summary>
    /// 地理位置消息
    /// </summary>
    public const string Location = "location";

    /// <summary>
    /// 链接消息
    /// </summary>
    public const string Link = "link";

    /// <summary>
    /// 事件推送消息
    /// </summary>
    public const string MessageEvent = "event";
}

参考
uni-app使用微信小程序原生组件

  • 10
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

假装我不帅

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值