公衆號開發精品教程(4)——生成帶參數的二維碼及合成海報

系列文章傳送門

公衆號開發精品教程(1)——緒論及環境搭建

公衆號開發精品教程(2)——將項目接入微信及簡單交互

公衆號開發精品教程(3)——創建菜單

公衆號開發精品教程(4)——生成帶參數的二維碼及合成海報

公衆號開發精品教程(5)——獲取用戶基本信息與網頁授權

整個項目的源碼已經上傳到百度網盤(博主的Git在維護,就不拿出來丟人了),永久有效,免費,在ChatConf類中填寫自己的APPID和開發者密鑰,在相關地方替換一下外網域名,即可使用,如有任何問題,歡迎在下方評論:

鏈接:百度網盤傳送門
提取碼:03eb

目錄

 

前言

實現思路

生成帶參數的二維碼及製作海報

上傳素材

 通過客服消息接口,將海報推給用戶

總結


前言

在上一篇文章中,我們在公衆號創建了菜單,其中有view類型的、click類型的,view類型的比較簡單,直接指定一個URL即可,click類型的按鈕功能我們還沒有完善好。那麼這一篇文章,就帶領大家實現生成海報的功能。

生成海報的場景在推廣中也很常見,我們爲用戶生成一張海報圖,通過獎勵機制促使其分享出去,其他用戶掃描海報中的二維碼就可以將其引入到公衆號來,如此循環往復,形成營銷閉環……

下面是在我朋友的公衆號生成的海報,我們今天就是要實現這樣的功能!

實現思路

用戶點擊“生成海報”按鈕,我們的功能點需要做以下這些事:

  1. 服務器端接收點擊菜單的事件,判斷EventKey
  2. 調用微信生成二維碼的接口,換取二維碼圖片
  3. 將換取到的二維碼,與現有的一張背景圖結合,生成一張帶二維碼的海報
  4. 將海報圖片發送給用戶

以上的四個步驟中,第二步、第三步是比較消耗性能的,因爲涉及到遠程IO,還有兩張圖片的結合繪製,而微信又要求必須在5秒內做出響應,所以我們接收到點擊菜單的事件之後,只獲取必要的信息(如ToUserName、FromUserName等),然後將生成海報的動作寫入阻塞隊列,再然後返回給微信一個空字符串即可,因爲微信文檔指出:若開發者無法保證在5s內做出響應,可回覆空串,微信將不做任何處理。

那麼我們返回了空串,怎樣將這張圖片發送給用戶呢?這就需要調用客服接口了,而且發送圖片消息,需要將這張圖片上傳至公衆號的素材,所以今天我們要學習使用微信的三個接口:

生成二維碼的接口、客服消息接口、上傳素材接口

總體上來講,流程如下:

今天也會是十足的乾貨,讓我們開始吧!

生成帶參數的二維碼及製作海報

接下來的所有操作,請大家打開微信公衆平臺官方文檔,以及登錄到自己的測試號,並且將自己的主機映射到外網上。

根據文檔,我們首先把創建臨時二維碼的接口地址、換取二維碼圖片的地址保存到之前的ChatConf類中:

//創建臨時二維碼的接口地址
public static final String CREATE_QRCODE_URL = "https://api.weixin.qq.com/cgi-bin/qrcode/create";
//換取二維碼的接口地址,會返回一張圖片,可直接使用IO流讀取
public static final String GET_QRCODE_URL = "https://mp.weixin.qq.com/cgi-bin/showqrcode";

創建一個創建二維碼的工具類:

package com.blog.wechat.utils;

import com.alibaba.fastjson.JSON;
import com.blog.wechat.conf.ChatConf;
import com.blog.wechat.conf.SSLConf;
import okhttp3.*;

import java.io.IOException;

/**
 *  創建二維碼的工具類
 * @author 秋楓豔夢
 * @date 2019-06-08
 * */
public class QRCodeUtil {

