java微信开发高级接口----群发功能

时间:2021-12-13 18:21:55

在做微信高级接口开发中,或许总会碰到很多神奇的错误码,而这些错误码在官方文档中还是找不到原因,因此贴出自己开发过程中用的一些demo,希望能够给一些小伙伴指点迷津。{"errcode":45028,"errmsg":"has no masssend quota hint: [c3ZjkA0323age9]"}如遇到这个错误码,官方文档是没有查询的,这个是因为测试号没有大型数据群发配额导致,解决办法申请一个认证的订阅号或者公众号,或者测试号模式下通过openid来测试群发,最后在公众号上更换成正确的post地址,就可以实现了,只是测试号无法用分组的那个post接口而已。


群发的步骤:


第一步,获取access_token,这部分就不写代码了,可以参照柳峰的博客专栏
http://blog.csdn.net/lyq8479/article/details/25076223 点击打开链接
获取到的ACCESS_TOKEN

第二部,发送消息

首先是准备post接口地址:

String groupUrl = "https://api.weixin.qq.com/cgi-bin/message/mass/sendall?access_token=ACCESS_TOKEN"; //这个地址是根据分组id来群发消息

String groupUrl1 = "https://api.weixin.qq.com/cgi-bin/message/mass/send?access_token=ACCESS_TOKEN"; //这个地址是根据openid来群发消息
由于接口调用有次数限制,测试号是200次一天,请珍惜。

再是准备post数据:

①文本消息

String group1data = "{\"filter\":{\"is_to_all\":false,\"group_id\":\"2\"},\"text\":{\"content\":\"群发消息测试\"},\"msgtype\":\"text\"}\";"; //这个是通过分组id发送的普通文本消息

String openid1data = "{\"touser\":[\"obGXiwHTGN_4HkR2WToFj_3ua\",\"obGXiwNu0z2o_RRWaODvaZctd\"],\"msgtype\": \"text\",\"text\": {\"content\": \"测试文本消息\"}}";//这个是通过openid发送的普通文本消息//发送给  OPENID   为obGXiwHTGN_4HkR2WToFj_3ua和obGXiwNu0z2o_RRWaODvaZctd的两位关注公众号的微信用户,可替换成开发测试号中任意的一个用户或多个用户均可。如下图的json格式就可以,分组群发的数据格式可以查看官方文档,自己拼成java字符串就好了

消息格式严格如上,可以参照官方文档微信官方文档,可以用JSONObject.from(Objec obj)这个来进行对象转json字符串,具体可以百度,红色字(由于颜色会影响代码美观,所以删除了)是关注当前微信公众号用户的openid

至于如何获取用户openid这里就不赘述,参照官方文档,或者参照博主其他文章。

②图片消息图片消息数据准备又要分两步,关键在于     获取图片     或者说    获取media_id

博主采用模拟表单上传方式来先上传一个临时素材文件并获取其id,代码如下:

import java.io.BufferedReader;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;

public class FileUpload {

