实践案例 | 如何通过微信公众号模板消息定向推送供应商招投标信息
小编推荐
供应链公司在招投标时,常面临招投标信息人工传递低效,且容易出错的痛点。
本文将展示如何通过微信公众号模板消息,实现对供应商精准、即时的消息推送,让招投标流程变得高效且无误。跟随小编,一探自动化信息传递的奥秘,开启供应链协同的新篇章~
业务背景
GY供应链公司是某省资源整合公司,合作的供应商达300家,在行业中属于龙头,能拿到许多标的,然后分包给供应商进行合作,是一家比较典型的供应链公司。
GY公司原先在招投标时,需要先整理意向供应商的资料,然后打电话通知意向客户将要开标的情况;当客户中标时,需要整理中标客户的情况,通过电话和邮件形式通知中标客户。这两个过程都需要人工通知、手动编辑,非常耗时耗力,而且容易出错。
因此,GY公司希望在使用供应商协同招投标时,将对应标的的主要信息通过微信公众号定向推送到意向供应商(需要供应商关注GY供应链公司公众号且进行了注册登录),保证意向客户能及时看到开标和中标的消息,以便及时进行报价,及中标后,进行后续的项目运作。
解决方案
方案整体思路
首先,供应商通过在微信公众号→自定义菜单中的移动供应商门户进行注册。注册之后,需要管理员在星瀚系统中进行审批,审批完成后,供应商进入另一个授权登录菜单进行账号激活及登录,登录之后即可正常接收星瀚系统消息。
GY公司在项目启动、定标单、议价单中发布公告时,自动从项目启动、定标单、议价单中抓取开标及定标的信息,然后将整理后的数据通过消息平台中新建的微信公众号渠道发送。过程对于客户是无感的,但是供应商能及时获取微信公众号消息,即使发送时发生了接口网络中断的情况,消息平台也会重试五次保证消息能成功发送。
项目启动、定标单使用标准的发布公告,议价单二开按钮绑定发布公告,其他功能如项目启动。
具体逻辑图如下:
关键功能点及效果展示
1、 微信公众号和星瀚消息平台集成
微信公众号申请类型为“服务号”,需要使用到“自定义菜单>模板消息”,模板消息需要确定服务类目,然后从所需要的服务类目中选择适合企业的模板。微信有严格的安全设置,需要提前设置好IP白名单、网页授权域名、JS接口安全域名等,具体设置方法如下:
IP白名单设置路径:【设置与开发】→【安全中心】→【IP白名单】,只有在IP白名单内才能获取access_token。
网页授权域名设置路径:【设置与开发】→【公众号设置】→【功能设置】→【网页授权域名】,将供应商门户相关网页域名在此配置。
JS接口安全域名设置路径:【设置与开发】→【公众号设置】→【功能设置】→【JS接口安全域名】,将供应商门户相关网页域名在此配置。
自定义菜单路径:【内容与互动】→【自定义菜单】,【消息类型】选择“跳转网页”,将移动端供应商门户地址配置在“网页链接”中。
模板消息配置路径:【新的功能】→【广告与服务】→【模板消息】 。
注意:模板消息需要先设置服务类目才可以使用,服务类目设置路径:【设置与开发】→【公众号设置】→【账号详情】→【服务类目】,如下图所示:
注:建议提前熟悉微信公众号开发官方文档:
https://developers.weixin.qq.com/doc/offiaccount/Getting_Started/Overview.html
b、星瀚端
新建消息渠道wechatOA,实现类继承AbstractServiceHandler,重写 sendMessage方法即可(具体可参考代码见附录)。
2、 微信模板消息参数抽取维护
为了后续客户可以直接通过修改基础资料,更改消息模板,将微信消息模板抽取为基础资料进行配置,二开表单如下图:
3、 二开项目启动等表单实现消息推送
项目启动、议价、定标等单据通过从【选择供应商】中抽取供应商,从【通用报价单】和【基本信息】中抽取投标和中标信息等数据,在发布公告时通过微信消息渠道直接定向发给公司选中的供应商,以便供应商能够即时获取企业动态(标准产品发布公告会弹出公告编辑页面,此处在弹出页面之前就可将信息发送给供应商对应的微信公众号中,对于客户是无感的)。
4、 通过微信公众号注册激活
注册审批通过的供应商首次登录时需要修改密码激活,激活之后可以正常使用移动供应商门户且免登。
5、 注册通过的供应商接收投标、中标消息
供应商正常接收星瀚投标中标消息,且能通过授权登录或者接收到消息的查看详情直接免登到星瀚移动供应商门户。
方案的可推广价值
1. 行业的普适程度
可适用范围广泛,应用到微信公众号消息通知或者其他第三方渠道需要进行个性化消息通知的项目都可应用,使用标准的消息渠道进行消息通知可以实现自动重试和标准日志打印,结合用户的个性化需求可以实现快速开发,完成上线验收。
1. 客户价值
1)减少操作风险:减少了由于人工统计、人工通知的遗漏,依赖系统能够保证数据准确、及时地传达给供应商。
2)提升工作效率:减少了公司业务的工作量,提升了公司通知效率,目前开标、定标可以实现自动化通知供应商,不需要人为通知。
附录
SendMessage方法代码示例:
public class SendMsgToWeChatOA extends AbstractServiceHandler{ private final Log logger = LogFactory.getLog(SendMsgToWeChatOA.class); @Override public void createToDo(MessageContext messageContext, ToDoInfo toDoInfo) { } @Override public void dealToDo(MessageContext messageContext, ToDoInfo toDoInfo) { } @Override public void deleteToDo(MessageContext messageContext, ToDoInfo toDoInfo) { } @Override public void sendMessage(MessageContext ctx, MessageInfo message) { super.sendMessage(ctx, message); try { String tag = message.getTag(); String content = message.getContent(); List<Long> userIds = message.getUserIds(); QFilter filter = new QFilter("name", QCP.equals, tag); DynamicObject WxgzhObj = BusinessDataServiceHelper.loadSingle("gy_wxgzh", "id,name,appid,secret,gy_templateid,gy_url,gy_tokenurl,gy_ssourl,gy_accesstoken,gy_expiresin,gy_key1,gy_key2,gy_key3,gy_key4,gy_moburl",filter.toArray()); //2024/4/9 QFilter qFilter1 = new QFilter("number", QCP.equals, "001"); //微信公众号系统的token会覆盖, 将其存入固定的一张表中 DynamicObject WxgzhObjtoken = BusinessDataServiceHelper.loadSingle("gy_wxgzhtoken", "id,name,gy_accesstoken,gy_expiresin", qFilter1.toArray()); String url = WxgzhObj.get("gy_tokenurl").toString(); logger.info("--url--"+url); //公众号appid String gzhappid = WxgzhObj.get("appid").toString(); logger.info("--gzhappid--"+gzhappid); //公众号密码 String gzhappsecret = WxgzhObj.get("secret").toString(); logger.info("--gzhappsecret--"+gzhappsecret); //公众号模板消息 模板id String templateid = WxgzhObj.get("gy_templateid").toString(); logger.info("--templateid--"+templateid); url = url+"&appid="+gzhappid+"&secret="+gzhappsecret; logger.info("--url--"+url); String ssourl = WxgzhObj.get("gy_ssourl").toString(); logger.info("--ssourl--"+ssourl); String accessToken = ""; String gyaccesstoken = WxgzhObjtoken.get("gy_accesstoken").toString(); logger.info("--gy_accesstoken--"+gyaccesstoken); String gyexpiresin = WxgzhObjtoken.get("gy_expiresin").toString(); logger.info("--gy_expiresin--"+gyexpiresin); Long gyid = WxgzhObjtoken.getLong("id"); logger.info("--gyid--"+gyid); String id = WxgzhObj.getString("id"); String gy_key1 = WxgzhObj.getString("gy_key1"); String gy_key2 = WxgzhObj.getString("gy_key2"); String gy_key3 = WxgzhObj.getString("gy_key3"); String gy_key4 = WxgzhObj.getString("gy_key4"); //新增 String gy_moburl = WxgzhObj.getString("gy_moburl"); if(!Strings.isNullOrEmpty(gyaccesstoken) && !Strings.isNullOrEmpty(gyexpiresin)){ Date expiresin = this.stringToDate(gyexpiresin,"yyyy-MM-dd HHss"); //过期时间大于当前时间超过10分钟 if(this.getDayDiff(new Date(),expiresin)>10){ accessToken = gyaccesstoken; } } accessToken = getAccessToken(accessToken, url, gyid); if(!StringUtils.isEmpty(accessToken)){ String sendurl = WxgzhObj.get("gy_url").toString(); logger.info("--sendurl--"+sendurl); sendurl = sendurl + accessToken; JSONObject requestJson = new JSONObject(); requestJson.put("template_id",templateid); List<Long> errUserIds = new ArrayList<Long>(); switch (tag){ case "项目启动": JSONObject dataJson = JSONObject.parseObject(content); String key1 = dataJson.getString("key1"); String key2 = dataJson.getString("key2"); String key3 = dataJson.getString("key3"); JSONObject data = new JSONObject(); JSONObject jsonObject1 = new JSONObject(); JSONObject jsonObject2 = new JSONObject(); JSONObject jsonObject3 = new JSONObject(); JSONObject jsonObject11 = new JSONObject(); jsonObject1.put("value",key1); jsonObject2.put("value",key2); jsonObject3.put("value",key3); jsonObject11.put("value","首轮报价"); //固定枚举值为1 首次 //模版中的参数 data.put(gy_key1,jsonObject1); data.put(gy_key2,jsonObject2); data.put(gy_key3,jsonObject3); data.put(gy_key4,jsonObject11); StringBuilder permobUrl = new StringBuilder(); // String mobUrl = "https://baixiangfood.test.kdcloud.com/mobile.html?form=" + "mobsp_apphomepage_new"; String mobUrl = gy_moburl; if(!Strings.isNullOrEmpty(ssourl)){ permobUrl.append(ssourl); permobUrl.append("appid="); permobUrl.append(gzhappid); permobUrl.append("&redirect_uri="); permobUrl.append(YunzhijiaCommonUtil.encode(mobUrl)); permobUrl.append("&response_type=code&scope=snsapi_base&state=wxgzh_split_"); permobUrl.append(gzhappid); permobUrl.append("_split_"); permobUrl.append(gzhappsecret); permobUrl.append("#wechat_redirect"); } requestJson.put("url",permobUrl); requestJson.put("data",data); Map<Long, String> userListOpenid = getUserListOpenid(userIds); for (long userId:userIds ) { String openid = userListOpenid.get(userId); if(!StringUtils.isEmpty(openid)){ // requestJson.put("client_msg_id",openid+data); 防重,十分钟有效, 可不填 requestJson.put("touser",openid); logger.info("公众号:"+requestJson.toJSONString()); String response = HttpUtils.post(sendurl, null, requestJson.toJSONString()); logger.info("公众号:"+response); if(StringUtils.isEmpty(response)){ errUserIds.add(userId); }else{ JSONObject json = JSONObject.parseObject(response); Integer errcode = json.getInteger("errcode"); if(errcode!=0){ errUserIds.add(userId); } } }else{ errUserIds.add(userId); } } if(errUserIds.size()>0){ message.setUserIds(errUserIds); throw new KDBizException("推送公众号消息失败,请联系管理员!"); } break; case "定标单": JSONObject dataJson1 = JSONObject.parseObject(content); String key21 = dataJson1.getString("key1"); JSONArray key22 = (JSONArray) dataJson1.get("key2"); StringBuilder permobUrl2 = new StringBuilder(); // String mobUrl2 = "https://baixiangfood.test.kdcloud.com/mobile.html?form=" + "mobsp_apphomepage_new"; String mobUrl2 = gy_moburl; if(!Strings.isNullOrEmpty(ssourl)){ permobUrl2.append(ssourl); permobUrl2.append("appid="); permobUrl2.append(gzhappid); permobUrl2.append("&redirect_uri="); permobUrl2.append(YunzhijiaCommonUtil.encode(mobUrl2)); permobUrl2.append("&response_type=code&scope=snsapi_base&state=wxgzh_split_"); permobUrl2.append(gzhappid); permobUrl2.append("_split_"); permobUrl2.append(gzhappsecret); permobUrl2.append("#wechat_redirect"); } requestJson.put("url",permobUrl2); // requestJson.put("client_msg_id",id); 防重, 可不填 for (int i = 0; i < key22.size(); i++) { JSONObject data2 = new JSONObject(); //模版中的参数 JSONObject jsonObject = key22.getJSONObject(i); List<Long> supplierlist = (List<Long>) jsonObject.get("supplier"); Map<Long, String> userListOpenid1 = getUserListOpenid(supplierlist); for (Long supplier: supplierlist ) { String openid = userListOpenid1.get(supplier); if(!StringUtils.isEmpty(openid)){ String result = jsonObject.getString("result"); JSONObject jsonObject4 = new JSONObject(); JSONObject jsonObject5 = new JSONObject(); jsonObject4.put("value",key21); jsonObject5.put("value",result); //模版参数 data2.put(gy_key1,jsonObject4); data2.put(gy_key2,jsonObject5); requestJson.put("touser",openid); requestJson.put("data",data2); logger.info("公众号:"+requestJson.toJSONString()); String response = HttpUtils.post(sendurl, null, requestJson.toJSONString()); logger.info("公众号:"+response); if(StringUtils.isEmpty(response)){ errUserIds.add(supplier); }else{ JSONObject json = JSONObject.parseObject(response); Integer errcode = json.getInteger("errcode"); if(errcode!=0){ errUserIds.add(supplier); } } } else{ errUserIds.add(supplier); } } } if(errUserIds.size()>0){ message.setUserIds(errUserIds); throw new KDBizException("推送公众号消息失败,请联系管理员!"); } break; case "议价单": JSONObject dataJson3 = JSONObject.parseObject(content); String key31 = dataJson3.getString("key1"); String key32 = dataJson3.getString("key2"); JSONArray key33 = (JSONArray) dataJson3.get("key3"); StringBuilder permobUrl3 = new StringBuilder(); // String mobUrl3 = "https://baixiangfood.test.kdcloud.com/mobile.html?form=" + "mobsp_apphomepage_new"; String mobUrl3 = gy_moburl; if(!Strings.isNullOrEmpty(ssourl)){ permobUrl3.append(ssourl); permobUrl3.append("appid="); permobUrl3.append(gzhappid); permobUrl3.append("&redirect_uri="); permobUrl3.append(YunzhijiaCommonUtil.encode(mobUrl3)); permobUrl3.append("&response_type=code&scope=snsapi_base&state=wxgzh_split_"); permobUrl3.append(gzhappid); permobUrl3.append("_split_"); permobUrl3.append(gzhappsecret); permobUrl3.append("#wechat_redirect"); } requestJson.put("url",permobUrl3); for (int i = 0; i < key33.size(); i++) { JSONObject jsonObject = key33.getJSONObject(i); List<Long> supplierlist = (List<Long>) jsonObject.get("supplier"); String material = jsonObject.getString("material"); String turns = jsonObject.getString("turns"); String turnss = ""; JSONObject data3 = new JSONObject(); JSONObject jsonObject6 = new JSONObject(); JSONObject jsonObject7 = new JSONObject(); JSONObject jsonObject8 = new JSONObject(); JSONObject jsonObject9 = new JSONObject(); jsonObject6.put("value",key31); jsonObject7.put("value",key32); jsonObject8.put("value",material); switch (turns){ case "1": turnss = "首轮报价"; break; case "2": turnss = "第2轮报价"; break; case "3": turnss = "第3轮报价"; break; case "4": turnss = "第4轮报价"; break; case "5": turnss = "第5轮报价"; break; case "6": turnss = "第6轮报价"; break; case "7": turnss = "第7轮报价"; break; case "8": turnss = "第8轮报价"; break; case "9": turnss = "第9轮报价"; break; case "10": turnss = "第10轮报价"; break; case "11": turnss = "第11轮报价"; break; case "30": turnss = "补价(1)"; break; case "31": turnss = "补价(2)";
实践案例 | 如何通过微信公众号模板消息定向推送供应商招投标信息
本文2024-09-23 00:58:30发表“云苍穹知识”栏目。
本文链接:https://wenku.my7c.com/article/kingdee-cangqiong-142952.html