uni-app + .NET 7实现微信小程序订阅消息推送

微信小程序的订阅消息是小程序的重要能力之一,为实现服务的闭环提供更优的体验。订阅消息我们应该经常见到,比如下单成功之后的服务通知,支付成功后的支付成功通知,都属于小程序的订阅消息。

本文只实现一次性订阅的功能,至于长期订阅设备订阅,有机会碰到再进行研究。

在开始之前,我们先看看微信小程序订阅消息的介绍:

功能介绍

消息能力是小程序能力中的重要组成,我们为开发者提供了订阅消息能力,以便实现服务的闭环和更优的体验。

  • 订阅消息推送位置:服务通知

  • 订阅消息下发条件:用户自主订阅

  • 订阅消息卡片跳转能力:点击查看详情可跳转至该小程序的页面

在这里插入图片描述

消息类型

1. 一次性订阅消息

一次性订阅消息用于解决用户使用小程序后,后续服务环节的通知问题。用户自主订阅后,开发者可不限时间地下发一条对应的服务消息;每条消息可单独订阅或退订。

2. 长期订阅消息

一次性订阅消息可满足小程序的大部分服务场景需求,但线下公共服务领域存在一次性订阅无法满足的场景,如航班延误,需根据航班实时动态来多次发送消息提醒。为便于服务,我们提供了长期性订阅消息,用户订阅一次后,开发者可长期下发多条消息。

目前长期性订阅消息仅向政务民生、医疗、交通、金融、教育等线下公共服务开放,后期将逐步支持到其他线下公共服务业务。

所以我们普通小程序,在注册成功后,订阅消息的模板选择,只有一次性订阅的选项,没有长期订阅的选项。
在这里插入图片描述

3. 设备订阅消息

设备订阅消息是一种特殊类型的订阅消息,它属于长期订阅消息类型,且需要完成「设备接入」才能使用。

了解了小程序订阅消息之后,我们开始进入正题!

基本流程

注意事项

由于后面的文章还很长,注意事项优先发出来,可能看到这里已经解决了你的问题。

  • 一次性模板 id 和永久模板 id 不可同时使用。

  • 低版本基础库2.4.4~2.8.3 已支持订阅消息接口调用,仅支持传入一个一次性 tmplId / 永久 tmplId。

  • 2.8.2 版本开始,用户发生点击行为或者发起支付回调后才可以调起订阅消息界面

  • 2.10.0 版本开始,开发版和体验版小程序将禁止使用模板消息 formId。

  • 一次授权调用里,每个 tmplId 对应的模板标题不能存在相同的,若出现相同的,只保留一个。

  • 2.10.0 版本开始,支持订阅语音消息提醒

特别注意第三条,版本库是2.8.2及以上的时候,订阅消息必须发生点击行为或是发起支付回调后,才可以调起订阅消息的界面。这个点击行为没有特别要求。比如一个表单,点击提交按钮后,也是可以调起订阅消息界面的。支付后的回调不需要点击行为,也可以调起订阅消息界面。

获取模板ID

在微信公众平台登录小程序,在订阅消息功能下,进入到我的模板,找到模板,并将模板id复制出来,如果没有模板,需要先添加模板,再获取模板id
在这里插入图片描述

要添加新模板,点击选用按钮,在公共模板库中选择需要的模板,添加就可以了。
在这里插入图片描述

有很多文章说,如果没有合适的模板,可以创建自定义模板。但如果你真想去创建自定义模板,会发现根本找不到地方。
如果想创建自定义模板,可通过以下方式进行。
1、点击选用按钮,来到公共模板库。(公共模板库中的模板,与你小程序的服务类目相关)
2、在搜索框中,输入比较长的关键词。
3、点击搜素,如果还是能匹配出模板来,则重新调整关键词,直到没有任何搜索结果为止。
4、点击页面中的帮忙我们完善模板库,进行自定义模板设置。
在这里插入图片描述
在这里插入图片描述

创建自定义模板的时候,一定要仔细阅读申请模板的流程,尤其是第1条。我单拉出来重点标注一下,因为没仔细看第1条,第一次申请的几个模板白白等了好几天。
模板标题需体现具体的服务场景,要求以“通知”或“提醒”结尾,如:物流到货通知、交易提醒。