	/**
	 * 模拟form表单的形式 ,上传文件 以输出流的形式把文件写入到url中,然后用输入流来获取url的响应
	 * @param url
	 *            请求地址 form表单url地址
	 * @param filePath
	 *            文件在服务器保存路径
	 * @return String url的响应信息返回值
	 * @throws IOException
	 */
	public String send(String url, String filePath) throws IOException {
		String result = null;
		File file = new File(filePath);
		if (!file.exists() || !file.isFile()) {
			throw new IOException("文件不存在");
		}
		/**
		 * 第一部分
		 */
		URL urlObj = new URL(url);
		// 连接
		HttpURLConnection con = (HttpURLConnection) urlObj.openConnection();
		/**
		 * 设置关键值
		 */
		con.setRequestMethod("POST"); // 以Post方式提交表单,默认get方式
		con.setDoInput(true);
		con.setDoOutput(true);
		con.setUseCaches(false); // post方式不能使用缓存
		// 设置请求头信息
		con.setRequestProperty("Connection", "Keep-Alive");
		con.setRequestProperty("Charset", "UTF-8");
		// 设置边界
		String BOUNDARY = "---------------------------" + System.currentTimeMillis();
		con.setRequestProperty("Content-Type", "multipart/form-data; boundary="
				+ BOUNDARY);
		// 请求正文信息
		// 第一部分:
		StringBuilder sb = new StringBuilder();
		sb.append("--"); // 必须多两道线
		sb.append(BOUNDARY);
		sb.append("\r\n");
		sb.append("Content-Disposition: form-data;name=\"media\";filename=\""
				+ file.getName() + "\"\r\n");
		sb.append("Content-Type:application/octet-stream\r\n\r\n");
		byte[] head = sb.toString().getBytes("utf-8");
		// 获得输出流
		OutputStream out = new DataOutputStream(con.getOutputStream());
		// 输出表头
		out.write(head);
		// 文件正文部分
		// 把文件以 流文件 的方式 推入到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();
		// 结尾部分
		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(
					con.getInputStream()));
			String line = null;
			while ((line = reader.readLine()) != null) {
				buffer.append(line);
			}
			if (result == null) {
				result = buffer.toString();
			}
		} catch (IOException e) {
			System.out.println("发送POST请求出现异常!" + e);
			e.printStackTrace();
			throw new IOException("数据读取异常");
		} finally {
			if (reader != null) {
				reader.close();
			}
		}
		return result;
	}

	public static void main(String[] args) throws IOException {
		String filePath = "C:/Users/Administrator/Desktop/1.jpg";//本地或服务器文件路径
		String sendUrl = "http://file.api.weixin.qq.com/cgi-bin/media/upload?access_token=ACCESS_TOKEN&type=image";//ACCESS_TOKEN是获取到的access_token
		String result = null;
		FileUpload fileUpload = new FileUpload();
		result = fileUpload.send(sendUrl, filePath);
		System.out.println(result);

	}
}
输出如下

{"type":"image","media_id":"1Le_MCBJShdaL1nLirSFRkYYiEXtPTUD_uOk0D9QtNLchWiQ2MHOtkT4Rm6T9Ciy","created_at":1447392539}

通过获取media_id,这个值也是就是后续的图文素材上传中的 thumb_media_id,这是后话。上传的图片素材是临时素材不占用永久素材容量。

数据格式如下:

String openid3data = "{\"touser\":[\"obGXiwHTGN_4HkR2WToFj_3ua\",\"obGXiwNu0z2o_RRWaODvaZctd\"], \"image\": {\"media_id\":\"1Le_MCBJShdaL1nLirSFRkYYiEXtPTUD_uOk0D9QtNLchWiQ2MHOtkT4Rm6T9Ciy\"},\"msgtype\":\"image\"}";

消息格式严格如上,可以参照官方文档 微信官方文档,可以用JSONObject.from(Objec obj)这个来进行对象转json字符串,具体可以百度,红色字( 由于颜色会影响代码美观,所以删除了,字符串中["obGXiwHTGN_4HkR2WToFj_3ua\",\"obGXiwNu0z2o_RRWaODvaZctd\"]是关注当前微信公众号用户的openid
③图文消息

图文消息需要先上传图文消息素材,

(一)上传素材

Post接口地址:https://api.weixin.qq.com/cgi-bin/media/uploadnews?access_token=ACCESS_TOKEN

post数据data格式:如是java编写需要注意特殊符号转义,这个就不多讲了,用\这个转义应该都知道。
{
   "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"
		 }
   ]
}
thumb_media_id是上一步中图片消息上传的临时素材media_id,这一部分代码如下:
import net.sf.json.JSONObject;

