微信生成帶引數的二維碼,合成海報,掃碼後推送小程式
背景:公司開發的小程式要實現將產品免費給使用者試用的功能,使用者登入小程式後在產品頁可以將產品以二維碼海報的方式分享給微信好友,好友掃碼後跳轉公眾號,關注後公眾號推送小程式,點選小程式後跳轉到小程式中的相應產品頁面。
如下圖:
這裡涉及到兩個重要的環節:
1.生成帶引數(產品id,產品縮圖、分享人openid,小程式跳轉路徑等)的二維碼;
2.掃碼後的事件推送,將小程式推送給好友。
一、微信公眾平臺申請服務號並通過認證
二、驗證請求來自微信
登入公眾號在 開發-基本配置中配置開發人員伺服器資訊:
該該伺服器url作為開發者驗證介面呼叫者來自微信伺服器,也作為掃碼關注微信公眾號後的事件推送介面url。
/** * 驗證微信伺服器 * @param signature * @param timestamp * @param nonce * @param echostr * @return */ @RequestMapping(value = "/checkSignature", method = {RequestMethod.GET}) public Object validate(String signature, String timestamp, String nonce, String echostr) { LOG.info("signature:" + signature + " timestamp:" + timestamp + " nonce:" + nonce + " echostr:" + echostr); if (StringUtils.isNotBlank(echostr)) { LOG.info("********************************"); String signatureRet = SignUtil.getSignature(timestamp, nonce, "jpkj"); LOG.info("signatureRet:" + signatureRet); if (StringUtils.isNotBlank(signatureRet) && signatureRet.equals(signature)) { return echostr; } } return ""; }
三、事件推送介面
用於接收掃碼後關注公眾號,微信伺服器將xml資料推送給此介面,接收xml資料後推送小程式。
注意:介面要與上面的伺服器url路徑一致,請求的路徑是一樣的,但是提交的資料方式不同,驗證的http是GET提交,推送則是POST方式。
/** * 微信掃碼後事件推送方法 * * @param msg 事件輸入xml資料封裝 * @return * @throws Exception */ @RequestMapping(value = "/checkSignature", method = {RequestMethod.POST}, produces = {MediaType.TEXT_XML_VALUE}) public Object pushEvent(@RequestBody InMsgEntity msg) throws Exception { LOG.info("進入方法***************************"); LOG.info("msg:" + msg.toString()); String accessToken = redisService.getValue("access_token"); if (StringUtils.isEmpty(accessToken)) { return ServiceResultHelper.genResultWithFaild(Constant.ErrorCode.INVALID_PARAM_MSG, Constant.ErrorCode.INVALID_PARAM_CODE); } String PUSH_APPLET_URL = "https://api.weixin.qq.com/cgi-bin/message/custom/send?access_token=" + accessToken; if (null != msg) { LOG.info("*************推送*************"); if (msg.getMsgType().equals("event")) { LOG.info("*************推送小程式開始*************"); Map<String, Object> params = new HashMap<>(); params.put("touser", msg.getFromUserName()); params.put("msgtype", "miniprogrampage"); Map<String, Object> miniprogrampageMap = new HashMap<>(); miniprogrampageMap.put("title", "小程式"); miniprogrampageMap.put("appid", "wx5c5e9ffc305b66d2111"); miniprogrampageMap.put("thumb_media_id", "tByjOrKtvK71V0XZUJ9RMCPyYSbkp2A8CyZCo6W6bK8s"); params.put("miniprogrampage", miniprogrampageMap); String result = HttpClientUtil.postJson(PUSH_APPLET_URL, JSON.toJSONString(params), "UTF-8"); LOG.info("*************推送小程式結束result:" + result); } } return null; }
注:appid為要推送的小程式的appid,thumb_media_id為推送時小程式附帶的縮圖,可呼叫微信提供的上傳素材介面獲取id。
InMsgEntity類: 對微信推送的xml格式的資料進行封裝
package com.jp.tech.applet.ms.scancode.domain;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;
/**
* 微信推送XML資料包實體
*/
@XmlRootElement(name="xml")
@XmlAccessorType(XmlAccessType.FIELD)
public class InMsgEntity {
// 開發者微訊號
@XmlElement(name="FromUserName")
protected String FromUserName;
// 傳送方帳號(一個OpenID)
@XmlElement(name="ToUserName")
protected String ToUserName;
// 訊息建立時間
@XmlElement(name="CreateTime")
protected Long CreateTime;
/**
* 訊息型別
* text 文字訊息 * image 圖片訊息 * voice 語音訊息 * video 視訊訊息 * music 音樂訊息
*/
@XmlElement(name="MsgType")
protected String MsgType;
//事件型別,subscribe
@XmlElement(name="Event")
protected Long Event;
// 事件KEY值,qrscene_為字首,後面為二維碼的引數值
@XmlElement(name="EventKey")
private String EventKey;
// 二維碼的ticket,可用來換取二維碼圖片
@XmlElement(name="Ticket")
private String Ticket;
//訊息id
@XmlElement(name="MsgId ")
private String MsgId ;
//文字內容
@XmlElement(name="Content ")
private String Content ;
public String getFromUserName() {
return FromUserName;
}
public void setFromUserName(String fromUserName) {
FromUserName = fromUserName;
}
public String getToUserName() {
return ToUserName;
}
public void setToUserName(String toUserName) {
ToUserName = toUserName;
}
public Long getCreateTime() {
return CreateTime;
}
public void setCreateTime(Long createTime) {
CreateTime = createTime;
}
public String getMsgType() {
return MsgType;
}
public void setMsgType(String msgType) {
MsgType = msgType;
}
public Long getEvent() {
return Event;
}
public void setEvent(Long event) {
Event = event;
}
public String getEventKey() {
return EventKey;
}
public void setEventKey(String eventKey) {
EventKey = eventKey;
}
public String getTicket() {
return Ticket;
}
public void setTicket(String ticket) {
Ticket = ticket;
}
public String getMsgId() {
return MsgId;
}
public void setMsgId(String msgId) {
MsgId = msgId;
}
public String getContent() {
return Content;
}
public void setContent(String content) {
Content = content;
}
@Override
public String toString() {
return "InMsgEntity{" +
"FromUserName='" + FromUserName + '\'' +
", ToUserName='" + ToUserName + '\'' +
", CreateTime=" + CreateTime +
", MsgType='" + MsgType + '\'' +
", Event=" + Event +
", EventKey='" + EventKey + '\'' +
", Ticket='" + Ticket + '\'' +
", MsgId='" + MsgId + '\'' +
", Content='" + Content + '\'' +
'}';
}
}
獲取事件推送的xml資料後,提取所需資料,呼叫客服服務介面推送小程式
傳送小程式卡片(要求小程式與公眾號已關聯)
介面呼叫示例:
{
"touser":"OPENID",
"msgtype":"miniprogrampage",
"miniprogrampage":
{
"title":"title",
"appid":"appid",
"pagepath":"pagepath",
"thumb_media_id":"thumb_media_id"
}
}
通過下面的命令將阿里雲上的圖片上傳至微信伺服器作為永久素材。詳見:新增其他型別永久素
材
curl -F [email protected]/data/applet/Artboard.png "https://api.weixin.qq.com/cgi-bin/material/add_material?access_token=13_4cy2_RiVPjHELyZdITK8_2hpb8_0Xjr8gxZI7I7EUf7lGsuRgtPmbvQ6af-QM6XbTsVYXA8wIHi9ON8ouzDqMNJclHEM9YuDxSEjDlrdCLrnf0t1VviHrhFeK__xOM9ZFQhOH0tRJwtU0rGnBDDfAGAKMM&type=thumb"
返回說明:
{
"media_id":MEDIA_ID,
"url":URL
}
返回引數說明
引數 | 描述 |
---|---|
media_id | 新增的永久素材的media_id |
url | 新增的圖片素材的圖片URL(僅新增圖片素材時會返回該欄位) |
media_id可以作為推送小程式時的縮圖使用。程式碼中的thumb_media_id使用此命令上傳後返回的media_id即可。
四、獲取微信公眾號access_token,存入redis
獲取帶引數的二維碼首先要獲取access_token,放到redis中快取。由於access_token的有效期是兩個小時,所以這裡我用的定時任務是quartz,與springboot整合(不知道怎麼整合的請參見springboot整合Quartz實現定時任務、springboot整合redis實現傳送簡訊驗證碼)後獲取access_token。
程式碼如下:
application.properties中加入微信公眾號的appid和secret:
#微信公眾號
wx.gzh.appid=wx6e3199c6254e43b3huh1
wx.gzh.appsecret=713966cacd2a5da74fa8024a4958bac0dsdq
定時任務獲取access_token:
package com.jp.tech.applet.web.schedule;
import com.jp.tech.applet.ms.scancode.util.AccessTokenUtil;
import com.jp.zpzc.service.IRedisService;
import org.apache.commons.lang3.StringUtils;
import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import javax.annotation.Resource;
/**
* @author yangfeng
* @desciption 獲取微信公眾號access_token任務
* @date 2018/9/10
*/
public class GetAccessTokenTask implements Job {
private static Logger LOG = LoggerFactory.getLogger(GetAccessTokenTask.class);
@Resource
private IRedisService redisService;
@Value("${wx.gzh.appid}")
String appid;
@Value("${wx.gzh.appsecret}")
String secret;
@Override
public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
try {
String accessToken = AccessTokenUtil.getAccessToken(appid, secret);
LOG.info("****access_token:******"+accessToken);
if (StringUtils.isNotBlank(accessToken)) {
redisService.setKeyNoExpire("access_token", accessToken);
}
} catch (Exception e) {
LOG.error(e.getMessage());
}
}
}
獲取token工具類:
package com.jp.tech.applet.ms.scancode.util;
import com.alibaba.fastjson.JSONObject;
import com.jp.tech.applet.common.http.HttpClientUtil;
import org.apache.commons.lang3.StringUtils;
/**
* 獲取公眾號access_token
*/
public class AccessTokenUtil {
private static String ACCESSTOKENURL = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid={appId}&secret={appSecret}";
/**
* 獲取access_Token
*
* @return
*/
public static String getAccessToken(String appId, String appSecret) throws Exception {
ACCESSTOKENURL = ACCESSTOKENURL.replace("{appId}", appId).replace("{appSecret}", appSecret);
String result = HttpClientUtil.post(ACCESSTOKENURL, null, "UTF-8");
if (StringUtils.isNotBlank(result)) {
return JSONObject.parseObject(result).getString("access_token");
}
return null;
}
}
redis介面:
public interface IRedisService {
void setKeyNoExpire(String var1, String var2);
}
redis實現類:
@Service
public class RedisService implements IRedisService {
@Resource
private RedisTemplate redisTemplate;
public RedisService() {
}
public void setKeyNoExpire(String key, String value) {
ValueOperations<String, String> ops = this.redisTemplate.opsForValue();
ops.set(key, value);
}
}
五、根據access_token生成ticket
/**
* 建立二維碼ticket
*
* @param accessToken 微信access_token
* @param objectId 專案id
* @param openId 微信使用者openid
* @param path 掃碼後小程式跳轉的頁面路徑
* @param thumbMediaId 小程式展示的縮圖id
* @param picId 海報圖片id
* @return
* @throws Exception
*/
public String generateTicket(String accessToken, String objectId, String openId, String path, String thumbMediaId, String picId) throws Exception {
LOG.info("************* 建立二維碼ticket*************");
String TICKET_URL = "https://api.weixin.qq.com/cgi-bin/qrcode/create?access_token=" + accessToken;
Map<String, Object> params = new HashMap<>();
params.put("expire_seconds", 604800);
params.put("action_name", "QR_STR_SCENE");
Map<String, Object> actionInfoMap = new HashMap<>();
Map<String, Object> sceneMap = new HashMap<>();
Map<String, Object> sceneValMap = new HashMap<>();
sceneValMap.put("objectId", objectId);
sceneValMap.put("openId", openId);
sceneValMap.put("path", path);
sceneValMap.put("thumbMediaId", thumbMediaId);
sceneValMap.put("picId", picId);
sceneMap.put("scene_str", JSON.toJSONString(sceneValMap));
LOG.info("*********scene_str:*************" + JSON.toJSONString(sceneValMap));
actionInfoMap.put("scene", sceneMap);
params.put("action_info", actionInfoMap);
LOG.info("*********json引數:*************" + JSON.toJSONString(params));
return HttpClientUtil.postJson(TICKET_URL, JSON.toJSONString(params), "UTF-8");
}
* @param objectId 專案id
* @param openId 微信使用者openid
* @param path 掃碼後小程式跳轉的頁面路徑
* @param thumbMediaId 小程式展示的縮圖id
* @param picId 海報圖片id
這些引數在生成ticket的時候放入scene_str中,根據ticket來生成二維碼,掃碼的時候可以從推送事件中獲取這些引數。
六、根據ticket獲取二維碼
/**
* 根據ticket生成二維碼
*
* @param ticketResult 票據生成結果
* @param picId
* @param response
*/
public static Object generateQRcodeByTicket(String ticketResult, String picId, HttpServletResponse response) {
String ticket = JSONObject.parseObject(ticketResult).getString("ticket");
LOG.info("*********ticket:********" + ticket);
//生成帶引數二維碼
if (StringUtils.isNotBlank(ticket)) {
String GET_QRCORE_URL = "https://mp.weixin.qq.com/cgi-bin/showqrcode?ticket=" + ticket;
//生成二維碼,和海報背景合成新的圖片
createPicture(picId, GET_QRCORE_URL, response);
} else {
return ServiceResultHelper.genResultWithFaild("生成二維碼失敗", -1);
}
return null;
}
七、二維碼和海報背景合成新的圖片
/**
* 二維碼和海報背景合成新的圖片
*
* @param picId 海報背景圖在mongodb上的id
* @param url 生成帶引數二維碼介面路徑
* @param response
* @return
*/
public static void createPicture(String picId, String url, HttpServletResponse response) {
BufferedImage img = new BufferedImage(550, 978, BufferedImage.TYPE_INT_RGB);//建立圖片
BufferedImage bg;//讀取海報圖
try {
bg = ImageIO.read(new URL("http://112.74.186.131:8080/zpzc_ms/file/downloadFile?file_id=" + picId));
//讀取微信生成的帶引數二維碼
BufferedImage qRCodeImg = ImageIO.read(new URL(url));
Graphics g = img.getGraphics();//開啟畫圖
g.drawImage(bg.getScaledInstance(550, 978, Image.SCALE_DEFAULT), 0, 0, null); // 繪製縮小後的圖
g.drawImage(qRCodeImg.getScaledInstance(126, 126, Image.SCALE_DEFAULT), 47, 817, null); // 繪製縮小後的圖
g.setColor(Color.black);
g.dispose();
ImageIO.write(img, "JPEG", response.getOutputStream());
} catch (IOException e) {
LOG.error(e.getMessage());
}
}
八、生成帶引數二維碼介面
/**
* 生成帶引數二維碼
*
* @param objectId 專案id
* @param openId 微信使用者openid
* @param path 掃碼後小程式跳轉的頁面路徑
* @param thumbMediaId 小程式展示的縮圖id
* @param picId 海報背景圖在mongodb上的id
* @return
* @throws Exception
*/
@RequestMapping(value = "/generateQRCodeWithParams", method = {RequestMethod.POST, RequestMethod.GET})
public Object generateQRCodeWithParams(String objectId, String openId, String path, String thumbMediaId, String picId, HttpServletResponse response) throws Exception {
String accessToken = redisService.getValue("access_token");
//如果token不存在則生成
if (StringUtils.isEmpty(accessToken)) {
accessToken = saveToken2Redis();
}
LOG.info("*********生成帶引數二維碼方法generateQRCodeWithParams********");
LOG.info("objectId:" + objectId + " openId:" + openId + " path:" + path + " thumbMediaId:" + thumbMediaId + " picId:" + picId);
//建立二維碼ticket
String ticketResult = generateTicket(accessToken, objectId, openId, path, thumbMediaId, picId);
LOG.info("*********ticketResult:*******" + ticketResult);
String errcode = StringUtils.isNotBlank(ticketResult) ? JSONObject.parseObject(ticketResult).getString("errcode") : null;
if (StringUtils.isNotBlank(errcode) && String.valueOf(40001).equals(errcode)) {
//如果token不是最新的,重新獲取
accessToken = saveToken2Redis();
LOG.info("*********token不是最新的,重新獲取********");
ticketResult = generateTicket(accessToken, objectId, openId, path, thumbMediaId, picId);
generateQRcodeByTicket(ticketResult, picId, response);
} else {
generateQRcodeByTicket(ticketResult, picId, response);
}
return null;
}
/**
* 生成token並儲存到redis
*
* @return
* @throws Exception
*/
public String saveToken2Redis() throws Exception {
String accessToken = AccessTokenUtil.getAccessToken(appid, secret);
LOG.info("****access_token:******" + accessToken);
if (StringUtils.isNotBlank(accessToken)) {
redisService.setKeyNoExpire("access_token", accessToken);
}
return accessToken;
}
最終在使用者關注公眾號後推送的小程式如下: