0、寫在前面的話
如何實現微信平台后台管理中的,圖文消息發送功能?
大概的過程如下:
- 通過類似表單的形式,將文章各部分內容提交到后台,封裝成一個實體類,並持久化到數據庫中
- 需要推送的時候,將不同的文章選擇取出交給后台,由后台組裝成規范化的數據結構,調用微信的圖文消息素材上傳和群發接口
- 其中文章的主體部分,我們采用UEditor富文本編輯器
本文主要針對模擬表單的圖片上傳,以及結合UEditor進行圖文內圖片上傳,進行重點說明。細節和具體代碼流程就不再詳細展開了。
參考鏈接:
1、什么是圖文消息
所謂圖文消息,就是我們常在訂閱號中收到的如下圖所示的消息類型:

從
微信開發者文檔 - 消息管理 - 發送消息 - 群發接口和原創校驗 - 上傳圖文消息素材 中可以看到,圖文消息就是文章的集合,每條圖文消息包含的文章不得超過8條。
我們從開發文檔中上傳圖文消息素材的POST格式來看,一個文章大概包含的內容有:
{
"articles": [
{
"thumb_media_id":"qI6_Ze_6PtV7svjolgs-rN6stStuHIjs9_DidOHaj0Q-mwvBelOXCFZiq2OsIU-p",
"author":"xxx",
"title":"Happy Day",
"content_source_url":"www.qq.com",
"content":"content",
"digest":"digest",
"show_cover_pic":1
},
{
"thumb_media_id":"qI6_Ze_6PtV7svjolgs-rN6stStuHIjs9_DidOHaj0Q-mwvBelOXCFZiq2OsIU-p",
"author":"xxx",
"title":"Happy Day",
"content_source_url":"www.qq.com",
"content":"content",
"digest":"digest",
"show_cover_pic":0
}
]
}
22
1
{
2
"articles": [
3
{
4
"thumb_media_id":"qI6_Ze_6PtV7svjolgs-rN6stStuHIjs9_DidOHaj0Q-mwvBelOXCFZiq2OsIU-p",
5
"author":"xxx",
6
"title":"Happy Day",
7
"content_source_url":"www.qq.com",
8
"content":"content",
9
"digest":"digest",
10
"show_cover_pic":1
11
},
12
{
13
"thumb_media_id":"qI6_Ze_6PtV7svjolgs-rN6stStuHIjs9_DidOHaj0Q-mwvBelOXCFZiq2OsIU-p",
14
"author":"xxx",
15
"title":"Happy Day",
16
"content_source_url":"www.qq.com",
17
"content":"content",
18
"digest":"digest",
19
"show_cover_pic":0
20
}
21
]
22
}
參數 | 是否必須 | 說明 |
Articles | 是 | 圖文消息,一個圖文消息支持1到8條圖文 |
thumb_media_id | 是 | 圖文消息縮略圖的media_id,可以在基礎支持-上傳多媒體文件接口中獲得 |
author | 否 | 圖文消息的作者 |
title | 是 | 圖文消息的標題 |
content_source_url | 否 | 在圖文消息頁面點擊“閱讀原文”后的頁面,受安全限制,如需跳轉Appstore,可以使用itun.es或appsto.re的短鏈服務,並在短鏈后增加 #wechat_redirect 后綴。 |
content | 是 | 圖文消息頁面的內容,支持HTML標簽。具備微信支付權限的公眾號,可以使用a標簽,其他公眾號不能使用 |
digest | 否 | 圖文消息的描述 |
show_cover_pic | 否 | 是否顯示封面,1為顯示,0為不顯示 |
而本篇博客要解決的,就是
如何獲取上圖紅色部分的 thumb_media_id 和 content,后者主要也是闡述其內容中的圖片上傳過程。
2、群發圖文消息的過程和UEditor使用
由微信的官方文檔可知,
群發圖文消息的過程如下:
- 首先,調用接口上傳封面縮略圖素材,獲取thumb_media_id;(目標1)
- 然后將圖文消息中需要用到的圖片,使用上傳圖文消息內圖片接口,上傳成功並獲得圖片 URL;(目標2)
- 上傳圖文消息素材,需要用到圖片時,請使用上一步獲取的圖片 URL;
- 使用對用戶標簽的群發,或對 OpenID 列表的群發,將圖文消息群發出去,群發時微信會進行原創校驗,並返回群發操作結果;
- 在上述過程中,如果需要,還可以預覽圖文消息、查詢群發狀態,或刪除已群發的消息等。
我們的目標很明確,下面就開始按步驟進行說明。
2.1 啟用UEditor
UEditor是百度的一款富文本編輯器,用它的主要目的是因為它配置比較靈活,所見即所得的友好體驗。這里只會提供編輯器部分,其他諸如標題作者,需要自行在網頁添加,此處不再闡述。
簡單說來,最終我們是為了實現一個類似提交表單的功能,
把各部分內容最后封裝到一個實體類中,而UEditor部分就是用來封裝文章的content的。
UEditor的安裝和工具欄設置等不是本文的討論重點,此處請自行谷歌,或按照官方文檔進行部署。網頁上部署好以后(如下圖中的部分3),再自行添加如標題輸入,作者輸入等input標簽,得到效果如下:
參考鏈接:

