-
2020-12-21 11:12:22
上级对下级的通知
【篇一:函、通知、请示的区别及例文】
函
函是不相隶属机关之间相互商洽工作、询问和答复问题,或者向有
关主管部门请求批准事项时所使用的公文。
函是应用写作实践中的
一种常用文体。
1
简介
函作为公文中惟一的一种平行文种,其适用的范围相当广泛。在行
文方向上,不仅可以在平行机关之间行文,而且可以在不相隶属的
机关之间行文,其中包括上级机关或者下级机关行文。在适用的内
容方面,它除了主要用于不相隶属机关相互商洽工作、询问和答复
问题外,也可以向有关主管部门请求批准事项,向上级机关询问具
体事项,还可以用于上级机关答复下级机关的询问或请求批准事项,
以及上级机关催办下级机关有关事宜,如要求下级机关函报报表、
材料、统计数字等。此外,函有时还可用于上级机关对某件原发文
件作较小的补充或更正。不过这种情况并不多见。
2
特点
(一)沟通性
函对于不相隶属机关之间相互商洽工作、询问和答复问题,起着沟
通作用,充分显示平行文种的功能,这是其他公文所不具备的特点。
(二)灵活性
表现在两个方面:一是行文关系灵活。函是平行公文,但是它除了
平行行文外,还可以向上行文或向下行文,没有其他文种那样严格
的特殊行文关系的限制。二是格式灵活,除了国家高级机关的主要
函必须按照公文的格式、行文要求行文外,其他一般函,比较灵活
自便,
也可以按照公文的格式及行文要求办。可以有文头版,也可以没有
文头版,不编发文字号,甚至可以不拟标题。
(三)单一性
函的主体内容应该具备单一性的特点,一份函只宜写一件事项。
3
作用
“
函
”
有下列三方面的作用:
(
一
)
相互商洽工作。如调动干部,联系参观、学习,联系业务,邀请
参观指导
??
。
更多相关内容 -
四部委联手下发通知,车联网遇政策“东风”.pdf
2021-07-16 21:21:15四部委联手下发通知,车联网遇政策“东风”.pdf -
微信小程序下发消息通知
2021-03-29 17:13:50话不多说,先熟悉微信小程序官网操作逻辑实现:(别搞错了!...步骤二:获取下发权限 (前端的活,哈哈…) let temId = 'QWERTYUOP1234567890' // 小程序配置模板信息-模板ID wx.getSetting({ withS话不多说,先熟悉微信小程序官网操作逻辑实现:(别搞错了!)
https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/subscribe-message.html
步骤一:获取模板 ID (这个没什么好说的,根据界面去配置模板就好)
步骤二:获取下发权限 (前端的活,哈哈…)
let temId = 'QWERTYUOP1234567890' // 小程序配置模板信息-模板ID wx.getSetting({ withSubscriptions: true,//是否同时获取用户订阅消息的订阅状态,默认不获取 success: (res)=> { console.log(res) if (res.subscriptionsSetting && res.subscriptionsSetting.itemSettings && res.subscriptionsSetting.itemSettings[temId] == "reject"){ //打开设置去设置 this.openConfirm('检测到您没打开推送权限,是否去设置打开?') }else { wx.requestSubscribeMessage({ tmplIds: [temId], success: (res)=> { if (res[temId] == 'accept'){ let obj ={ "thing1": { "value": '风里来雨里去' }, "name2": { "value": '张三' } ,"date6": { "value": '2021-03-29' } ,"amount7": { "value": '200' } ,"thing3": { "value": '远航' } } _that.$http.request({ url:'/api/applets/xxxxxx', method:"post", data:{ templateId: temId, pageUrl:'/pages/index/index', jsonData: JSON.stringify(obj), } }).then(res=>{ console.log(res); }) } }, fail: (res)=> { console.info(res) }, }) } } })
步骤三:调用接口下发订阅消息(用户登录时记得存储openId,后续会用到)
调用接口时最好是真机调试
下发消息通知:
POST https://api.weixin.qq.com/cgi-bin/message/subscribe/send?access_token=ACCESS_TOKEN特别注意参数类别与参数值限制
这里涉及到两个API:一个获取token另外一个下发消息
/** * 返回的 JSON 数据包 * @param appId * @param appSecret * @return */ // https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/access-token/auth.getAccessToken.html public static String getWxMiniToken(String appId, String appSecret) { String requestUrl = "https://api.weixin.qq.com/cgi-bin/token"; Map<String, String> requestParam = new HashMap<>(); requestParam.put("grant_type", "client_credential"); //默认参数 requestParam.put("appid", appId); //小程序appId requestParam.put("secret", appSecret); //小程序secret return UrlUtils.sendGet(requestUrl, requestParam); } /** * 微信小程序-订阅 下发消息通知 * @param accessToken * @param openId * @param templateId * @param pageUrl * @param jsonData * @return */ public static String wxMiniSend(String wxModel, String accessToken, String openId, String templateId, String pageUrl, String jsonData){ String requestUrl = "https://api.weixin.qq.com/cgi-bin/message/subscribe/send?access_token=" + accessToken; Map<String, Object> requestParam = new HashMap<>(); requestParam.put("touser", openId); //接收者(用户)的 openid requestParam.put("template_id", templateId); //所需下发的订阅模板id requestParam.put("page", pageUrl); //点击模板卡片后的跳转页面,仅限本小程序内的页面 requestParam.put("data", JSONObject.parse(jsonData)); //模板内容,格式形如 { "key1": { "value": any }, "key2": { "value": any } } requestParam.put("miniprogram_state", wxModel); //跳转小程序类型:developer为开发版;trial为体验版;formal为正式版;默认为正式版 requestParam.put("lang", "zh_CN"); //语言类型,支持zh_CN(简体中文)、en_US(英文)、zh_HK(繁体中文)、zh_TW(繁体中文),默认为zh_CN return UrlUtils.sendPostJson(requestUrl, JSONObject.toJSONString(requestParam)); }
-
关于下发年度公司考核方案的通知.pdf
2021-10-14 20:04:53关于下发年度公司考核方案的通知.pdf -
《关于下发有关精算规定的通知》
2020-12-20 00:52:09规范 -
关于下发《涉农贷款专项统计制度》补充说明的通知..doc
2021-11-06 11:54:55关于下发《涉农贷款专项统计制度》补充说明的通知..doc -
关于下发《人力资源管理流程操作指引》的通知.doc
2022-01-30 09:06:19关于下发《人力资源管理流程操作指引》的通知.doc -
监理单位下发监理通知单后施工单位拒不整改监理报告.docx
2021-08-02 11:39:12理单位下发监理通知单后施工单位拒不整改监理报告 -
参考资料-关于下发《培训管理制度》的通知.zip
2022-03-05 03:16:09参考资料-关于下发《培训管理制度》的通知.zip -
关于下发商场渠道新产品终端销量考核指标的通知
2020-12-30 10:47:01这一款整理发布的关于下发商场渠道新产品终端销量考核指标的通知,适合超市管理人员学习参考超...该文档为关于下发商场渠道新产品终端销量考核指标的通知,是一份很不错的参考资料,具有较高参考价值,感兴趣的可以... -
微信小程序云开发-订阅消息下发系统
2021-11-28 20:57:50稍加分析,其实核心的模块是实现下发订阅消息的系统,把这个做了,稍加改造就可以实现管理员主动下发以及订单状态更改后自动下发的功能,毕竟这两个功能本质都是给指定的用户下发消息。 二、选择实现方案一、前言
本篇文章需要实现的是一个基于云开发搭配CMS实现的消息下发系统。当然不是简简单单的实现功能就好,在实现之前我们需要考虑这个功能的可拓展性以及可复性。
具体的业务场景:管理员登陆CMS系统后可以创建一条消息,下发给指定的用户。或者更新订单状态后,自动下发订阅消息给该用户。
这种类似的场景在项目开发中还是很普遍的。稍加分析,其实核心的模块是实现下发订阅消息的系统,把这个做了,稍加改造就可以实现管理员主动下发以及订单状态更改后自动下发的功能,毕竟这两个功能本质都是给指定的用户下发消息。
二、选择实现方案
小程序触达用户,实现业务逻辑的闭环,这是一个非常重要的事情,基本是每个较为完善的小程序都必须实现功能。使用场景非常的广泛,例如订单处理进度状态的改变,我们需要在CMS上更新订单的状态,从处理中改为处理完成,需要通知到用户。
当前小程序主动发送消息给用户的常用途径有以下2种:
- 订阅消息
- 统一服务消息(公众号的模板消息)
订阅消息是目前较为主流的做法,基本是小程序长期维护稳定的一种方式,例如小程序的模板消息就被砍掉了,不是很稳定。订阅消息熟悉小程序开发的都知道,最大的痛点就是,必须用户订阅才可下发,而且是订阅一次发送一次。(长期订阅消息除外,但是普通企业以及用户基本申请不了)。但它依旧是目前的主流做法,也是官方推荐的做法。
统一服务消息,其实就是当前服务号的模板消息,只不过它可以使用小程序的openid通过绑定同一个开放平台的服务号下发模板消息。虽然需要操作的步骤多了一些,但是好处是不言而喻的,只要关注了服务号的用户,就可以无限次给他发送模板消息,不需要像订阅消息那样,订阅一次发送一次。缺点也有,一是需要绑定开放平台,比较繁琐一点。二是需要挂个服务号,用户没有关注也没办法给他下发。三是不稳定,公众号的模板消息在2020年就准备被下架,但是由于开发者的反对意见比较多,也就一直延迟到现在还在使用,不排除后期砍掉这个功能,假设砍掉的话,这个基本就废了。
简单讲了一下,希望大家在小程序需求分析以及评估的时候能够考虑这两者的优缺点,进行取舍。至于哪个好哪个坏,其实没有一个结论,只需要根据项目的规划进行选择就好。合适就好。当然,如果选择统一服务消息的话,就要考虑好后期此功能被砍掉的替代方案。
本文的话,选择小程序的订阅消息进行实现。两种方案的主要实现过程大致相同,所以基本可以类比的。
三、实现思路
创建一个消息队列(msgList)集合,专门用来存放要下发的消息的数据,例如接收者的openid,发送的参数啥的。当需要下发消息的时候就往这个表里插入一条记录,然后下发就可以了。下发完成后顺便把下发的结果啥的更新进来,就可以确定有没有通知到位了。
但是此时需要考虑的问题有:
1、订阅消息的模板的参数个数以及参数名都不一致,而且对于参数的类型也有较为严格的限制,如何将所有参数统一。
例如模版A需要三个参数:订单进度、订单金额、购买物品。而模板B需要的是5个参数:收货人、手机号、快递名称、快递订单号、预计送达时间。
这种情况下,参数类型、参数名、参数值都不尽相同该如何进行统一?
解决方案有两种:一是将数据库记录的参数名称改为参数一、参数二、参数三依此类推下去,然后在代码里根据下发的模版类型进行参数的插入即可。至于参数个数,直接定为10个,大部分情况下10个参数已经足够了。二是将参数存放在数组里。这样就不用在意参数的个数限制了。我个人的话,采用第二种方案,因为可拓展性比较强。
当然这是基于当后台是云开发自带的CMS的情况下需要考虑的事情,如果是自己开发的管理后台就没这种必要了。直接根据选择的模板类型进行参数的个数以及类型的显示即可。
2、当插入消息记录后,如何触发订阅消息的下发,这个也是一个问题。解决方案也有两种:第一种是通过定时器触发,每分钟遍历一下集合里有没有需要下发的记录,如果有就执行下发逻辑。这种做法的好处就是开发方便;坏处就是下发不是即时的,也会比较消耗资源,毕竟每分钟都要执行一下。第二种是通过数据库底层的触发器来实现,创建一个触发器,监听消息集合的插入事件,当有记录插入时就触发消息下发逻辑。这种的好处就是即时下发、不会浪费资源;坏处是开发麻烦一点,需要多挂一个触发器。我个人的话,直接采用第二种方案,因为它值得选择。
tips:这就会有小伙伴有疑问了。为啥不使用CMS的云函数类型WebHook呢?因为CMS触发是没有登录态的,简单来说就是它没办法触发云调用的API,因为它没有权限。所以使用这种方案目前是行不通的,只是目前行不通,不排除它后期更新后支持云调用,大家留意一下官方消息吧。
四、实现过程
很多步骤都是很基础的,如果已经做了的,就可以跳过,没有的话,看一下需要的准备的东西,按照自己的实际情况去弄一下就好,基本没什么难度。
第一步:准备工作
首先肯定要创建一个用户集合啦,不然哪来的openid呢。然后就是创建消息列表集合以及订单集合。再然后就是去申请模板啥的了。不会的自行百度即可。如果没有创建以及配置CMS的当然也需要创建配置一下,创建以及配置的流程我就不说了,简简单单,自行搞定。
第二步:写一个简单的小程序
直接引用我之前写的文章吧,没有的朋友直接cv大法即可。
第三步:创建下发消息的云函数
名字随意,后面用到的地方保持一致就好。我因为已经创建了一个sendSubMsg了,所以这个就叫sendSubMsgV2好了。不想删除之前那个。创建好,先不用编写代码。等下再编写。
第四步:创建触发器
前往腾讯云控制台,选择小程序公众号登陆。
登陆后选择云开发
然后选择你对应的环境
进入后添加触发器
创建完成。这时候,只要往subMsgList集合里插入数据的时候就会触发指定的云函数了。
关于触发器更多的用法可以看一下官方文档。
触发器官方文档第五步:创建CMS模型
创建一下简单的用户列表以及消息列表的模型,至于订单列表的话,可以先不创建,看一下就会了,不一定要创建。后面我也会实现一下搭配订单来下发消息的过程。
简单搞一个实例,其他的,根据自己的实际需求来就好。
顺便提一下:订阅消息模板我是申请了两个,没有的话,去申请一下就好。
第六步:代码实现
首先是编写下发订阅消息的云函数代码。
创建一个js文件:templateData.js。这个文件主要是放置我们所需要的各种模板消息的数据,然后返回一个发送数据对象。
对于一些参数类型的限制以及参数名称,直接根据文档以及申请的模板详情来写就行。每个模板的参数都不尽相同。对于点击跳转的页面、跳转的小程序版本(开发版、体验版、正式版)等,需要动态设置的就使用参数的形式实现就可以了。
/** * 会员充值成功模板消息数据 * @param {string} openid 接收者openid * @param {string} name 会员姓名 * @param {string} cardNumber 会员卡号 * @param {number} payMoney 充值金额 * @param {number} balance 账户余额 * @param {string} remark 备注 20字内 * */ const vipPaySuccess = (openid, name, cardNumber, payMoney, balance, remark) => { const sendObj = { "touser": openid, "page": 'pages/APITest/APITest', data: { "name1": { "value": name }, "character_string2": { "value": cardNumber }, "amount4": { "value": payMoney }, "amount5": { "value": balance }, "thing13": { "value": remark } }, "templateId": 'AcxnCbDhMKA0bunvuvBa5RJT-OkxmfZfDPB_mTvirxw', "miniprogramState": 'developer' } return sendObj } /** * 订单状态通知模板消息数据 * @param {string} openid 接收者的openid * @param {string} goodsName 商品名称 * @param {number} goodsPrice 商品价格 * @param {string} orderStatus 订单配送状态 * @param {string} remark 备注 */ const orderStatusMsg = (openid, goodsName, goodsPrice, orderStatus, remark) => { const sendObj = { "touser": openid, "page": 'pages/APITest/APITest', data: { "thing2": { "value": goodsName }, "amount3": { "value": goodsPrice + "元" }, "thing11": { "value": orderStatus }, "thing5": { "value": remark } }, "templateId": 'zT99mpoUCb28M02K7ohTO3h81IvBUlgt8JVRn6rkNSc', "miniprogramState": 'developer' } return sendObj } module.exports = { vipPaySuccess, orderStatusMsg }
接下来是云函数index.js的代码编写。编写前先配置一下index.json,因为需要订阅下发的权限,具体的可以查看官方文档。
index.json
{ "permissions": { "openapi": [ "subscribeMessage.send" ] } }
index.js
// 云函数入口文件 const cloud = require('wx-server-sdk') const templateData = require("./templateData") cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV }) const db = cloud.database() // 云函数入口函数 exports.main = async (event) => { console.log(event) //打印一下触发器的入参,看看传入了啥给我们。 const docData = event.data.doc // 获取插入的记录的详细数据 const templateId = docData.templateId const docId = docData._id const openid = docData.openid const parameterArr = docData.parameterArr let sendData = {} let sendRes = {} switch (templateId) { // 会员充值 case "AcxnCbDhMKA0bunvuvBa5RJT-OkxmfZfDPB_mTvirxw": sendData = templateData.vipPaySuccess(openid, parameterArr[0], parameterArr[1], parameterArr[2], parameterArr[3], parameterArr[4]) break // 订单通知 case "zT99mpoUCb28M02K7ohTO3h81IvBUlgt8JVRn6rkNSc": sendData = templateData.orderStatusMsg(openid, parameterArr[0], parameterArr[1], parameterArr[2], parameterArr[3]) break default: return "没有符合的消息模板" } // 发送 try { sendRes = await cloud.openapi.subscribeMessage.send(sendData) } catch (error) { sendRes = error } // 更新发送状态 const nowTime = new Date().getTime() const sendStatus = sendRes.errCode == 0 ? 'success':'fail' const sendStatusMsg = sendRes.errMsg await updateSendStatus(docId, {updateTime: nowTime, sendTime: nowTime,sendStatus, sendStatusMsg}) return sendRes } /** * 更新发送的状态以及返回值 * @param {string} docid 记录唯一ID * @param {object} updateObj 更新的数据对象 */ const updateSendStatus = async (docid, updateObj) => { return await db.collection("subMsgList").doc(docid).update({ data: updateObj }) }
编写完成记得上传一下。
第七步:测试一波
基本上是做完了。现在我们创建一条订单的消息,看看效果如何。
CMS创建记录参数
发送结果
完美。这样一个单一指定用户发送订阅消息的功能模块就完成了。如果还需要执行其他的业务逻辑,自己根据自己的业务需求去编写就好了。
从此只要是涉及指定用户下发订阅消息的需求都可以基于这个功能模块去拓展。
五、拓展:自动下发
接下来实现的是基于订单的状态更新去下发订阅消息的功能了。这次下发的模板选择会员充值成功通知的来做。
简单来说,就是管理员操作订单,将充值状态从充值中更新为充值成功时,下发订阅消息。通知用户充值会员成功。但是这里只是简单的实现一下主要的思路,具体的业务还是需要额外进行处理。这里最主要的还是实现自动下发的功能。
思路是这样的:在CMS创建一个云函数类型的webHook,监听order集合的更新 ,当符合下发条件的时候,就执行云函数,插入一条数据到消息列表,剩下的就基本跟上面的一致了。但是这里的话,有个小细节,并不是每次更新订单都需要通知用户,所以加个标记,当管理员选择需要通知用户的时候再去通知。
创建webHook
创建完成后,也需要创建一个云函数cmsHook,用来执行触发后的逻辑。
编写cmsHook函数
index.js
过程的异常捕获以及处理在实际开发中自己看情况需不需要做吧。我这里就不做了,快速演示一遍就好。主要是将思路分享出来。
// 云函数入口文件 const cloud = require('wx-server-sdk') cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV }) const db = cloud.database() // 云函数入口函数 exports.main = async (event) => { console.log(event) // 打印一下入参,看看传入的参数 const docId = event.actionFilter._id // 更新的记录唯一ID const collection = event.collection // 可以用来判断是哪个集合更新而触发的进而执行不同的逻辑 let res = {} // 执行过程结果 switch (collection) { case "order": let orderData = await getOrderData(docId) orderData = orderData.data if (!orderData.isSendMsg) { return "此次更新无需通知用户" } const openid = orderData._openid // 这里可以根据订单的类型来选择触发那种模板,这里直接写死了 const templateId = "AcxnCbDhMKA0bunvuvBa5RJT-OkxmfZfDPB_mTvirxw" // 实际上这些数据应该来自会员卡数据集合才对,但是这里为了演示,就直接从订单表拿了。 //合理的做法是再创建一个会员集合的。 const parameterArr = [orderData.name, orderData.cardNumber, orderData.payMoney, orderData.balance, orderData.remark] const nowTime = new Date().getTime() const addSendMsgData = { openid, templateId, sendStatus: "wait", parameterArr: parameterArr, createTime: nowTime, updateTime: nowTime, } // 插入消息 const addRes = await insterSubMsg(addSendMsgData) // 更新订单 const updateRes = await updateOrder(docId, { isSendMsg: false }) res.addSendMsg = addRes res.updateOrderRes = updateRes break default: return "不存在此集合的更新逻辑" } return res } /** 根据_id获取订单信息 */ const getOrderData = async (docId) => { return await db.collection("order").doc(docId).get() } /** * 插入发送记录到订阅消息发送集合 */ const insterSubMsg = async (addObj) => { return await db.collection("subMsgList").add({ data: addObj }) } /** 更新订单数据,主要是将通知标记恢复 */ const updateOrder = async (docId, updateObj) => { return await db.collection("order").doc(docId).update({ data: updateObj }) }
测试一波
创建一条订单。(实际订单表这样设计并不合理,所以这个的话就不要吸收了。)
对于是否发送通知标记。这个细节需要注意一下。因为不是每次通知都需要通知到用户。
创建完后更新一下,因为我们只监听了更新的事件。
运行结果
发送失败是正常的,因为我没有订阅这个模板消息,所以逻辑是跑得通的。我也就懒得写订阅这个模板的代码了。大家自己搞定。
六、结语
分享的是思维不是技术。所以很多地方写得并不是很严谨,仅仅是把逻辑跑了一遍。(大佬们手下留情,谢谢)
实际开发中的其他逻辑就不写了,这里只是最简单的实现。
有任何疑问可以在评论区留下。我每天都会进行回复,私聊不回。(为了刷积分)
以上均是本人开发过程中的一些经验总结与领悟,如果有什么不正确的地方,希望大佬们评论区斧正。
💥最后!!!不管这篇文章对你有没有用,既然都看到最后了。
👍赞一个!!!
🤩当然,顺带收藏就最好了。
😎欢迎转载,原创不易,转载请注明出处✍。😊如果你对小程序开发有兴趣或者正在学习小程序开发,可以关注我。每一篇都是原创,每一篇都是干货噢~。
————————————————
版权声明:本文为CSDN博主「super–Yang」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。 -
关于下发动感地带全国宣传推广指导意见的通知.doc
2021-12-24 21:23:09关于下发动感地带全国宣传推广指导意见的通知.doc -
关于下发2020年内部市场化推进工作实施方案的通知.docx
2022-02-10 08:03:12关于下发2020年内部市场化推进工作实施方案的通知.docx -
关于下发《出口水产品加工企业注册卫生规范》的通知.doc
2022-02-19 19:38:47关于下发《出口水产品加工企业注册卫生规范》的通知.doc -
关于下发《出口水产品加工企业注册卫生规范》的通知
2020-12-11 18:36:03关于下发《出口水产品加工企业注册卫生规范》的通知简洁、实用的特性,相信能够为大家利用人力、物力、财...该文档为关于下发《出口水产品加工企业注册卫生规范》的通知,是一份很不错的参考资料,具有较高参考价值... -
关于下发动感地带(M-Zone)市场推广指导意见的通知.doc
2021-12-24 21:23:08关于下发动感地带(M-Zone)市场推广指导意见的通知.doc -
国家版权局关于下发对台港澳版权贸易示范出版合同范本的通知
2020-12-10 09:05:02小编带来了一篇国家版权局关于下发对台港澳版权贸易示范出版合同范本的通知,在进行交易的时候,合同是最...该文档为国家版权局关于下发对台港澳版权贸易示范出版合同范本的通知,是一份很不错的参考资料,具有较高... -
ZG银行有关下发ZG银行对外商投资企业贷款业务操作指南通知.docx
2021-12-17 23:00:09ZG银行有关下发ZG银行对外商投资企业贷款业务操作指南通知.docx -
关于下发《发电有限责任公司工会会员会籍管理办法》的通知.pdf
2021-11-23 02:03:44关于下发《发电有限责任公司工会会员会籍管理办法》的通知.pdf -
关于下发《出口食品厂、库卫生注册申请书》格式的通知.doc
2022-02-19 19:37:46关于下发《出口食品厂、库卫生注册申请书》格式的通知.doc -
uni-app中开发消息实时通知模块
2021-09-25 16:26:14开发代码 总结 需求背景 最近客户想做一个社团购物的软件,技术选型是采用uni-app开发的微信小程序,其中有一个功能是想开发一个买家和卖家沟通的功能模块,实现实时消息通知。 一、WebSocket是什么? WebSocket是一...
需求背景最近客户想做一个社团购物的软件,技术选型是采用uni-app开发的微信小程序,其中有一个功能是想开发一个买家和卖家沟通的功能模块,实现实时消息通知。
一、WebSocket是什么?WebSocket是一种在单个TCP连接上进行全双工通信的协议。WebSocket通信协议于2011年被IETF定为标准RFC 6455,并由RFC7936补充规范。WebSocket API也被W3C定为标准。
WebSocket使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在WebSocket API中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。
二、开发步骤
1.区分开发场景在实际开发工作中,我们没有特别多的时间去实现一个scoket的完整功能,会利用到一些开源的插件,例如socket.io等,那么我们要如何区分在不同场景下怎么利用这些插件更好的为我我们的工作服务呢?
1、在H5页面中开发聊天功能,我们可以实际用到原生的js功能去对聊天的新增在做一些追加的操作,可以使用socket.io等类似的插件
2、在uni-app开发微信小程序时,由于我们的运行环境是小程序,我们最好是利用uni-app自带的socket的api来进行开发
3、对于做平台的小程序来讲,可以直接利用微信自带的客服功能,在微信公众平台配置自己的客服,可以说是既省时又省力
2.开发代码(1)开发过程中的思路设计:
1、在用户登陆时,根据用户的id创建一个全局的socket,与服务器建立联系
2、进入聊天页面,拉取用户联系过的人
3、点击联系卖家,即是创建买家和买家的socket联系,会先获取双方的聊天记录
4、自己发送消息和收到买家消息如何展示,在页面展示过程中,我们一般自己的聊天在右侧,收到别人的消息在左侧,可以通过判断 不同的id来展示相应的消息
5、监听对方发送的消息,自己发送的消息会push到消息数组中,然后监听对方发送过来的消息,再push到消息数组中,这样数组会及时更新消息展示
(2) 效果图片展示
(3) 代码展示,以下代码都是以vue的风格写的
message.js的代码
<template> <view class="container pad24"> <scroll-view :show-scrollbar="false" :style="{ height: scrollHeight + 'px' }" scroll-y class="list" v-if="list.length"> <view class="item acea-row" v-for="item in list" :key="item" @tap="enterChat(item)"> <image :src="item.targetUserFace|| '/static/images/avatar.png'" mode="scaleToFill" /> <text class="count" v-if="item.waitCount">{{ item.waitCount }}</text> <view class="info"> <view class="name acea-row row-between-wrapper"> <view class="name-text">{{ item.targetUserName || '商城用户' }}</view> <view class="time">{{ item.newContentTime }}</view> </view> <view class="tip">{{ item.newContent && item.newContent.indexOf('oss') > -1 ? '[图片]' : item.newContent }}</view> </view> </view> </scroll-view> <empty title="暂无消息" :height="scrollHeight * 1.3" v-else></empty> </view> </template> <script> import { getChatList } from '@/api/chat.js' import storage from "@/utils/storage"; import empty from '@/components/AmptyPage.vue'; import api from "@/config/api.js"; import websocketUtil from "@/lib/chat"; export default { data() { return { userId: '', list: [], scrollHeight: 500 } }, components: { empty }, computed: { userInfo() { return this.$store.state.userInfo; } }, onShow() { this.userId = this.userInfo.id; this.getList(); }, onLoad(options) { let self = this; wx.getSystemInfo({ success: function(res) { self.scrollHeight = res.windowHeight ; } }); // 监听对方发送的消息,对于新的消息,会更新消息通知的数量,对于已知联系人发送的消息,可以自己手动更新,防止多次调用列表接口,对于未知的用户可以调取联系人列表接口,更新联系人 uni.$on('chat', data => { data = JSON.parse(data); let ind = null; if(data && data.content) { uni.vibrate(); this.list && this.list.map((item, index) => { if(item.targetUserId === data.fromUserId) { ind = index; item.waitCount++; item.newContent = data.content; } }) } if(!ind && ind != 0) { this.getList(); } }) }, watch: { userId: { handler: function(newV, oldV) { if(newV != oldV) { // 在确认有userId的情况下,我们创建全局的socket,我存在了store里 this.websocket = null; this.websocket = new websocketUtil(`${api.socketUrl}/websocket?id=${newV}`, 5000); this.$store.commit('linksocket', this.websocket) } }, deep: true } }, methods: { getList() { getChatList({ userId: this.userId }).then(res => { if(res.data.success) { this.list = res.data.result; } }) }, enterChat(item) { // 进入聊天页面要带上自己的id和对方的id uni.navigateTo({ url: `/pages/message/chat?targetUserId=${item.targetUserId}&roomId=${item.id}` }) } } } </script> <style lang="scss" scoped> .container { background: #fff; overflow: hidden; .list { width: calc(100% + 3rpx); :-webkit-scrollbar { display: none; width: 0 !important; height: 0 !important; -webkit-appearance: none; background: transparent; } .item { padding-bottom: 29rpx; &:first-child { padding-top: 29rpx; } image { position: relative; margin-right: 20rpx; width: 81rpx; height: 81rpx; border-radius: 50%; border: 2rpx solid #EEEEEE; } .count { position: absolute; left: 59rpx; z-index: 2; display: inline-block; min-width: 30rpx; height: 30rpx; background: #FF2828; border-radius: 20rpx; color: #fff; line-height: 30rpx; font-size: 20rpx; text-align: center; } .info { flex: 1; padding-bottom: 28rpx; display: flex; flex-direction: column; justify-content: space-between; border-bottom: 2rpx solid #EEEEEE; .name { margin-bottom: 10rpx; font-size: 28rpx; font-weight: 500; color: #333333; .time { font-size: 20rpx; font-weight: 400; color: #666666; } } .tip { font-size: 24rpx; font-weight: 400; color: #666666; } } } } } </style>
chat.js的代码
class websocketUtil { constructor(url, time) { this.is_open_socket = false //避免重复连接 this.url = url //地址 this.data = null //心跳检测 this.timeout= time //多少秒执行检测 this.heartbeatInterval= null //检测服务器端是否还活着 this.reconnectTimeOut= null //重连之后多久再次重连 try { return this.connectSocketInit() } catch (e) { console.log('catch'); this.is_open_socket = false this.reconnect(); } } // 进入这个页面的时候创建websocket连接【整个页面随时使用】 connectSocketInit() { this.socketTask = uni.connectSocket({ url: this.url, success:()=>{ console.log("正准备建立websocket中..."); // 返回实例 return this.socketTask }, }); this.socketTask.onOpen((res) => { console.log("WebSocket连接正常!"); uni.$emit('socket_open', 'WebSocket连接正常'); clearTimeout(this.reconnectTimeOut) clearTimeout(this.heartbeatInterval) this.is_open_socket = true; this.start(); // 注:只有连接正常打开中 ,才能正常收到消息 this.socketTask.onMessage((res) => { uni.$emit('chat', res.data); }); }) // 监听连接失败,这里代码我注释掉的原因是因为如果服务器关闭后,和下面的onclose方法一起发起重连操作,这样会导致重复连接 uni.onSocketError((res) => { console.log('WebSocket连接打开失败,请检查!'); uni.$emit('onSocketError', res); this.is_open_socket = false; this.reconnect(); }); // 这里仅是事件监听【如果socket关闭了会执行】 this.socketTask.onClose(() => { console.log("已经被关闭了") this.is_open_socket = false; this.reconnect(); }) } sendMsg(msg) { console.log( msg, 'mssg') //向后端发送命令 msg = JSON.stringify(msg); try { //通过 WebSocket 连接发送数据 this.socketTask.send({ data: msg }); } catch (e) { if (this.is_open_socket) { return; } else { this.reconnect(url, onErrFn); } } } //重新连接 reconnect(){ //停止发送心跳 clearInterval(this.heartbeatInterval) //如果不是人为关闭的话,进行重连 if(!this.is_open_socket){ this.reconnectTimeOut = setTimeout(()=>{ this.connectSocketInit(); },3000) } } //开启心跳检测 start(){ this.heartbeatInterval = setTimeout(()=>{ this.data={value:"传输内容",method:"方法名称"} console.log(this.data) this.sendMsg(this.data); },this.timeout) } //关闭连接 stop() { this.is_open_socket = false; this.heartbeatInterval = null; } } module.exports = websocketUtil
chat.vue的代码
<template> <div class="broadcast-details" :style="'height:'+windowH+'px'"> <view class="hd-wrapper" :class="active === true ? 'on' : ''"> <scroll-view scroll-y="true" style="height: 100%; overflow: hidden;" :scroll-top="scrollTop" scroll-with-animation="true" :scroll-into-view="intoindex" lower-threshold="150" @scrolltoupper="scrolltoupper" > <div class="chat" ref="chat" id="chat" > <template v-for="item in history"> <div class="item acea-row row-top" v-if="item.fromUserId === toUid" :key="item.id"> <div class="pictrue"><img :src="item.avatar || '/static/images/avatar.png'" /></div> <div class="text"> <div class="name">{{ item.nickName || '麻雀商城用户' }}</div> <div class="acea-row"> <div class="conter acea-row row-middle" v-if="item.msn_type === 4"> <img src="/static/images/signal2.gif" class="signal" style="margin-right: 0.27rem;" />12’’ </div> <div class="conter acea-row row-middle" v-if="item.msn_type == 3"> <img :src="item.msn" /> </div> <div class="conter acea-row row-middle" v-if="item.msn_type === 2"> <i class="em" :class="item.msn"></i> </div> <div class="conter acea-row row-middle" v-if="item.msn_type === 1"> {{ item.msn }} </div> </div> </div> </div> <div class="item acea-row row-top row-right" v-else :key="item.id"> <div class="text textR"> <div class="name">{{ item.nickName || '麻雀商城用户' }}</div> <div class="acea-row "> <div class="conter acea-row row-middle" v-if="item.msn_type === 3"> <img :src="item.msn" /> </div> <div class="conter acea-row row-middle" v-if="item.msn_type === 2"> <i class="em" :class="item.msn"></i> </div> <div class="conter acea-row row-middle" v-if="item.msn_type === 1"> {{ item.msn }} </div> </div> </div> <div class="pictrue"><img :src="item.avatar || '/static/images/avatar.png'" /></div> </div> </template> </div> <div :style=" active === true ? 'height:' + footerConH + 'rem;' : 'height:' + footerH + 'rem;' "></div> </scroll-view> </view> <div class="footerCon" :class="active === true ? 'on' : ''" :style="'transform: translate3d(0,' + percent + '%,0);'" ref="footerCon"> <form> <div class="footer acea-row row-between row-bottom" ref="footer"> <img @click="uploadImg" src="/static/images/plus.png" /> <img :src=" active === true ? '/static/images/keyboard.png' : '/static/images/face.png' " @click="emoticon" /> <div class="voice acea-row row-center-wrapper" v-if="voice" @touchstart.prevent="start" @touchmove.prevent="move" @touchend.prevent="end"> {{ speak }} </div> <input type="text" placeholder="请输入内容" class="input" ref="input" v-show="!voice" @input="bindInput" @keyup="keyup" @focus="focus" cursor-spacing="20" v-model="textCon"> <div class="send" :class="sendColor === true ? 'font-color-red' : ''" @click="sendTest"> 发送 </div> </div> </form> </div> <div class="banner slider-banner"> <swiper class="swiper-wrapper" :autoplay="autoplay" :circular="circular" :interval="interval" :duration="duration" v-if="emojiGroup.length > 0"> <block v-for="(emojiList, index) in emojiGroup" :key="index"> <swiper-item> <i class="em" :class="emoji" v-for="emoji in emojiList" :key="emoji" @click="addEmoji(emoji)"></i> <img src="/static/images/del.png" class="emoji-outer" /> </swiper-item> </block> <swiper-slide class="swiper-slide acea-row" v-for="(emojiList, index) in emojiGroup" :key="index"> <i class="em" :class="emoji" v-for="emoji in emojiList" :key="emoji" @click="addEmoji(emoji)"></i> <img src="/static/images/del.png" class="emoji-outer" /> </swiper-slide> <div class="swiper-pagination" slot="pagination"></div> </swiper> </div> <div class="recording" v-if="recording"> <img src="/static/images/recording.png" /> </div> <home></home> </div> </template> <script> import emojiList from "@/utils/emoji"; import storage from "@/utils/storage.js"; import api from "@/config/api.js"; import { getContactList } from '@/api/chat.js' import { HTTP_REQUEST_URL } from '@/config/app.js'; const chunk = function(arr, num) { num = num * 1 || 1; var ret = []; arr.forEach(function(item, i) { if (i % num === 0) { ret.push([]); } ret[ret.length - 1].push(item); }); return ret; }; const NAME = "CustomerService"; export default { name: NAME, props: { couponList: { type: Array, default: () => [] } }, data: function() { return { url: `${HTTP_REQUEST_URL}/api/upload/image`, headers: { "Authori-zation": "Bearer " + this.$store.state.token }, emojiGroup: chunk(emojiList, 20), active: false, voice: false, speak: "按住 说话", recording: false, swiperOption: { pagination: { el: ".swiper-pagination", clickable: true }, speed: 1000, observer: true, observeParents: true }, percent: 0, footerConH: 0, footerH: 1.08, socket: null, toUid: '', page: 1, limit: 30, loading: false, loaded: false, history: [], sendColor: false, sendtxt: "", productId: 0, productInfo: {}, orderId: "", orderInfo: {}, cartInfo: {}, autoplay: false, circular: true, interval: 3000, duration: 500, upload_max: 2, //图片大小 //上传的图片地址 uploadImages: [], //展示的图片地址 uploads: [], // 超出限制数组 exceeded_list: [], windowH: 0, isBQ: false, scrollTop:0 ,//滚动距离 textCon:'', //文字 intoindex: 'chat' }; }, computed: { websocket() { return this.$store.state.websocket }, userInfo() { return this.$store.state.userInfo } }, onLoad(option) { let self = this; this.userId = this.userInfo.id; this.toUid = option.targetUserId; this.targetUserId = option.targetUserId; this.roomId = option.roomId; console.log(this.userId, 'userid', this.websocket) this.productId = parseInt(option.productId) || 0; this.orderId = option.orderId || ""; this.getContactList(); uni.getSystemInfo({ success: function(res) { self.windowH = res.windowHeight } }) uni.$on('onSocketError', (data) => { console.log(data, 'onSocketError') }) uni.$on('chat', data => { // 监听对方消息 data = JSON.parse(data); if(data && data.content && data.fromUserId === this.targetUserId) { uni.vibrate(); this.history.push({ nickName: '测试', targetUserId: data.fromUserId, fromUserId: this.targetUserId, avatar: this.contactInfo.targetUserFace, msn: data.content, msn_type: 1, nickName: this.contactInfo.targetUserName }); self.getTop(); } }) }, methods: { scrolltoupper() { }, getTop() { // 滚动页面到当前视图 const self = this; const query = uni.createSelectorQuery().in(this); query.select('#chat').boundingClientRect() .exec((data)=>{ data && data.map(item => { self.scrollTop = item.height; }) }) }, getContactList() { getContactList({ roomId: this.roomId, fromUserId: this.userId, targetUserId: this.targetUserId }).then(res => { if(res.data.success) { this.contactInfo = res.data.result; this.history = res.data.result.detailDTOList.map(item => { return { msn: item.content, msn_type: item.type, ...item, nickName: item.targetUserId === this.toUid ? this.contactInfo.fromUserName : this.contactInfo.targetUserName, avatar: item.targetUserId === this.toUid ? this.contactInfo.fromUserFace : this.contactInfo.targetUserFace } }); this.scrollTop = this.history.length * 73; } }) }, uploadImg() { let self = this uni.chooseImage({ count: 1, //默认1 sizeType: ['original', 'compressed'], //可以指定是原图还是压缩图,默认二者都有 sourceType: ['album', 'camera'], //从相册选择 success: (res) => { for (let i = 0; i < res.tempFiles.length; i++) { if (Math.ceil(res.tempFiles[i].size / 1024) < this.upload_max * 1024) { this.uploads.push(res.tempFiles[i].path) this.uploadImages.push(res.tempFiles[i].path) } else { this.exceeded_list.push(i === 0 ? 1 : i + 1); uni.showModal({ title: '提示', content: `第${[...new Set(this.exceeded_list)].join(',')}张图片超出限制${this.upload_max}MB,已过滤` }); } } uni.uploadFile({ url: api.common + '/common/upload/file', filePath: self.uploadImages[0], name: 'file', header: { "Authori-zation": "Bearer " + Boolean(storage.getAccessToken()), accessToken: storage.getAccessToken() }, //请求参数 success: (uploadFileRes) => { let data = JSON.parse(uploadFileRes.data) console.log(data, 'ddd') self.sendMsg(data.result, 3) } }); }, fail: (err) => { uni.showModal({ content: JSON.stringify(err) }); } }); }, focus: function() { this.active = false; }, keyup: function() { if (this.$refs.input.innerHTML.length > 0) { this.sendColor = true; } else { this.sendColor = false; } }, emoticon: function() { this.voice = false; if (this.active === true) { this.active = false; } else { this.active = true; } this.getTop(); }, addEmoji(name) { this.sendMsg(name, 2); }, sendMsg(msn, type) { if(msn) { let message = { fromUserId: this.userId, targetUserId: this.toUid, content: msn, msn, type, msn_type: type, nickName: this.userInfo.nickName } this.websocket.sendMsg(message) this.history.push({ ...message, avatar: this.userInfo.face, }); this.getTop(); } }, sendTest() { this.sendMsg(this.textCon, 1); this.textCon = '' this.getTop(); }, bindInput: function(e) { if(e.detail.value){ this.sendColor = true }else{ this.sendColor = false } }, move() { clearTimeout(this.timeOutEvent); this.timeOutEvent = 0; } } }; </script> <style lang="scss" scoped> @import url('@/static/emoji-awesome/css/google.min.css'); page { width: 100%; height: 100%; } .broadcast_num { padding: 0 10rpx !important; } .em { display: inline-block; width: 50rpx; height: 50rpx; } .swiper-wrapper { .em { margin: 40rpx 0 0 50rpx; } } .emoji-outer { position: absolute; right: 50rpx; bottom: 30rpx; width: 50rpx; height: 50rpx; } .broadcast-details { display: flex; flex-direction: column; width: 100%; overflow: hidden; .hd-wrapper { flex: 1; display: flex; flex-direction: column; overflow: hidden; &.on{ padding-bottom: 300rpx; } } } </style>
(4) 遇到的问题及解决办法
Q: 怎么避免你和某个联系 人的聊天会同步给其他联系人?
A:可以在监听对方发送的消息的时候,前端判断是否是和你正在聊天的用户,如果不是消息则不会被放到消息队列中,后端也要做一下校验,这样可以防止消息会发给其他人
Q: 在联系人页面,怎么显示消息通知有多少条?有性能比较好的方法吗?
A:可以联系人页面监听发送的消息,并且在当前列表中找到对应的数据,并更新消息数量,如果在消息列表中找不到对应的联系人,则是新联系人可以重新获取联系人列表,这样可以避免多次调取联系人来获取消息,并且能及时的显示消息的通知
总结
以上就是今天要讲的内容,本文仅仅简单介绍了uni-app中开发消息模块的思路及方法,如果你有更好的想法可以和我分享 -
关于2018江苏书法水平等级证书考试报名工作的通知下发学校2018年31425.doc
2021-11-28 10:49:21关于2018江苏书法水平等级证书考试报名工作的通知下发学校2018年31425.doc -
关于下发新聘人员任职条件(修订)的通知类文件(模板).docx
2021-12-17 11:21:53关于下发新聘人员任职条件(修订)的通知类文件(模板).docx -
客服服务《关于下发“我的e家”套餐调优方案的通知》.rar
2021-08-16 19:18:58客服服务《关于下发“我的e家”套餐调优方案的通知》.rar -
传媒行业周报:国家新闻出版署下发未成年防沉迷通知,各家游戏公司积极回应.pdf
2021-09-12 22:14:58传媒行业周报:国家新闻出版署下发未成年防沉迷通知,各家游戏公司积极回应.pdf -
关于下发《福清市学校构筑消防安全“防火墙”工程实施方案》的通知.docx
2021-10-13 02:35:33关于下发《福清市学校构筑消防安全“防火墙”工程实施方案》的通知.docx -
某某银行关于网络平台涉政负面信息自查报告.docx
2021-11-21 23:26:32某某银行关于网络平台涉政负面信息自查报告.docx -
教育部办公厅下发关于开展全国学校国防教育典型案例推荐遴选工作的通知.pdf
2021-08-21 22:45:56教育部办公厅下发关于开展全国学校国防教育典型案例推荐遴选工作的通知.pdf -
腾讯云短信下发状态通知
2019-06-05 16:20:32腾讯云短信下发状态通知 接口描述 功能描述 短信下发给用户后,腾讯云短信服务可以通过回调业务 URL 的方式,通知业务方短信下发的状态。 URL 示例 POST http://example.com/sms/callback 请求参数 [ { “user_... -
微信小程序-服务通知的订阅与下发(基于云调用)
2021-10-11 23:41:40这篇博客的内容比较基础,主要是实现小程序服务通知的订阅以及下发(云调用)。虽然简单但是应用场景却非常、非常、非常的普遍。例如:下单通知、小程序取餐通知等等,其实就是通知用户一些事件的进度、结果等。如下...