public class ImageArticleUpload {
	public String upload(){
		String url = "https://api.weixin.qq.com/cgi-bin/media/uploadnews?access_token=NkwaEYtrewLfkgMphjwUmnZ0l6vguB794RBhstvHUT58nLk2HMrQ3Ji9EJTD8aOyViteTyV6H1xrVLx2Myee1MUdwz0xEcw0gmlBpf6VkFoMQRfAAAQUV";//ACCESS_TOKEN是获取到的access_token
		//上传的图文消息数据,其中thumb_media_id是文件上传图片上传的id
		String data = "{\"articles\": [{\"thumb_media_id\":\"BW4eDIdYSvO7AFjfsZKsQ9ujNma_TkCj3VSo3JNTQkYmk_iPuhpUKm48oZ4umHED\",\"author\":\"xxx\",\"title\":\"Happy Day\",\"content_source_url\":\"www.qq.com\",\"content\":\"content\",\"digest\":\"digest\",\"show_cover_pic\":\"0\"}]}";
		String data1 = "{\"articles\":[{\"author\":\"王传清|毕强|Wang Chuanqing|Bi Qiang\",\"content\":\"基于关联关系维度的数字资源聚合是数字资源知识发现的重要基础和工具。超网络是由多个类型的同质和异质子网络组成的网络,通过多种关联维度聚合的数字资源即形成了拥有相同以及不同性质的结点和关系的数字资源超网络,这些不同性质的关联与链接是知识关联、挖掘、发现与创新的脉络线索。结合超网络理论,构建和描述数字资源超网络,并分析超网络中不同性质的关系类型,如引用关系、共现关系、耦合关系等,从关联维度探讨数字资源深度聚合的模式,进而分析利用数字资源超网络进行知识发现的具体应用方法,最后构建数字资源超网络聚合系统模型。\",\"content_source_url\":\"http://d.g.wanfangdata.com.cn/Periodical_qbxb201501002.aspx\",\"digest\":\"测试\",\"show_cover_pic\":1,\"thumb_media_id\":\"BW4eDIdYSvO7AFjfsZKsQ9ujNma_TkCj3VSo3JNTQkYmk_iPuhpUKm48oZ4umHED\",\"title\":\"超网络视域下的数字资源深度聚合研究\"}]}";
		JSONObject json = CommUtil.httpRequest(url, "POST", data1);
		return json.toString();
	}
	public static void main(String[] args) {
		System.out.println(new ImageArticleUpload().upload());
	}
}
输出如下:

{"type":"news","media_id":"Sf3S9D8mCM7T5TIwphe3CSAgWnXQzaDnuA7mdgX6SV1JKRGRv9Je3e7AXB8St7yy","created_at":1447394345}

这个时候获取到的media_id就是我们要进行图文消息群发的media_id

(二)组装数据:

数据格式如下:

String openid4data = "{\"touser\":[\"obGXiwHTGN_4HkR2WToFj_3ua\",\"obGXiwNu0z2o_RRWaODvaZctd\"], \"mpnews\": {\"media_id\":\"Sf3S9D8mCM7T5TIwphe3CSAgWnXQzaDnuA7mdgX6SV1JKRGRv9Je3e7AXB8St7yy\"},\"msgtype\":\"mpnews\"}";

详情参照官方文档。 微信官方文档


接下来是voice群发,视频群发,操作差不多,官方文档比较垃圾简单的说明了如何处理数据,我就不过多介绍了。


准备好数据后就是进行群发操作了。

代码如下:直接拷贝的注意更改token和media_id

import net.sf.json.JSONObject;