看到这里,会发现以上大部分跟网上的文章没啥区别,别急,正文来了!

uni-app代码

前端实现的是点击提交按钮,保存表单,保存成功后发送订阅消息,在pages/index/index.vue下编写如下代码:

<template>
	<view>
		<view class="setp">
			<publishStep :list="setpList" :current="0" mode="number" active-color="#eb3572"></publishStep>
		</view>
		<view class="container">
			<u-form :model="form" ref="uForm" :rules="rules" :error-type="errorType">
				<u-form-item label="姓名" label-width="160rpx" :border-bottom="true" :label-style="{'font-size':'28rpx'}" prop="realName">
					<u-input v-model="form.realName" placeholder="" input-align="right" />
				</u-form-item>
				
				<u-form-item label="服务时间" label-width="160rpx" :border-bottom="true" :label-style="{'font-size':'28rpx'}"
				 right-icon="arrow-right" prop="serviceTime">
					<u-input v-model="form.serviceTime" placeholder="请选择服务时间" :disabled="true" input-align="right" @click="timeShow=true" />
				</u-form-item>
				
				<u-form-item label="服务地址" label-width="160rpx" :border-bottom="true" :label-style="{'font-size':'28rpx'}" prop="serviceAddress">
					<u-input v-model="form.serviceAddress" placeholder="" input-align="right" @click="selectAddress" />
				</u-form-item>
				<u-form-item label="联系电话" label-width="160rpx" :border-bottom="true" :label-style="{'font-size':'28rpx'}"
				 prop="lxtel">
					<u-input v-model="form.lxtel" type="number" placeholder="请输入联系电话" input-align="right" :clearable="false" />
				</u-form-item>
				<u-form-item label="需求描述" label-width="160rpx" :border-bottom="true" :label-style="{'font-size':'28rpx'}"
				 prop="remarks">
					<u-input v-model="form.remarks" type="text" placeholder="请输入您的需求" input-align="right" :clearable="false" />
				</u-form-item>
			</u-form>
		</view>
		<view style="height: 160rpx;"></view>
		<view class="bottom_nav">
			<view class="buttom_box padding-horizontal-20 padding-vertical-10">
				<u-button type="error" @click="submitForm" :loading="submit_loading" style="height: 100rpx; font-weight: bold; font-size: 36rpx;">确认提交</u-button>
			</view>
		</view>
		<u-picker mode="time" v-model="timeShow" :params="timeParams" @confirm="timeConfirm"></u-picker>
	</view>
</template>
<script>
	export default {
		data() {
			return {
				
				form:{
					realName:"",
					serviceTime:'',
					serviceAddress:"",
					lxtel:"",
					remarks:""
					
				},
				rules:{
					realName: [{
						required: true,
						message: "请填写您的姓名",
						trigger: 'change'
					}],
					
					serviceTime: [{
						required: true,
						message: "请选择服务时间",
						trigger: 'change'
					}],
					
					lxtel: [{
						required: true,
						message: "请输入联系电话",
						trigger: 'change'
					}],
				},
				errorType: ['toast'],
				
				timeShow:false,
				timeParams:{
					year: true,
					month: true,
					day: true,
					hour: false,
					minute: false,
					second: false
				},
				submit_loading:false,
			}
		},
		
		
		onReady() {
			this.$refs.uForm.setRules(this.rules);
		},
		
		onLoad(params) {
			let that = this;
		},
		methods: {
			
			timeConfirm(e){
				let that = this;
				that.form.serviceTime = e.year +"-"+e.month+"-"+e.day
			},
			gotoOrder(){
			    uni.redirectTo({
					url:"/pages/order/order"
			    })
			},
			submitForm(){
				 let that = this;
				this.$refs.uForm.validate(valid=>{
					if (valid){
						that.$u.api.submit_order(that.form).then(res => {
							if (res.success) {
								let data = res.data;
								uni.showToast({
									title: '提交成功',
									icon: 'success'
								})
								// #ifdef MP-WEIXIN
								uni.requestSubscribeMessage({
									tmplIds:['XXXXXXXXXXX'], //这里填写tempid
									success:function(subscribeMessageRes){
										if(subscribeMessageRes.errMsg=="requestSubscribeMessage:ok"){
											if(subscribeMessageRes.XXXXXXXXXXX=="accept"){
												uni.login({
													provider: 'weixin',
													success:function(loginRes){
														if(loginRes.errMsg=="login:ok"){
															const code = loginRes.code;
															that.$u.api.sendSubscribeMessage({
																"code":code,
																"orderId":data.orderId
															}).then(res=>{
																that.gotoOrder()
															})
														}else{
															that.gotoOrder()
														}
													},
													fail() {
														that.gotoOrder()
													}
												})
											}else{
												that.gotoOrder()
											}
										}else{
											that.gotoOrder()
										}
										
									},
									fail:function(){
										that.gotoOrder()
									}
								})
								// #endif
								
							} else {
								uni.$u.toast(res.message);
							}
						});
					}
				})
				
			}
		}
	}
