草庐IT

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

gmval 2023-03-28 原文

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

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

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

功能介绍

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

  • 订阅消息推送位置:服务通知
  • 订阅消息下发条件:用户自主订阅
  • 订阅消息卡片跳转能力:点击查看详情可跳转至该小程序的页面

消息类型

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、为了能快速表达清楚意思,以上前端与服务端代码,都是精简过的,万万不可直接使用!

有关uni-app + .NET 7实现微信小程序订阅消息推送的更多相关文章

  1. ruby-on-rails - Ruby net/ldap 模块中的内存泄漏 - 2

    作为我的Rails应用程序的一部分,我编写了一个小导入程序,它从我们的LDAP系统中吸取数据并将其塞入一个用户表中。不幸的是,与LDAP相关的代码在遍历我们的32K用户时泄漏了大量内存,我一直无法弄清楚如何解决这个问题。这个问题似乎在某种程度上与LDAP库有关,因为当我删除对LDAP内容的调用时,内存使用情况会很好地稳定下来。此外,不断增加的对象是Net::BER::BerIdentifiedString和Net::BER::BerIdentifiedArray,它们都是LDAP库的一部分。当我运行导入时,内存使用量最终达到超过1GB的峰值。如果问题存在,我需要找到一些方法来更正我的代

  2. ruby - 如何模拟 Net::HTTP::Post? - 2

    是的,我知道最好使用webmock,但我想知道如何在RSpec中模拟此方法:defmethod_to_testurl=URI.parseurireq=Net::HTTP::Post.newurl.pathres=Net::HTTP.start(url.host,url.port)do|http|http.requestreq,foo:1endresend这是RSpec:let(:uri){'http://example.com'}specify'HTTPcall'dohttp=mock:httpNet::HTTP.stub!(:start).and_yieldhttphttp.shou

  3. ruby - RuntimeError(自动加载常量 Apps 多线程时检测到循环依赖 - 2

    我收到这个错误:RuntimeError(自动加载常量Apps时检测到循环依赖当我使用多线程时。下面是我的代码。为什么会这样?我尝试多线程的原因是因为我正在编写一个HTML抓取应用程序。对Nokogiri::HTML(open())的调用是一个同步阻塞调用,需要1秒才能返回,我有100,000多个页面要访问,所以我试图运行多个线程来解决这个问题。有更好的方法吗?classToolsController0)app.website=array.join(',')putsapp.websiteelseapp.website="NONE"endapp.saveapps=Apps.order("

  4. ruby - 如何根据特征实现 FactoryGirl 的条件行为 - 2

    我有一个用户工厂。我希望默认情况下确认用户。但是鉴于unconfirmed特征,我不希望它们被确认。虽然我有一个基于实现细节而不是抽象的工作实现,但我想知道如何正确地做到这一点。factory:userdoafter(:create)do|user,evaluator|#unwantedimplementationdetailshereunlessFactoryGirl.factories[:user].defined_traits.map(&:name).include?(:unconfirmed)user.confirm!endendtrait:unconfirmeddoenden

  5. ruby-on-rails - 每次我尝试部署时,我都会得到 - (gcloud.preview.app.deploy) 错误响应 : [4] DEADLINE_EXCEEDED - 2

    我是Google云的新手,我正在尝试对其进行首次部署。我的第一个部署是RubyonRails项目。我基本上是在关注thisguideinthegoogleclouddocumentation.唯一的区别是我使用的是我自己的项目,而不是他们提供的“helloworld”项目。这是我的app.yaml文件runtime:customvm:trueentrypoint:bundleexecrackup-p8080-Eproductionconfig.ruresources:cpu:0.5memory_gb:1.3disk_size_gb:10当我转到我的项目目录并运行gcloudprevie

  6. ruby-on-rails - 如何重命名或移动 Rails 的 README_FOR_APP - 2

    当我在我的Rails应用程序根目录中运行rakedoc:app时,API文档是使用/doc/README_FOR_APP作为主页生成的。我想向该文件添加.rdoc扩展名,以便它在GitHub上正确呈现。更好的是,我想将它移动到应用程序根目录(/README.rdoc)。有没有办法通过修改包含的rake/rdoctask任务在我的Rakefile中执行此操作?是否有某个地方可以查找可以修改的主页文件的名称?还是我必须编写一个新的Rake任务?额外的问题:Rails应用程序的两个单独文件/README和/doc/README_FOR_APP背后的逻辑是什么?为什么不只有一个?

  7. ruby - Net::HTTP 获取源代码和状态 - 2

    我目前正在使用以下方法获取页面的源代码:Net::HTTP.get(URI.parse(page.url))我还想获取HTTP状态,而无需发出第二个请求。有没有办法用另一种方法做到这一点?我一直在查看文档,但似乎找不到我要找的东西。 最佳答案 在我看来,除非您需要一些真正的低级访问或控制,否则最好使用Ruby的内置Open::URI模块:require'open-uri'io=open('http://www.example.org/')#=>#body=io.read[0,50]#=>"["200","OK"]io.base_ur

  8. 华为OD机试用Python实现 -【明明的随机数】 2023Q1A - 2

    华为OD机试题本篇题目:明明的随机数题目输入描述输出描述:示例1输入输出说明代码编写思路最近更新的博客华为od2023|什么是华为od,od薪资待遇,od机试题清单华为OD机试真题大全,用Python解华为机试题|机试宝典【华为OD机试】全流程解析+经验分享,题型分享,防作弊指南华为o

  9. 基于C#实现简易绘图工具【100010177】 - 2

    C#实现简易绘图工具一.引言实验目的:通过制作窗体应用程序(C#画图软件),熟悉基本的窗体设计过程以及控件设计,事件处理等,熟悉使用C#的winform窗体进行绘图的基本步骤,对于面向对象编程有更加深刻的体会.Tutorial任务设计一个具有基本功能的画图软件**·包括简单的新建文件,保存,重新绘图等功能**·实现一些基本图形的绘制,包括铅笔和基本形状等,学习橡皮工具的创建**·设计一个合理舒适的UI界面**注明:你可能需要先了解一些关于winform窗体应用程序绘图的基本知识,以及关于GDI+类和结构的知识二.实验环境Windows系统下的visualstudio2017C#窗体应用程序三.

  10. Get https://registry-1.docker.io/v2/: net/http: request canceled while waiting - 2

    1.错误信息:Errorresponsefromdaemon:Gethttps://registry-1.docker.io/v2/:net/http:requestcanceledwhilewaitingforconnection(Client.Timeoutexceededwhileawaitingheaders)或者:Errorresponsefromdaemon:Gethttps://registry-1.docker.io/v2/:net/http:TLShandshaketimeout2.报错原因:docker使用的镜像网址默认为国外,下载容易超时,需要修改成国内镜像地址(首先阿里

随机推荐