public class SendGroupMessage {
	public String sendGroupMessage(){
		String groupUrl = "https://api.weixin.qq.com/cgi-bin/message/mass/sendall?access_token=ACCESS_TOKEN";//ACCESS_TOKEN是获取到的access_token,根据分组id发群发消息地址
		String groupUrl1 = "https://api.weixin.qq.com/cgi-bin/message/mass/send?access_token=NkwaEYtrewLfkgMphjwUmnZ0l6vguB794RBhstvHUT58nLk2HMrQ3Ji9EJTD8aOyViteTyV6H1xrVLx2Myee1MUdwz0xEcw0gmlBpf6VkFoMQRfAAAQUV";//根据openid发群发消息地址
		String group1data = "{\"filter\":{\"is_to_all\":false,\"group_id\":\"2\"},\"text\":{\"content\":\"群发消息测试\"},\"msgtype\":\"text\"}\";";
		String openid1data = "{\"touser\":[\"obGXiwHTGN_4HkR2WToFj_3uaEKY\",\"obGXiwNu0z2o_RRWaODvaZctdWEM\"],\"msgtype\": \"text\",\"text\": {\"content\": \"测试文本消息\"}}";
		String openid2data = "{\"touser\":[\"obGXiwHTGN_4HkR2WToFj_3uaEKY\",\"obGXiwNu0z2o_RRWaODvaZctdWEM\"], \"voice\": {\"media_id\":\"UfMRvSiXAD5_iUS8u0Gc3JrKGWOABE9ivQbgrX6i-mVrKGBRL9KnKlioK1BxTPc3\"},\"msgtype\":\"voice\"}";
		String openid3data = "{\"touser\":[\"obGXiwHTGN_4HkR2WToFj_3uaEKY\",\"obGXiwNu0z2o_RRWaODvaZctdWEM\"], \"image\": {\"media_id\":\"fNUzGbYzTRui4N7-eyx9e3viP8uJuzztAvA32lIdjX4Cucj7mGN_1jpWjn7O80c8\"},\"msgtype\":\"image\"}";
		String openid4data = "{\"touser\":[\"obGXiwHTGN_4HkR2WToFj_3uaEKY\",\"obGXiwNu0z2o_RRWaODvaZctdWEM\"], \"mpnews\": {\"media_id\":\"6I8DOB-7rJsY_zdOCe6YJKJ59MwXWPb2iYBKVqb22cBHPtECYdRgiWIULfCW-hcF\"},\"msgtype\":\"mpnews\"}";
		JSONObject json = CommUtil.httpRequest(groupUrl1, "POST", openid4data);
		return json.toString();
	}
	public static void main(String[] args) {
		System.out.println(new SendGroupMessage().sendGroupMessage());
	}
}
运行代码如下:

{"errcode":0,"errmsg":"send job submission success","msg_id":2548581905,"msg_data_id":401097527}

这个时候表示群发成功,大功告成。

当然,博主不会漏掉那个工具类的代码的,代码如下:

import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.ConnectException;
import java.net.URL;

import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;

import net.sf.json.JSONException;
import net.sf.json.JSONObject;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.pojo.AccessToken;
import com.pojo.BindPhone;
import com.pojo.JsapiTicket;
import com.pojo.Menu;
import com.thread.TokenThread;

/**
 * 
 * @Description: 公众平台通用接口工具类
 * @Package Name: com.util
 * @ClassName: CommUtil
 * @author heboy
 * @date 2015年9月8日 下午2:21:56
 */
public class CommUtil {
	private static Logger log = LoggerFactory.getLogger(CommUtil.class);
	// 获取access_token的接口地址(GET) 限200(次/天)
	public final static String access_token_url = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=APPID&secret=APPSECRET";
	public final static String jsapi_ticket_url = "https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token=ACCESS_TOKEN&type=jsapi";
	
	/**
	 * 发起https请求并获取结果
	 * 
	 * @param requestUrl 请求地址
	 * @param requestMethod 请求方式(GET、POST)
	 * @param outputStr 提交的数据
	 * @return JSONObject(通过JSONObject.get(key)的方式获取json对象的属性值)
	 */
	public static JSONObject httpRequest(String requestUrl, String requestMethod, String outputStr) {
		JSONObject jsonObject = null;
		StringBuffer buffer = new StringBuffer();
		try {
			// 创建SSLContext对象,并使用我们指定的信任管理器初始化
			TrustManager[] tm = { new MyX509TrustManager() };
			SSLContext sslContext = SSLContext.getInstance("SSL", "SunJSSE");
			sslContext.init(null, tm, new java.security.SecureRandom());
			// 从上述SSLContext对象中得到SSLSocketFactory对象
			SSLSocketFactory ssf = sslContext.getSocketFactory();

			URL url = new URL(requestUrl);
			HttpsURLConnection httpUrlConn = (HttpsURLConnection) url.openConnection();
			httpUrlConn.setSSLSocketFactory(ssf);

			httpUrlConn.setDoOutput(true);
			httpUrlConn.setDoInput(true);
			httpUrlConn.setUseCaches(false);
			// 设置请求方式(GET/POST)
			httpUrlConn.setRequestMethod(requestMethod);

			if ("GET".equalsIgnoreCase(requestMethod))
				httpUrlConn.connect();

			// 当有数据需要提交时
			if (null != outputStr) {
				OutputStream outputStream = httpUrlConn.getOutputStream();
				// 注意编码格式,防止中文乱码
				outputStream.write(outputStr.getBytes("UTF-8"));
				outputStream.close();
			}

			// 将返回的输入流转换成字符串
			InputStream inputStream = httpUrlConn.getInputStream();
			InputStreamReader inputStreamReader = new InputStreamReader(inputStream, "utf-8");
			BufferedReader bufferedReader = new BufferedReader(inputStreamReader);

			String str = null;
			while ((str = bufferedReader.readLine()) != null) {
				buffer.append(str);
			}
			bufferedReader.close();
			inputStreamReader.close();
			// 释放资源
			inputStream.close();
			inputStream = null;
			httpUrlConn.disconnect();
			jsonObject = JSONObject.fromObject(buffer.toString());
		} catch (ConnectException ce) {
			log.error("Weixin server connection timed out.");
		} catch (Exception e) {
			log.error("https request error:{}", e);
		}
		return jsonObject;
	}