</script>

<style>
	.setp{ padding: 40rpx 0;}
	.bottom_nav {
		position: fixed;
		width: 100%;
		height: 100rpx;
		left: 0;
		bottom: 0;
		z-index: 9999;
		background: #FFFFFF;
		border-top: 1rpx #f3f3f3 solid;
	}
</style>

这里的流程分为3步:
1、提交表单,服务端返回订单号(orderId)
2、使用uni.requestSubscribeMessage,调起授权框,当点击同意后,进入第三步。调起授权后,如果用户同意,回调函数的参数subscribeMessageRes有两个对象:errMsgXXXXXXXXXXX,errMsg不必多说。主要是这个XXXXXXXXXXX是什么。XXXXXXXXXXX是授权生成的,目测来看就是模板Id。
3、使用uni.login,获取code
4、将codeorderId发送到服务器,服务器通过code获取到openId,再根据orderId获取到具体订单数据。
5、发送模板消息。

如果不出意外的话,提交成功后,弹出如下授权框
在这里插入图片描述

服务端代码

服务端ORM使用SqlSugar,微信小程序接口使用SKIT.FlurlHttpClient.Wechat库。

生成订单

提交订单,这里只做演示,具体的代码自己实现下就可以了!

[HttpPost]public async Task<AjaxResult> SubmitOrder(order model){//生成订单号
    model.order_no = DateTime.Now.ToString("yyyyMMddHHssfffff");
    model.addtime = DateTime.Now;
    //ExecuteReturnIdentity方法会返回自增id
    var id = await db.Insertable(model).ExecuteReturnIdentity();
    return new AjaxResult(){
     success=true,
     data = id    };}

AjaxResult.cs

public class AjaxResult
{
/// <summary>
/// 是否成功
/// </summary>
public bool success { get; set; } = true;

/// <summary>
/// 错误代码
/// </summary>
public int code { get; set; } = 0;

/// <summary>
/// 返回消息
/// </summary>
public string message { get; set; }
/// <summary>
/// 返回数据
/// </summary>
public object data{ get; set;}

}

order.cs

[SugarTable("order")]public class order{
     /// <summary>
     /// 主键,自增Id
     /// </summary>
    [SugarColumn(IsPrimaryKey = true)]
    public int id { get; set; }
     /// <summary>
     /// 订单编号
     /// </summary>
    public string order_no { get; set; }
    /// <summary>
    /// 姓名
    /// </summary>
    public string realName { get; set; }
    /// <summary>
    /// 时间
    /// </summary>
	public DateTime serviceTime { get; set; }
	/// <summary>
    /// 地址
    /// </summary>
	public string serviceAddress { get; set; }
	/// <summary>
    /// 联系电话
    /// </summary>
	public string lxtel { get; set; }
	/// <summary>
    /// 备注
    /// </summary>
	public string remarks { get; set; }
	/// <summary>
    /// 创建时间
    /// </summary>
	public DateTime addtime { get; set; }}

发送模板消息

发送一次性订阅的模板消息,传的参数为前端获取的codeorderId。根据订单编号获取订单信息,以便在订阅消息中,设置小程序信息以及打开路径。code用于获取用户的openId

