小弟我是做android开发的,新版本H5 需求要微信公众号开发,后台人员不够,无奈 老大嫌弃 移动端事情太少,就分配给我了.
1.外网
因为微信公众号开发在后台配置的url 只支持外网 并且是端口80或者443 的,所以准备下载一个花生壳,进行内网穿透(如果你有自己的服务器,当我没说)。在花生壳 进行内网穿透,新增映射,选择映射类型HTTP80 固定端口 ,完成:
2.微信公众号
微信公众号开发文档
微信公众平台账号 申请
1.开发配置
在微信公众平台 基本配置 或者测试号管理 里面 接口配置 ,微信服务器将发送GET请求到填写的服务器地址URL上,通过检验signature对请求进行校验,确认此次GET请求来自微信服务器,返回echostr参数内容,则接入生效。
@RequestMapping(value = "/connectValidate.do", method = RequestMethod.GET)
@ResponseBody
public void connectValidate(HttpServletRequest request, HttpServletResponse response) throws IOException {
String signature = request.getParameter("signature");
String timestamp = request.getParameter("timestamp");
String nonce = request.getParameter("nonce");
String echostr = request.getParameter("echostr");
logger.info("" + signature + "@" + timestamp + "$" + nonce + "^" + echostr);
PrintWriter out = response.getWriter();
if (CheckConnectUtils.checkConncetWithWeChat(signature, timestamp, nonce)) {
out.print(echostr);
}
}
校验代码:
public class CheckConnectUtils {
private static final String token = "token";
/**
* 判断是否链接匹配
* @param signature
* @param timestamp
* @param nonce
* @return
*/
public static boolean checkConncetWithWeChat(String signature,String timestamp,String nonce){
String[] arr = new String[]{token,timestamp,nonce};
//排序
Arrays.sort(arr);
//生成字符串
StringBuilder stringBuilder = new StringBuilder();
for (String str:arr) {
stringBuilder.append(str);
}
//进行SHA1加密
String encodeString = passSha1Encode(stringBuilder.toString());
if(signature.equals(encodeString)){
return true;
}else{
return false;
}
}
/**
* 字符串进行SHA1加密
* @param str
* @return
*/
public static String passSha1Encode(String str){
if(str == null || str.length() == 0){
return null;
}
char hexDigits[] = {'0','1','2','3','4','5','6','7','8','9'
,'a','b','c','d','e','f'};
try{
MessageDigest mdTemp = MessageDigest.getInstance("SHA1");
mdTemp.update(str.getBytes());
byte[] md = mdTemp.digest();
int j = md.length;
char[] buf = new char[j*2];
int k = 0;
for(int i=0 ; i >>4 & 0xf];
buf[k++] = hexDigits[byte0 & 0xf];
}
return new String(buf);
} catch (NoSuchAlgorithmException e) {
return null;
}
}
}
2.消息的接收和回复
首先我们创建消息实体类:
BaseMessage.java:
public class BaseMessage {
private String ToUserName;
private String FromUserName;
private String CreateTime;
private String MsgType;
public String getToUserName() {
return ToUserName;
}
public void setToUserName(String toUserName) {
ToUserName = toUserName;
}
public String getFromUserName() {
return FromUserName;
}
public void setFromUserName(String fromUserName) {
FromUserName = fromUserName;
}
public String getCreateTime() {
return CreateTime;
}
public void setCreateTime(String createTime) {
CreateTime = createTime;
}
public String getMsgType() {
return MsgType;
}
public void setMsgType(String msgType) {
MsgType = msgType;
}
}
文本消息TextMessage.java:
public class TextMessage {
private String Content;
private String MsgId;
private String ToUserName;
private String FromUserName;
private String CreateTime;
private String MsgType;
public String getToUserName() {
return ToUserName;
}
public void setToUserName(String toUserName) {
ToUserName = toUserName;
}
public String getFromUserName() {
return FromUserName;
}
public void setFromUserName(String fromUserName) {
FromUserName = fromUserName;
}
public String getCreateTime() {
return CreateTime;
}
public void setCreateTime(String createTime) {
CreateTime = createTime;
}
public String getMsgType() {
return MsgType;
}
public void setMsgType(String msgType) {
MsgType = msgType;
}
public String getContent() {
return Content;
}
public void setContent(String content) {
Content = content;
}
public String getMsgId() {
return MsgId;
}
public void setMsgId(String msgId) {
MsgId = msgId;
}
}
前期都准备好了,此时当我们向公众账号发消息时,微信服务器将POST消息的XML数据包到我们配置URL上。我们接收到消息xml包,解析根据msgType 回复我们想要回复的消息。
@RequestMapping(value = "/connectValidate.do", method = RequestMethod.POST)
@ResponseBody
public void backTextMessage(HttpServletRequest request, HttpServletResponse response) {
logger.info("公众号消息:"+ JsonUtil.toJson(request.getParameterMap()));
try {
PrintWriter writer = response.getWriter();
Map map = MessageUtil.xmlToMap(request);
logger.debug("公众号消息xmlToMap:"+ JsonUtil.toJson(map));
String fromUserName = map.get("FromUserName");
String toUserName = map.get("ToUserName");
String msgType = map.get("MsgType");
String content = map.get("Content");
String message = null;
logger.debug("content:"+content+",msgType:"+msgType);
if (MessageUtil.MESSAGE_TEXT.equals(msgType)) {
//普通文本消息回复
textMsg = “你输出的内容:”+content;
}else if(MessageUtil.MESSAGE_EVENT.equals(msgType)){
String event = map.get("Event");
//事件类型
if (MessageUtil.MESSAGE_SUBSCRIBE.equals(event)){
//关注公众号 回复
textMsg = MessageUtil.subscriBackMessage();
}else if(MessageUtil.MESSAGE_CLICK.equals(event)){
//click 类型的 菜单 回复
textMsg="Hi~亲,有什么需要帮助的吗~\n";
}
}
message = MessageUtil.initText(fromUserName, toUserName, textMsg);
logger.debug("返回内容:"+message);
writer.print(message);
} catch (IOException e) {
logger.error("公众号消息回复异常:",e);
}
}
MessageUtil 工具类:
public class MessageUtil {
/**
* 定义多种消息类型
*/
public static final String MESSAGE_TEXT = "text";
public static final String MESSAGE_IMAGE = "image";
public static final String MESSAGE_VOICE = "voice";
public static final String MESSAGE_MUSIC = "music";
public static final String MESSAGE_VIDEO = "video";
public static final String MESSAGE_LINK = "link";
public static final String MESSAGE_LOCATION = "location";
public static final String MESSAGE_EVENT = "event";
public static final String MESSAGE_SUBSCRIBE = "subscribe";
public static final String MESSAGE_UNSUBSCRIBE = "unsubscribe";
public static final String MESSAGE_CLICK = "CLICK";
public static final String MESSAGE_VIEW = "VIEW";
//扫码事件
public static final String MESSAGE_SCANCODE = "scancode_push";
public static final String CREATE_MENU_URL="https://api.weixin.qq.com/cgi-bin/menu/create?access_token=ACCESS_TOKEN";
public static final String GET_ACCESS_TOKEN_URL="https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=APPID&secret=APPSECRET";
/**
* XML格式转为map格式
* @param request
* @return
*/
public static Map xmlToMap(HttpServletRequest request){
Map map = new HashMap();
try {
InputStream inputStream =null;
inputStream = request.getInputStream();
SAXReader reader = new SAXReader();
Document doc = reader.read(inputStream);
Element rootElement = doc.getRootElement();
List elements = rootElement.elements();
for (Element el:elements) {
map.put(el.getName() , el.getText());
}
inputStream.close();
return map ;
} catch (Exception e) {
e.printStackTrace();
return null ;
}
}
/**
* 文本消息对象转为xml格式
* @param textMessage
* @return
*/
public static String textMessage2Xml(TextMessage textMessage){
XStream xStream = new XStream();
xStream.alias("xml" , textMessage.getClass());
return xStream.toXML(textMessage);
}
/**
* 设置需要返回的文本信息
* @param fromUserName
* @param toUserName
* @param content
* @return
*/
public static String initText(String fromUserName , String toUserName , String content){
TextMessage text = new TextMessage();
//注意接受消息和发送消息的顺序要烦过来
text.setFromUserName(toUserName);
text.setToUserName(fromUserName);
text.setMsgType(MESSAGE_TEXT);
long time = System.currentTimeMillis();
text.setCreateTime(String.valueOf(time));
text.setContent(content);
return textMessage2Xml(text);
}
public static String subscriBackMessage(){
StringBuffer sb = new StringBuffer();
sb.append("请多支持,谢谢");
return sb.toString();
}
public static Menu initMenu(){
Menu menu = new Menu();
ViewButton viewButton = new ViewButton();
viewButton.setName("开发文档");
viewButton.setType("view");
viewButton.setUrl("https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421140453");
ViewButton viewButton2 = new ViewButton();
viewButton2.setName("我的博客");
viewButton2.setType("view");
viewButton2.setUrl("https://mp.csdn.net/");
ClickButton clickButton = new ClickButton();
clickButton.setType("click");
clickButton.setName("在线客服");
clickButton.setKey("1");
Button button = new Button();
button.setName("在线客服");
button.setSub_button(new Button[]{clickButton});
menu.setButton(new Button[]{viewButton,viewButton2,button});
return menu;
}
public static void main(String[] args) {
String menu = JSONObject.fromObject(initMenu()).toString();
System.out.println("菜单:"+menu);
EzzHttpClient ezzHttpClient = new EzzHttpClient();
String result = ezzHttpClient.postData(CREATE_MENU_URL.replace("ACCESS_TOKEN", getToken()), menu, "utf-8");
System.out.println("创建菜单接口:"+result);
if (result!=null){
JSONObject object = JSONObject.fromObject(result);
if (object.getInt("errcode") == 0){
System.out.print("创建菜单成功");
}
}
}
public static String getToken(){
String token ="";
EzzHttpClient ezzHttpClient = new EzzHttpClient();
String url = GET_ACCESS_TOKEN_URL.replace("APPID", "你的appid").replace("APPSECRET", "你的appsecret");
String data = ezzHttpClient.getData(url, "utf-8");
System.out.println("获取token接口:"+data);
JSONObject object = JSONObject.fromObject(data);
if (object!=null){
token = object.getString("access_token");
}
return token;
}
}
此时如果 出现" 该公众号提供的服务出现故障,请稍后再试" ,请检查你的回复消息 体格式,或者转成XML 出现异常
3.创建菜单
首先从开发文档中知道,菜单分为:
1.click类型(点击事件)
用户点击click类型按钮后,微信服务器会通过消息接口(event类型)推送点击事件给开发者,并且带上你设置的key值,我们可以在msgType 为event类型 里通过key进行一系列回复。
2.view类型(网页)
用户点击view类型按钮后,会直接跳转到我们指定的url中。
注意:创建自定义菜单成功后,由于微信客户端缓存,不会立刻显示出来。建议测试时取消关注公众账号后,再次关注,则可以看到创建后的效果。
菜单实体类:Button.java
public class Button {
//菜单类型
private String type;
//菜单名称
private String name;
//二级菜单
private Button[] sub_button;
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Button[] getSub_button() {
return sub_button;
}
public void setSub_button(Button[] sub_button) {
this.sub_button = sub_button;
}
}
ClickButton.java:
public class ClickButton extends Button {
//Click类型菜单key
private String key;
public String getKey() {
return key;
}
public void setKey(String key) {
this.key = key;
}
}
ViewButton.java:
public class ViewButton extends Button {
//view类型菜单url
private String url;
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
}
Menu.java:
public class Menu {
//一级菜单
private Button[] button;
public Button[] getButton() {
return button;
}
public void setButton(Button[] button) {
this.button = button;
}
}
服务请求:EzzHttpClient.java
public String getData(String url,String charset){
String result="";
String id = Thread.currentThread().getId() + "";
if(StringTool.isBlank(charset)){
charset = "UTF-8";
}
try{
HttpGet httpget = new HttpGet(url);
logger.debug(id + " - about to get something from " + url);
try(CloseableHttpResponse response = EzzHttpClient.getHttpClient().execute(httpget)){
// get the response body as an array of bytes
HttpEntity entity = response.getEntity();
if (entity != null) {
logger.debug(id + " - " + entity.getContentLength() + " bytes read ");
result = EntityUtils.toString(entity, charset);
}else{
logger.debug(id + " - read nothing");
}
EntityUtils.consume(entity);
}
}catch(IOException e){
logger.error("启动远程调用服务异常,url:"+url, e);
}catch(Exception e){
logger.error("调用远程服务服务失败,url:"+url, e);
}
return result;
}
public String postData(String url,String data,String charset){
String result="";
String id = Thread.currentThread().getId() + "";
if(StringTool.isBlank(charset)){
charset = "UTF-8";
}
try{
HttpPost httpPost = new HttpPost(url);
httpPost.setEntity(new StringEntity(data, Charset.forName(charset)));
logger.debug(id + " - about to get something from " + url);
try(CloseableHttpResponse response = EzzHttpClient.getHttpClient().execute(httpPost)){
// get the response body as an array of bytes
HttpEntity entity = response.getEntity();
if (entity != null) {
logger.debug(id + " - " + entity.getContentLength() + " bytes read");
result = EntityUtils.toString(entity, charset);
}else{
logger.debug(id + " - read nothing ");
}
EntityUtils.consume(entity);
}
}catch(IOException e){
logger.error("启动远程调用服务异常,url:"+url, e);
}catch(Exception e){
logger.error("调用远程服务服务失败,url:"+url, e);
}
return result;
}
菜单创建代码实现在MessageUtil 中,手动往上翻即可(嘿嘿)。
最后分享下 创建菜单 当时遇到的坑:
1.{"errcode":40001,"errmsg":"invalid credential, access_token is invalid or not latest hint: [XXXXXXXXXXXXXX]"}
40001代表获取 access_token 时 AppSecret 错误,或者 access_token 无效
2.40018: invalid button name size
40018 不合法的按钮名字长度
3.40019: invalid button key size
40019 代表不合法的按钮KEY长度,检查你的clickButton是否设置了key 值
4.{"errcode":48001,"errmsg":"api unauthorized, hints: [ req_id: 1QoCla0699ns81 ]"}
如果此时你用的appId和appSecret是你申请的订阅号的,那么建议你更换为测试公众号的appid和appsecret
5.获取access_token 错误码 40164
此时在你微信公众号基本配置里配置ip白名单
最后附上错误码:
返回码 | 说明 |
---|---|
-1 | 系统繁忙 |
0 | 请求成功 |
40001 | 验证失败 |
40002 | 不合法的凭证类型 |
40003 | 不合法的OpenID |
40004 | 不合法的媒体文件类型 |
40005 | 不合法的文件类型 |
40006 | 不合法的文件大小 |
40007 | 不合法的媒体文件id |
40008 | 不合法的消息类型 |
40009 | 不合法的图片文件大小 |
40010 | 不合法的语音文件大小 |
40011 | 不合法的视频文件大小 |
40012 | 不合法的缩略图文件大小 |
40013 | 不合法的APPID |
40014 | 不合法的access_token |
40014 | 不合法的access_token |
40015 | 不合法的菜单类型 |
40016 | 不合法的按钮个数 |
40017 | 不合法的按钮个数 |
40018 | 不合法的按钮名字长度 |
40019 | 不合法的按钮KEY长度 |
40020 | 不合法的按钮URL长度 |
40021 | 不合法的菜单版本号 |
40022 | 不合法的子菜单级数 |
40023 | 不合法的子菜单按钮个数 |
40024 | 不合法的子菜单按钮类型 |
40025 | 不合法的子菜单按钮名字长度 |
40026 | 不合法的子菜单按钮KEY长度 |
40027 | 不合法的子菜单按钮URL长度 |
40028 | 不合法的自定义菜单使用用户 |
41001 | 缺少access_token参数 |
41002 | 缺少appid参数 |
41003 | 缺少refresh_token参数 |
41004 | 缺少secret参数 |
41005 | 缺少多媒体文件数据 |
41006 | 缺少media_id参数 |
41007 | 缺少子菜单数据 |
42001 | access_token超时 |
43001 | 需要GET请求 |
43002 | 需要POST请求 |
43003 | 需要HTTPS请求 |
44001 | 多媒体文件为空 |
44002 | POST的数据包为空 |
44003 | 图文消息内容为空 |
45001 | 多媒体文件大小超过限制 |
45002 | 消息内容超过限制 |
45003 | 标题字段超过限制 |
45004 | 描述字段超过限制 |
45005 | 链接字段超过限制 |
45006 | 图片链接字段超过限制 |
45007 | 语音播放时间超过限制 |
45008 | 图文消息超过限制 |
45009 | 接口调用超过限制 |
45010 | 创建菜单个数超过限制 |
46001 | 不存在媒体数据 |
46002 | 不存在的菜单版本 |
46003 | 不存在的菜单数据 |
47001 | 解析JSON/XML内容错误 |