    /**
     *  創建二維碼
     * @param param 二維碼參數
     * @return 獲取到的ticket,調用換取二維碼圖片的接口時,需要使用此ticket
     * */
    public static String createQRCode(String param){
        OkHttpClient client = null;
        Request request = null;
        Response response = null;
        String ticket = "";

        try {
            client = new OkHttpClient.Builder()
                    .sslSocketFactory(SSLConf.getSslSocketFactory(),new SSLConf.TrustAllManager())
                    .hostnameVerifier(new SSLConf.TrustAllHost()).build();

            //請求體參數,JSON字符串,scene_str的value就是我們要放到二維碼中的參數
            String body = "{\"expire_seconds\": 604800, \"action_name\": \"QR_STR_SCENE\", \"action_info\": {\"scene\": {\"scene_str\": \""+param+"\"}}}";
            //將JSON字符串放入請求體中
            RequestBody requestBody = RequestBody.create(MediaType.parse("application/json;charset=utf-8"),body);
            //實例化請求體對象
            request = new Request.Builder().url(ChatConf.CREATE_QRCODE_URL+"?access_token="+ChatConf.getToken())
                    .post(requestBody).build();
            //發起請求,獲取響應體
            response = client.newCall(request).execute();
            if (response.isSuccessful()){
                ticket = JSON.parseObject(response.body().string()).getString("ticket");
            }
        }catch (IOException e){

        }finally {
            if (response!=null){
                response.close();
            }
            client.dispatcher().executorService().shutdown();
        }

        return ticket;
    }
}

然後創建一個生成海報的工具類,負責合成海報:

package com.blog.wechat.utils;

import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.net.URL;

/**
 *  生成海報的工具類
 * @author 秋楓豔夢
 * @date 2019-06-08
 * */
public class PosterUtil {

    /**
     *  生成海報
     * @param qrcodeUrl 換取二維碼圖片的地址,這個地址返回的是一張圖片,所以我們待會可以直接IO流讀取它
     * @param backgroundUrl 背景圖的地址,需要定位到一張圖片,如http://xxx.com/statics/img/demo.jpg
     * */
    public static File createPoster(String qrcodeUrl,String backgroundUrl){
        //最終的海報圖
        BufferedImage posterImg = new BufferedImage(530,950,BufferedImage.TYPE_INT_RGB);
        //二維碼圖片
        BufferedImage qrcodeImg;
        //海報背景圖
        BufferedImage backgroundImg;
        //File對象,將生成的海報保存到這個隨機文件名,最後再返回給調用者
        File file = new File("C:\\Users\\Administrator\\Desktop\\poster\\"+System.currentTimeMillis()+".jpg");
        try {
            //讀取二維碼圖片
            qrcodeImg = ImageIO.read(new URL(qrcodeUrl));
            backgroundImg = ImageIO.read(new URL(backgroundUrl));

            Graphics g = posterImg.getGraphics();//開啓畫圖
            g.drawImage(backgroundImg.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(posterImg, "jpg", file);
        }catch (IOException e){

        }finally {

        }
        return file;
    }
}

然後我們需要有一個阻塞隊列,去監聽生成海報的事件,在此之前我們先在項目中放一張背景圖:

要確保能夠訪問(咳咳咳,不要在意圖片是啥,重要的是知識):

 然後我們寫一個阻塞隊列的類,用於監聽生成海報的交互事件:

package com.blog.wechat.queue;

import com.blog.wechat.conf.ChatConf;
import com.blog.wechat.utils.PosterUtil;
import com.blog.wechat.utils.QRCodeUtil;

import java.io.File;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingDeque;

public class QRCodeQueue {
    //存放openid的阻塞隊列.openid即微信推送的數據包中的FromUserName
    public static BlockingQueue<String> codeQueue = new LinkedBlockingDeque<>();
    //監聽隊列的線程數量,這裏我們開啓15個線程去處理(並不是越多越好),提高吞吐量
    public static final int THREADS = 15;