[HttpPost]
public async Task<AjaxResult> SendSubscribeMessage(string code,string orderId)
{
    AjaxResult result = new AjaxResult();
    if (string.IsNullOrEmpty(code) || string.IsNullOrEmpty(orderId))
    {
        result.success = false;
        result.message = "参数错误";
        return result;
    }
    var order_model = await db.Queryable<order>().InSingleAsync(orderId);
    if(order_model is null)
    {
        result.success = false;
        result.message = "参数错误";
        return result;
    }
    //初始化WechatApiClient
    var options = new WechatApiClientOptions()
    {
        AppId = "appId",
        AppSecret = "appSecret "
    };
    var client = new WechatApiClient(options);
    //获取openId
    var request = new SnsJsCode2SessionRequest();
    request.JsCode = code;
    var response = await client.ExecuteSnsJsCode2SessionAsync(request);
    string openId = response.OpenId;
    //获取token
    var tokenRequest = new CgibinTokenRequest();
    var tokenResponse = await client.ExecuteCgibinTokenAsync(tokenRequest);
    var token = tokenResponse.AccessToken;
    //发送模板消息
    var messageRequest = new CgibinMessageSubscribeSendRequest();
    IDictionary<string, CgibinMessageSubscribeSendRequest.Types.DataItem> messageData = new Dictionary<string, CgibinMessageSubscribeSendRequest.Types.DataItem>
            {
                {
                    "params1",
                     new CgibinMessageSubscribeSendRequest.Types.DataItem() {Value=order_model.order_no}
                },
                {
                    "params1",
                    new CgibinMessageSubscribeSendRequest.Types.DataItem(){Value=order_model.userNmae}
                },
                {
                    "params3",
                    new CgibinMessageSubscribeSendRequest.Types.DataItem(){Value=order_model.serviceTime}
                },
                {
                    "params4",
                    new CgibinMessageSubscribeSendRequest.Types.DataItem(){Value=order_model.serviceAddress}
                },
                {
                    "params5",
                    new CgibinMessageSubscribeSendRequest.Types.DataItem(){Value=order_model.addtime.ToString("yyyy-MM-dd HH:ss")}
                }
            };
     messageRequest.AccessToken = token;
     messageRequest.ToUserOpenId = openId;
     messageRequest.TemplateId = "XXXXXXXXXXX"; 
     messageRequest.MiniProgramState = "developer";
     //微信小程序要跳转的地址。可以加参数
     messageRequest.MiniProgramPagePath = "/pages/order/order_details?id=" + order_model.id;
     messageRequest.Data = messageData;
     var messageResponse = await client.ExecuteCgibinMessageSubscribeSendAsync(messageRequest);
     if(messageResponse.ErrorCode==0)
     {
         result.success=true;
         result.message = "ok";
         return result;
     }
     result.success = false;
     result.message = "error";
     return result;
}


构造模板消息的时候,使用IDictionary<string, CgibinMessageSubscribeSendRequest.Types.DataItem> messageData = new Dictionary<string, CgibinMessageSubscribeSendRequest.Types.DataItem>来进行构造,
假设一个模板消息的详细内容是这样的:
在这里插入图片描述

  • 那么上面代码中的params1 就是character_string22,同理params2就是thing7。也就是说。IDictionary的key就是模板中.DATA前面的内容。

  • messageRequest.TemplateId,要与前端的模板Id一致。

  • messageRequest.MiniProgramState表示跳转微信小程序的类型。默认为正式版

    • developer为开发版;

    • trial为体验版;

    • formal为正式版;

如果不出意外的话,你的微信会收到服务通知。点击卡片后,进入小程序的订单详情页面!

总结

1、其实微信小程序的订阅消息和公众号的订阅消息模板还是比较好申请的。如果在类目模板与历史模板中无法找到合适自己的模板,那么自己申请一个模板。审核的话,2-3天就可以收到通知了。
需要注意的是,申请模板的时候,最好把各项在本地保留一份。因为一旦提交申请,在公众号或小程序后台,你就找不到了。玩意审核没通过,再申请的时候,前面写的啥内容,已经忘的差不多了!

2、感谢SqlSugar,为.Net开发者提供这么强大的ORM。真的是太方便了。
3、感谢SKIT.FlurlHttpClient.Wechat,为.Net开发者提供这么便捷的工具。
4、为了能快速表达清楚意思,以上前端与服务端代码,都是精简过的,万万不可直接使用!