2.2 封面和文章內圖片上傳
2.2.1 上傳封面縮略圖永久素材
在永久圖文消息中,要求縮略圖也是永久素材類型,所以我們這里調用的接口是 “新增其他類型的永久素材的接口”,如下所示(這里type我們要用thumb):
http請求方式: POST,需使用https
https://api.weixin.qq.com/cgi-bin/material/add_material?access_token=ACCESS_TOKEN&type=TYPE
調用示例(使用curl命令,用FORM表單方式新增一個其他類型的永久素材,curl命令的使用請自行查閱資料)
4
1
http請求方式: POST,需使用https
2
https://api.weixin.qq.com/cgi-bin/material/add_material?access_token=ACCESS_TOKEN&type=TYPE
3
4
調用示例(使用curl命令,用FORM表單方式新增一個其他類型的永久素材,curl命令的使用請自行查閱資料)
參數說明:
- access_token(必需) 調用接口憑證
- type(必需) 媒體文件類型,分別有圖片(image)、語音(voice)、視頻(video)和縮略圖(thumb)
- media(必需) form-data中媒體文件標識,有filename、filelength、content-type等信息
那么問題來了,為了避免接口次數的頻繁使用和浪費,我們在標題0中提到的過程是,將文章各部分內容提交到后台,封裝成一個實體類,並持久化到數據庫中。需要推送的時候,再取出來調用微信接口進行上傳和推送(而不是編輯的時候就調用微信的接口)。
也就是說,縮略圖信息,都是提前寫好並整合封裝在某個類中的(或者放在數據庫中),這意味着我們要在推送消息的時候從內容里把需要上傳的東西抽出來再去調用接口,獲取返回值后再組裝微信格式的圖文消息發送。
而這里的接口要求使用表單的形式提交才能生效,但用這種方式,密鑰信息accessToken就暴露在頁面中,並且也獲取不到上傳成功后的媒體ID。怎么辦?
public static String uploadTemp(File file, String url) throws IOException {
String REQUEST_METHOD = "POST";
String result = null;
//請求地址
URL urlObj = new URL(url);
//連接
HttpURLConnection connection = (HttpURLConnection) urlObj.openConnection();
//設置關鍵值
//POST提交表單,默認是GET
connection.setRequestMethod(REQUEST_METHOD);
connection.setDoInput(true);
connection.setDoOutput(true);
//POST方式無法使用緩存
connection.setUseCaches(false);
//設置請求頭信息
connection.setRequestProperty("Connection", "Keep-Alive");
connection.setRequestProperty("Charset", "UTF-8");
//設置邊界
String BOUNDARY = "----------" + System.currentTimeMillis();
connection.setRequestProperty("Content-Type", "multipart/form-data; boundary=" + BOUNDARY);
//請求正文信息
//part1 開頭
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append("--"); // 必須多兩道線
stringBuilder.append(BOUNDARY);
stringBuilder.append("\r\n");
stringBuilder.append("Content-Disposition: form-data;name=\"file\";filename=\"" + file.getName() + "\"\r\n");
stringBuilder.append("Content-Type:application/octet-stream\r\n\r\n");
byte[] head = stringBuilder.toString().getBytes("utf-8");
//獲得輸出流
OutputStream out = new DataOutputStream(connection.getOutputStream());
//輸出表頭
out.write(head);
//part2 文件正文
//把文件以流文件的方式,推入到url中
DataInputStream in = new DataInputStream(new FileInputStream(file));
int bytes = 0;
byte[] bufferOut = new byte[1024];
while ((bytes = in.read(bufferOut)) != -1) {
out.write(bufferOut, 0, bytes);
}
in.close();
//part3 結尾部分
byte[] foot = ("\r\n--" + BOUNDARY + "--\r\n").getBytes("utf-8");// 定義最后數據分隔線
out.write(foot);
out.flush();
out.close();
StringBuffer buffer = new StringBuffer();
BufferedReader reader = null;
try {
// 定義BufferedReader輸入流來讀取URL的響應
reader = new BufferedReader(new InputStreamReader(connection.getInputStream()));
String line = null;
while ((line = reader.readLine()) != null) {
buffer.append(line);
}
if (result == null) {
result = buffer.toString();
}
} catch (IOException e) {
log.error("發送POST請求出現異常!" + e);
e.printStackTrace();
throw new IOException("數據讀取異常");
} finally {
if(reader!=null) {
reader.close();
}
}
return result;
}
85
1
public static String uploadTemp(File file, String url) throws IOException {
2
String REQUEST_METHOD = "POST";
3
String result = null;
4
5
//請求地址
6
URL urlObj = new URL(url);
7
8
//連接
9
HttpURLConnection connection = (HttpURLConnection) urlObj.openConnection();
10
11
//設置關鍵值
12
//POST提交表單,默認是GET
13
connection.setRequestMethod(REQUEST_METHOD);
14
connection.setDoInput(true);
15
connection.setDoOutput(true);
16
//POST方式無法使用緩存
17
connection.setUseCaches(false);
18
19
//設置請求頭信息
20
connection.setRequestProperty("Connection", "Keep-Alive");
21
connection.setRequestProperty("Charset", "UTF-8");
22
23
//設置邊界
24
String BOUNDARY = "----------" + System.currentTimeMillis();
25
connection.setRequestProperty("Content-Type", "multipart/form-data; boundary=" + BOUNDARY);
26
27
//請求正文信息
28
//part1 開頭
29
StringBuilder stringBuilder = new StringBuilder();
30
stringBuilder.append("--"); // 必須多兩道線
31
stringBuilder.append(BOUNDARY);
32
stringBuilder.append("\r\n");
33
stringBuilder.append("Content-Disposition: form-data;name=\"file\";filename=\"" + file.getName() + "\"\r\n");
34
stringBuilder.append("Content-Type:application/octet-stream\r\n\r\n");
35
36
byte[] head = stringBuilder.toString().getBytes("utf-8");
37
38
//獲得輸出流
39
OutputStream out = new DataOutputStream(connection.getOutputStream());
40
//輸出表頭
41
out.write(head);
42
43
//part2 文件正文
44
//把文件以流文件的方式,推入到url中
45
DataInputStream in = new DataInputStream(new FileInputStream(file));
46
int bytes = 0;
47
byte[] bufferOut = new byte[1024];
48
while ((bytes = in.read(bufferOut)) != -1) {
49
out.write(bufferOut, 0, bytes);
50
}
51
in.close();
52
53
//part3 結尾部分
54
byte[] foot = ("\r\n--" + BOUNDARY + "--\r\n").getBytes("utf-8");// 定義最后數據分隔線
55
out.write(foot);
56
57
58
out.flush();
59
out.close();
60
61
62
StringBuffer buffer = new StringBuffer();
63
BufferedReader reader = null;
64
try {
65
// 定義BufferedReader輸入流來讀取URL的響應
66
reader = new BufferedReader(new InputStreamReader(connection.getInputStream()));
67
String line = null;
68
while ((line = reader.readLine()) != null) {
69
buffer.append(line);
70
}
71
if (result == null) {
72
result = buffer.toString();
73
}
74
} catch (IOException e) {
75
log.error("發送POST請求出現異常!" + e);
76
e.printStackTrace();
77
throw new IOException("數據讀取異常");
78
} finally {
79
if(reader!=null) {
80
reader.close();
81
}
82
}
83
84
return result;
85
}
最終返回的result字符串,是一個JSON格式的微信服務器返回的值,其中我們可以提取其中的media_id。
2.2.2 結合UEditor實現圖文內圖片消息的圖片上傳
上傳圖文消息內的圖片,有專門的接口,這個接口上傳的圖片是不占用永久素材的數量限制的。調用該接口,可以獲得圖片的URL,這里的調用方式,也是和如上一樣使用表單模擬提交的方式,這里主要講解的是,如何結合UEditor。
UEditor編輯器中的圖片上傳,我們需要修改其上傳后的地址,也就是說希望它把圖片以流的形式給我們自己寫的Action,再由我們的Action把文件提交給微信服務器以獲取URL,再放到文章中去。
首先,更改編輯器的上傳圖片的后台地址,重寫其js腳本部分的getActionUrl方法,定義跳轉到自己的Action:
ue = UE.getEditor("editor");
//重寫UEDITOR的getActionUrl 方法,定義自己的Action,上傳圖片保存至相應的位置
UE.Editor.prototype._bkGetActionUrl = UE.Editor.prototype.getActionUrl;
UE.Editor.prototype.getActionUrl = function (action) {
if (action == 'uploadimage' || action == 'uploadfile') {
var id = $('#carInfoId').val();
//修改定義自己的Action
return '/admin/news/do/ueditor_uploadNewsImage.q';
} else {
return this._bkGetActionUrl.call(this, action);
}
};
12
1
ue = UE.getEditor("editor");
2
//重寫UEDITOR的getActionUrl 方法,定義自己的Action,上傳圖片保存至相應的位置
3
UE.Editor.prototype._bkGetActionUrl = UE.Editor.prototype.getActionUrl;
4
UE.Editor.prototype.getActionUrl = function (action) {
5
if (action == 'uploadimage' || action == 'uploadfile') {
6
var id = $('#carInfoId').val();
7
//修改定義自己的Action
8
return '/admin/news/do/ueditor_uploadNewsImage.q';
9
} else {
10
return this._bkGetActionUrl.call(this, action);
11
}
12
};
由於提交過來的是tmp格式,直接調用微信接口會出現類型錯誤的返回值,所以在Action中我們要將圖片格式轉換一下再調用微信接口:
參考鏈接:
/**
* ueditor編輯器上傳圖片到微信服務器
* <p>該部分是使用了百度的富文本web編輯器UEditor,以及微信的圖文消息內圖片上傳接口</p>
*
* @throws IOException
*/
public void ueditor_uploadNewsImage() throws IOException {
WeChatUtil weChatUtil = new WeChatUtil();
String accessToken = weChatUtil.getAccessToken();
HttpServletResponse response = ServletActionContext.getResponse();
//struts請求 包裝過濾器
MultiPartRequestWrapper wrapper = (MultiPartRequestWrapper) ServletActionContext.getRequest();
//獲取文件過濾器
File tmp = wrapper.getFiles("upfile")[0];
//獲得上傳的文件名
String filename = wrapper.getFileNames("upfile")[0];
int suffixIndex = filename.indexOf(".");
//文件后綴確定
if (suffixIndex == filename.lastIndexOf(".")) {
String suffix = filename.substring(suffixIndex);
File file = File.createTempFile("tmp", suffix);
FileCopyUtils.copy(tmp, file);
String imageUrl = weChatUtil.uploadImg(accessToken, file); //這里最終還是調用的表單模擬的那個方法
//如果獲取到了有效的imageUrl
if (imageUrl != null && imageUrl.substring(0, 4).equals("http")) {
String json = "{\"state\":\"SUCCESS\", \"url\":\"%s\", \"title\":\"%s\", \"original\":\"%s\"}";
json = String.format(json, imageUrl, filename, filename);
log.debug("上傳圖片返回信息:" + json);
response.getWriter().write(json);
}
}
}
x
1
/**
2
* ueditor編輯器上傳圖片到微信服務器
3
* <p>該部分是使用了百度的富文本web編輯器UEditor,以及微信的圖文消息內圖片上傳接口</p>
4
*
5
* @throws IOException
6
*/
7
public void ueditor_uploadNewsImage() throws IOException {
8
WeChatUtil weChatUtil = new WeChatUtil();
9
String accessToken = weChatUtil.getAccessToken();
10
HttpServletResponse response = ServletActionContext.getResponse();
11
12
//struts請求 包裝過濾器
13
MultiPartRequestWrapper wrapper = (MultiPartRequestWrapper) ServletActionContext.getRequest();
14
//獲取文件過濾器
15
File tmp = wrapper.getFiles("upfile")[0];
16
//獲得上傳的文件名
17
String filename = wrapper.getFileNames("upfile")[0];
18
19
int suffixIndex = filename.indexOf(".");
20
//文件后綴確定
21
if (suffixIndex == filename.lastIndexOf(".")) {
22
String suffix = filename.substring(suffixIndex);
23
File file = File.createTempFile("tmp", suffix);
24
FileCopyUtils.copy(tmp, file);
25
26
String imageUrl = weChatUtil.uploadImg(accessToken, file); //這里最終還是調用的表單模擬的那個方法
27
//如果獲取到了有效的imageUrl
28
if (imageUrl != null && imageUrl.substring(0, 4).equals("http")) {
29
String json = "{\"state\":\"SUCCESS\", \"url\":\"%s\", \"title\":\"%s\", \"original\":\"%s\"}";
30
json = String.format(json, imageUrl, filename, filename);
31
log.debug("上傳圖片返回信息:" + json);
32
response.getWriter().write(json);
33
}
34
}
35
}
另外,需要注意的是,UEditor要求
后端返回固定格式的返回碼才能實現圖片插入到文本編輯內容中。注意這里的json返回碼格式,內部必須使用雙引號,使用單引號替代是無效的。
上面兩個就是該部分的核心內容,還有一個小地方需要修改,如果使用的Struts框架,那么要注意,由於Struts2框架默認使用Apache的Commons FileUpload組件和內建的FileUploadInterceptor攔截器實現文件上傳,將request中的文件域封裝到action中的一個File類型的屬性中,並刪除request中的原有文件域,因此直接用Ueditor上傳文件會失敗。
解決的方法是自己寫一個Filter替代Struts2的Filter,排除掉ueditor的controller.jsp文件。
參考鏈接:
public class MyStrutsFilter extends StrutsPrepareAndExecuteFilter {
public void doFilter(ServletRequest req, ServletResponse res,FilterChain chain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
//不過濾的url
String url = request.getRequestURI();
System.out.println(url);
try {
if (url.contains("/controller.jsp")) {
System.out.println("使用自定義的過濾器");
chain.doFilter(req, res);
} else {
System.out.println("使用默認的過濾器");
super.doFilter(req, res, chain);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
19
1
public class MyStrutsFilter extends StrutsPrepareAndExecuteFilter {
2
public void doFilter(ServletRequest req, ServletResponse res,FilterChain chain) throws IOException, ServletException {
3
HttpServletRequest request = (HttpServletRequest) req;
4
//不過濾的url
5
String url = request.getRequestURI();
6
System.out.println(url);
7
try {
8
if (url.contains("/controller.jsp")) {
9
System.out.println("使用自定義的過濾器");
10
chain.doFilter(req, res);
11
} else {
12
System.out.println("使用默認的過濾器");
13
super.doFilter(req, res, chain);
14
}
15
} catch (Exception e) {
16
e.printStackTrace();
17
}
18
}
19
}
2.3 圖文素材上傳和發送
到這里,我們再來看下上傳圖文永久素材所需要的格式:
{
"articles": [
{
"thumb_media_id":"qI6_Ze_6PtV7svjolgs-rN6stStuHIjs9_DidOHaj0Q-mwvBelOXCFZiq2OsIU-p",
"author":"xxx",
"title":"Happy Day",
"content_source_url":"www.qq.com",
"content":"content",
"digest":"digest",
"show_cover_pic":1
},
{
"thumb_media_id":"qI6_Ze_6PtV7svjolgs-rN6stStuHIjs9_DidOHaj0Q-mwvBelOXCFZiq2OsIU-p",
"author":"xxx",
"title":"Happy Day",
"content_source_url":"www.qq.com",
"content":"content",
"digest":"digest",
"show_cover_pic":0
}
]
}
1
{
2
"articles": [
3
{
4
"thumb_media_id":"qI6_Ze_6PtV7svjolgs-rN6stStuHIjs9_DidOHaj0Q-mwvBelOXCFZiq2OsIU-p",
5
"author":"xxx",
6
"title":"Happy Day",
7
"content_source_url":"www.qq.com",
8
"content":"content",
9
"digest":"digest",
10
"show_cover_pic":1
11
},
12
{
13
"thumb_media_id":"qI6_Ze_6PtV7svjolgs-rN6stStuHIjs9_DidOHaj0Q-mwvBelOXCFZiq2OsIU-p",
14
"author":"xxx",
15
"title":"Happy Day",
16
"content_source_url":"www.qq.com",
17
"content":"content",
18
"digest":"digest",
19
"show_cover_pic":0
20
}
21
]
22
}
- thumb_media_id 上傳封面縮略圖后返回的media_id,我們已經說明了
- author 這個寫個input就可以搞定
- title 同上
- content_source_url 非必需
- content 這里就是我們用UEditor編輯器中的內容了,其中重點是圖片上傳,我們已經說明了
- digest 摘要,加input解決
- show_cover_pic 非必需
東西都有了,調用接口也就簡單了。
然后接着調用群發的接口,也就很容易了。這里主要參考文檔(微信公眾平台技術文檔 - 消息管理 - 發送消息 - 群發接口和原創校驗)就行了,時間精力有限,就不再展開了。