    /**
     *  監聽阻塞隊列,執行相關業務
     * */
    public static void startListen(){
        for (int i = 0; i < THREADS; i++) {
            Runnable runnable = new Runnable() {
                @Override
                public void run() {
                    while (true){
                        try {
                            String openId = codeQueue.take();
                            //創建二維碼,將用戶的openid作爲參數,用於後期數據消費,同時獲取ticket
                            String ticket = QRCodeUtil.createQRCode(openId);
                            //生成海報,背景圖
                            File file = PosterUtil.createPoster(ChatConf.GET_QRCODE_URL+"?ticket="+ticket,
                                    "http://3j4xkg.natappfree.cc/statics/img/back.jpg");
                            //先到這一步,我們先運行一下,看看現在的效果,再往下進行
                        }catch (Exception e){

                        }
                    }
                }
            };
            new Thread(runnable).start();
        }
    }
}

還記得我們在之前的文章中配置好的監聽器嗎?我們在監聽器中啓動這個方法,項目一啓動就開始監聽:

package com.blog.wechat.listener;

import com.blog.wechat.queue.QRCodeQueue;
import com.blog.wechat.utils.TokenUtil;

import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;

public class ProjectListener implements ServletContextListener {
    @Override
    public void contextInitialized(ServletContextEvent servletContextEvent) {
        //定時獲取access_token
        TokenUtil.startTask();
        //監聽生成海報的事件
        QRCodeQueue.startListen();
    }