	/**
	 * 
	 * @Description: 获取access_token
	 * @param appid 
	 * @param appsecret 
	 * @Title: getAccessToken
	 * @return AccessToken    
	 * @date: 2015年9月8日 下午2:23:01
	 * @author heboy
	 */
	public static AccessToken getAccessToken(String appid, String appsecret) {
		AccessToken accessToken = null;

		String requestUrl = access_token_url.replace("APPID", appid).replace("APPSECRET", appsecret);
		JSONObject jsonObject = httpRequest(requestUrl, "GET", null);
		// 如果请求成功
		if (null != jsonObject) {
			try {
				accessToken = new AccessToken();
				accessToken.setToken(jsonObject.getString("access_token"));
				accessToken.setExpiresIn(jsonObject.getInt("expires_in"));
			} catch (JSONException e) {
				accessToken = null;
				// 获取token失败
				log.error("获取token失败 errcode:{} errmsg:{}", jsonObject.getInt("errcode"), jsonObject.getString("errmsg"));
			}
		}
		return accessToken;
	}
	
	// 菜单创建(POST) 限100(次/天)
	public static String menu_create_url = "https://api.weixin.qq.com/cgi-bin/menu/create?access_token=ACCESS_TOKEN";

	/**
	 * 创建菜单
	 * 
	 * @param menu 菜单实例
	 * @param accessToken 有效的access_token
	 * @return 0表示成功,其他值表示失败
	 */
	public static int createMenu(Menu menu, String accessToken) {
		int result = 0;

		// 拼装创建菜单的url
		String url = menu_create_url.replace("ACCESS_TOKEN", accessToken);
		// 将菜单对象转换成json字符串
		String jsonMenu = JSONObject.fromObject(menu).toString();
		// 调用接口创建菜单
		JSONObject jsonObject = httpRequest(url, "POST", jsonMenu);

		if (null != jsonObject) {
			if (0 != jsonObject.getInt("errcode")) {
				result = jsonObject.getInt("errcode");
				log.error("创建菜单失败 errcode:{} errmsg:{}", jsonObject.getInt("errcode"), jsonObject.getString("errmsg"));
			}
		}

		return result;
	}
	/**
	 * 
	 * @Description: 获取js票据
	 * @Title: getJsapiTicket
	 * @return JsapiTicket    
	 * @date: 2015年9月11日 上午10:48:07
	 * @author heboy
	 */
	public static JsapiTicket getJsapiTicket(String tocken){
		JsapiTicket jsapiTicket = null;
		String requestUrl = jsapi_ticket_url.replace("ACCESS_TOKEN", tocken);
		JSONObject jsonObject = httpRequest(requestUrl, "GET", null);
		// 如果请求成功
		if (null != jsonObject) {
			try {
				jsapiTicket = new JsapiTicket();
				jsapiTicket.setTicket(jsonObject.getString("ticket"));
				jsapiTicket.setExpiresIn(jsonObject.getInt("expires_in"));
			} catch (JSONException e) {
				jsapiTicket = null;
				// 获取token失败
				log.error("获取jsapi_ticket失败 errcode:{} errmsg:{}", jsonObject.getInt("errcode"), jsonObject.getString("errmsg"));
			}
		}
		return jsapiTicket;
	}
}