    @Override
    public void contextDestroyed(ServletContextEvent servletContextEvent) {

    }
}

然後,我們在控制層做一下處理,修改之前的代碼:

//先判斷是事件消息,還是普通消息
        if (map.get("MsgType").equals("event")){
            //如果是被關注事件,向用戶回覆內容,只需要將整理好的XML文本參數返回給微信即可
            if (map.get("Event").equals("subscribe")){
                content = "歡迎關注秋楓豔夢的測試公衆號!";
            }else if (map.get("Event").equals("CLICK")){
                //點擊菜單事件,判斷EventKey
                if (map.get("EventKey").equals("CREATE_POSTER")){
                    //這裏不返回空串了,沒必要,因爲所有的IO操作我們都是通過阻塞隊列異步實現的
                    content = "正在爲您製作海報,請稍等";
                    //寫入到阻塞隊列
                    try {
                        QRCodeQueue.codeQueue.put(toUser);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }

好了,我們來測試一下吧:

 看一下我們的文件夾:

 OK,現在海報已經生成完畢,我們暫且不處理掃碼事件,先把海報圖片發送給用戶。

上傳素材

雖然我們已經生成了海報,但是想要發送給用戶,必須要把這張圖片作爲素材上傳到公衆號,這是微信要求的,因爲調用客服消息接口發送圖片消息時,需要傳入一個media_id,而這個參數需要在上傳素材時獲取。

以下是文檔描述:

生死看淡,不服就幹,來吧皮卡丘!

首先保存上傳素材的接口地址(ChatConf.java):

//上傳素材的接口地址
public static final String UPLOAD_FILE_URL = "https://api.weixin.qq.com/cgi-bin/media/upload";

然後寫一個上傳素材的工具類:

package com.blog.wechat.utils;

import com.alibaba.fastjson.JSON;
import com.blog.wechat.conf.ChatConf;
import com.blog.wechat.conf.SSLConf;
import okhttp3.*;

import java.io.File;
import java.io.IOException;

/**
 *  上傳素材的工具類
 * @author 秋楓豔夢
 * @date 2019-06-08
 * */
public class UploadUtil {

    /**
     *  上傳圖片素材
     * @param file 素材文件
     * @return 素材ID,media_id
     * */
    public static String uploadImage(File file){
        OkHttpClient client = null;
        Request request = null;
        Response response = null;
        String mediaId = "";

        try {
            client = new OkHttpClient.Builder()
                    .sslSocketFactory(SSLConf.getSslSocketFactory(),new SSLConf.TrustAllManager())
                    .hostnameVerifier(new SSLConf.TrustAllHost()).build();

            MultipartBody body = new MultipartBody.Builder()
                    .setType(MultipartBody.FORM)
                    .addFormDataPart("media", file.getName(),RequestBody.create(MediaType.parse("application/octet-stream"),file))
                    .build();

            request = new Request.Builder().url(ChatConf.UPLOAD_FILE_URL+"?access_token="+ChatConf.getToken()+"&type=image")
                    .post(body).build();

            response = client.newCall(request).execute();
            if (response.isSuccessful()){
                //從響應體中獲取media_id
                mediaId = JSON.parseObject(response.body().string()).getString("media_id");
            }
        }catch (IOException e){

        }finally {
            if (response!=null){
                response.close();
            }
            client.dispatcher().executorService().shutdown();
        }

        return mediaId;
    }
}

我們在阻塞隊列中進行調用,修改之前的代碼如下:

/**
     *  監聽阻塞隊列,執行相關業務
     * */
    public static void startListen(){
        for (int i = 0; i < THREADS; i++) {
            Runnable runnable = new Runnable() {
                @Override
                public void run() {
                    while (true){
                        try {
                            String openId = codeQueue.take();
                            //創建二維碼,將用戶的openid作爲參數,用於後期數據消費,同時獲取ticket
                            String ticket = QRCodeUtil.createQRCode(openId);
                            //生成海報,背景圖
                            File file = PosterUtil.createPoster(ChatConf.GET_QRCODE_URL+"?ticket="+ticket,
                                    "http://3j4xkg.natappfree.cc/statics/img/back.jpg");
                            //將海報圖片上傳至素材
                            String mediaId = UploadUtil.uploadImage(file);
                            System.out.println("上傳成功:"+mediaId);
                        }catch (Exception e){

                        }
                    }
                }
            };
            new Thread(runnable).start();
        }
    }

重新運行項目,我們可以看到素材已經上傳成功了:

 通過客服消息接口,將海報推給用戶

現在我們也有media_id了,接下來就可以通過客服消息接口,將圖片發送給用戶了。但是在這之前,我們需要先爲自己的公衆號創建客服,然後才能調用發送消息的接口。

 

 保存一下這兩個接口地址:

//添加客服的接口地址
public static final String ADD_SERVICE_URL = "https://api.weixin.qq.com/customservice/kfaccount/add";
//發送客服消息的接口地址
public static final String SEND_MESSAGE_URL = "https://api.weixin.qq.com/cgi-bin/message/custom/send";

然後先創建客服(這裏的access_token就粘貼一個最新獲取的,因爲創建客服的動作一般只有一次,我們直接走main()方法):

注意:對於測試號,直接調用發送客服消息的接口就行了,這裏只是爲了記錄正式號的場景。

package com.blog.wechat.utils;

import com.blog.wechat.conf.ChatConf;
import com.blog.wechat.conf.SSLConf;
import okhttp3.*;

import java.io.IOException;

/**
 *  客服接口的工具類
 * @author 秋楓豔夢
 * @date 2019-06-08
 * */
public class ServiceUtil {

    /**
     *  創建客服
     *
     * */
    public static void createService(){
        OkHttpClient client = null;
        Request request = null;
        Response response = null;

        try {
            client = new OkHttpClient.Builder()
                    .sslSocketFactory(SSLConf.getSslSocketFactory(),new SSLConf.TrustAllManager())
                    .hostnameVerifier(new SSLConf.TrustAllHost()).build();

            String param = "{\"kf_account\":\"service-1\",\"nickname\" : \"客服1\",\"password\" : \"pswd\"}";
            RequestBody requestBody = RequestBody.create(MediaType.parse("application/json;charset=utf-8"),param);
            request = new Request.Builder().url(ChatConf.ADD_SERVICE_URL+"?access_token=22_ueOrELkPh8yT5MTy4ESXYsPOupU9uxjGplVGbMokmkl12YYzYCXZt4c8dv67QTU1bHymP4YoqfPlO8ot8CpeHRCRlJKjIkGuKOQ9HVAXSYz99rjM0LL-i8w8ZUoPBGcAIAGSX")
                    .post(requestBody).build();

            response = client.newCall(request).execute();
            if (response.isSuccessful()){
                System.out.println(response.body().string());
            }
        }catch (IOException e){

        }finally {
            if (response!=null){
                response.close();
            }
            client.dispatcher().executorService().shutdown();
        }
    }

    public static void main(String[] args) {
        createService();
    }

}

 然後我們繼續在這個類裏面寫一個發送消息的方法,最後的ServiceUtil類:

package com.blog.wechat.utils;

import com.blog.wechat.conf.ChatConf;
import com.blog.wechat.conf.SSLConf;
import okhttp3.*;

import java.io.IOException;

/**
 *  客服接口的工具類
 * @author 秋楓豔夢
 * @date 2019-06-08
 * */
public class ServiceUtil {

    /**
     *  創建客服
     *
     * */
    public static void createService(){
        OkHttpClient client = null;
        Request request = null;
        Response response = null;

        try {
            client = new OkHttpClient.Builder()
                    .sslSocketFactory(SSLConf.getSslSocketFactory(),new SSLConf.TrustAllManager())
                    .hostnameVerifier(new SSLConf.TrustAllHost()).build();

            String param = "{\"kf_account\":\"service-1\",\"nickname\" : \"客服1\",\"password\" : \"pswd\"}";
            RequestBody requestBody = RequestBody.create(MediaType.parse("application/json;charset=utf-8"),param);
            request = new Request.Builder().url(ChatConf.ADD_SERVICE_URL+"?access_token=22_ueOrELkPh8yT5MTy4ESXYsPOupU9uxjGplVGbMokmkl12YYzYCXZt4c8dv67QTU1bHymP4YoqfPlO8ot8CpeHRCRlJKjIkGuKOQ9HVAXSYz99rjM0LL-i8w8ZUoPBGcAIAGSX")
                    .post(requestBody).build();

            response = client.newCall(request).execute();
            if (response.isSuccessful()){
                System.out.println(response.body().string());
            }
        }catch (IOException e){

        }finally {
            if (response!=null){
                response.close();
            }
            client.dispatcher().executorService().shutdown();
        }
    }

    public static void main(String[] args) {
        createService();
    }

    /**
     *  發送圖片消息
     * @param openid 接收人
     * @param mediaId 素材ID
     * */
    public static void sendImage(String openid,String mediaId){
        OkHttpClient client = null;
        Request request = null;
        Response response = null;

        try {
            client = new OkHttpClient.Builder()
                    .sslSocketFactory(SSLConf.getSslSocketFactory(),new SSLConf.TrustAllManager())
                    .hostnameVerifier(new SSLConf.TrustAllHost()).build();

            String param = "{\"touser\":\""+openid+"\",\"msgtype\":\"image\",\"image\":{\"media_id\":\""+mediaId+"\"}}";
            RequestBody requestBody = RequestBody.create(MediaType.parse("application/json;charset=utf-8"),param);
            request = new Request.Builder().url(ChatConf.SEND_MESSAGE_URL+"?access_token="+ChatConf.getToken())
                    .post(requestBody).build();

            response = client.newCall(request).execute();
        }catch (IOException e){

        }finally {
            if (response!=null){
                response.close();
            }
            client.dispatcher().executorService().shutdown();
        }
    }
}

這樣,我們只需要在阻塞隊列中進行調用即可:

package com.blog.wechat.queue;

import com.blog.wechat.conf.ChatConf;
import com.blog.wechat.utils.PosterUtil;
import com.blog.wechat.utils.QRCodeUtil;
import com.blog.wechat.utils.ServiceUtil;
import com.blog.wechat.utils.UploadUtil;

import java.io.File;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingDeque;

public class QRCodeQueue {
    //存放openid的阻塞隊列.openid即微信推送的數據包中的FromUserName
    public static BlockingQueue<String> codeQueue = new LinkedBlockingDeque<>();
    //監聽隊列的線程數量,這裏我們開啓15個線程去處理(並不是越多越好),提高吞吐量
    public static final int THREADS = 15;

    /**
     *  監聽阻塞隊列,執行相關業務
     * */
    public static void startListen(){
        for (int i = 0; i < THREADS; i++) {
            Runnable runnable = new Runnable() {
                @Override
                public void run() {
                    while (true){
                        try {
                            String openId = codeQueue.take();
                            //創建二維碼,將用戶的openid作爲參數,用於後期數據消費,同時獲取ticket
                            String ticket = QRCodeUtil.createQRCode(openId);
                            //生成海報,背景圖
                            File file = PosterUtil.createPoster(ChatConf.GET_QRCODE_URL+"?ticket="+ticket,
                                    "http://3j4xkg.natappfree.cc/statics/img/back.jpg");
                            //將海報圖片上傳至素材
                            String mediaId = UploadUtil.uploadImage(file);
                            //調用客服接口,發送圖片消息,將海報發送給用戶
                            ServiceUtil.sendImage(openId,mediaId);
                        }catch (Exception e){

                        }
                    }
                }
            };
            new Thread(runnable).start();
        }
    }
}

這樣,就可以實現想要的效果了。我們來運行一下:

處理掃碼事件

海報既然生成了,就肯定有掃這個二維碼的時候,所以我們還需要處理一下掃碼事件。

根據文檔描述,掃碼事件有兩種情況:

  1. 用戶未關注公衆號,引導其進入到關注頁面,如果用戶取消關注,事件停止;如果用戶關注了,那麼微信會推給我們一個關注事件,與普通的關注事件的區別是有一個EventKey參數和Ticket參數
  2. 用戶已關注公衆號,微信則推給我們一個掃碼事件,Event類型是SCAN,依然有EventKey參數和Ticket參數

 根據以上信息,我們就可以在控制層做以下處理:

//先判斷是事件消息,還是普通消息
        if (map.get("MsgType").equals("event")){
            //如果是被關注事件,向用戶回覆內容,只需要將整理好的XML文本參數返回給微信即可
            if (map.get("Event").equals("subscribe")){
                //如果沒有EventKey,說明是普通關注,否則是掃碼關注事件
                String eventKey = map.get("EventKey");
                if (eventKey==null){
                    content = "歡迎關注秋楓豔夢的測試公衆號!";
                }else {
                    String param = eventKey.substring(eventKey.indexOf("_")+1);
                    //爲了簡單,這裏直接返回一句話,實際業務場景要更復雜
                    content = "您是由openid爲"+param+"的用戶引進來的,我們已對其進行了獎勵,您也可以生成海報,分享給朋友,可獲得獎勵";
                }
            }else if (map.get("Event").equals("CLICK")){
                //點擊菜單事件,判斷EventKey
                if (map.get("EventKey").equals("CREATE_POSTER")){
                    //這裏不返回空串了,沒必要,因爲所有的IO操作我們都是通過阻塞隊列異步實現的
                    content = "正在爲您製作海報,請稍等";
                    //寫入到阻塞隊列
                    try {
                        QRCodeQueue.codeQueue.put(fromUser);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }else if (map.get("Event").equals("SCAN")){
                content = "您已關注我們的公衆號,此活動僅限首次關注時參加一次";
            }
        }

 現在我們來測試一下,先取消關注公衆號,再來掃碼我上面生成的海報:

掃碼關注時:

 已經關注時,再次掃碼:

總結

今天的內容就到這裏了,乾貨還是挺多的。代碼量相比之前有點多,但是思路還是很清晰的,希望大家越來越好!

在下一篇文章中,博主將帶大家進行網頁授權的開發,獲取用戶的基本信息,敬請關注!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章