- 微信公众号是一个非常轻便的项目平台,我主要是做web应用,在我看来,公众号就是一个web项目存放平台,通过view类型的菜单,可以存放多个web应用。
- 公众号开发入门:
配置服务器地址————即为你的公众号token验证的controller(我用的SpringMVC),这个controller比较简单,但是极为重要,这是整个项目的入口,关于微信公众号的最基本操作都放在里面,比如token验证,消息回复,其中也有一定的代码规范,比如异常处理,又比如在五秒内必须做出对微信消息的响应,如果代码不规范就会造成你的控制太明明打印的是验证成功,微信公众平台却显示配置失败的情况。
令牌(Token)————这个和你检验程序中的token一致就可以。
消息加解秘钥————我选择随机生成,(43位太麻烦,建议大家还是随机生成吧)。
消息加解密方式————如果要追求安全系数可以选择安全模式,但要自己写加密程序。
wechatcontroller:
@Autowired
private WechatPostService wechatPostService;
@RequestMapping(value = "/wechat/connect", method = { RequestMethod.GET, RequestMethod.POST })
@ResponseBody
public void ConnectWeixin(HttpServletRequest request, HttpServletResponse response) throws IOException, DocumentException {
// 将请求、响应的编码均设置为UTF-8(防止中文乱码)
request.setCharacterEncoding("UTF-8"); // 微信服务器POST消息时用的是UTF-8编码,在接收时也要用同样的编码,否则中文会乱码;
response.setCharacterEncoding("UTF-8"); // 在响应消息(回复消息给用户)时,也将编码方式设置为UTF-8,原理同上;
boolean isGet = request.getMethod().toLowerCase().equals("get");
PrintWriter out = response.getWriter();
try {
if (isGet) {
String signature = request.getParameter("signature");
String timestamp = request.getParameter("timestamp");
String nonce = request.getParameter("nonce");
String echostr = request.getParameter("echostr");
if (CheckUtils.checkSignature(signature, timestamp, nonce)) {
System.out.println("验证成功!");
out.write(echostr);
} else {
System.out.println("验证失败!");
}
} else {
String respMessage = "异常消息!";
respMessage = wechatPostService.weixinPost(request);
out.write(respMessage);
System.out.println(respMessage);
}
} catch (Exception e) {
System.out.println("链接失败!");
}finally {
out.close();
}
}
chickUtil:
/**
* 开发者模式-开发者自己填写的 token (令牌)
*/
private static final String token = "lm03";
/**
* 功能:验证消息的确来自微信服务器
*/
public static boolean checkSignature(String signature,String timestamp,String nonce) {
String[] arr = new String[] {token,timestamp,nonce};
//排序
Arrays.sort(arr);
//生成,拼接字符串
StringBuffer content = new StringBuffer();
for(int i=0; i<arr.length; i++) {
content.append(arr[i]);
}
//sha1加密
String shaString = getSha1(content.toString());
return shaString.equals(signature);
}
/**
* sha1加密
* @param str
* @return
*/
public static String getSha1(String str){
if (null == str || 0 == str.length()){
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("UTF-8"));
byte[] md = mdTemp.digest();
int j = md.length;
char[] buf = new char[j * 2];
int k = 0;
for (int i = 0; i < j; i++) {
byte byte0 = md[i];
buf[k++] = hexDigits[byte0 >>> 4 & 0xf];
buf[k++] = hexDigits[byte0 & 0xf];
}
return new String(buf);
} catch (Exception e) {
return null;
}
}
WechatPostService:
@SuppressWarnings("null")
public String weixinPost(HttpServletRequest request) {
String reqMessage = null;
try {
Map<String, String> map = MessageUtils.xmlToMap(request);
String fromUserName = map.get("FromUserName");
String toUserName = map.get("ToUserName");
String msgType = map.get("MsgType");
String content = map.get("Content");
if (MessageUtils.MESSAGE_TEXT.equals(msgType)) {
if ("1".equals(content)) {
System.out.println(fromUserName);
reqMessage = MessageUtils.initText(toUserName, fromUserName, MessageUtils.firstMenu());
} else if ("2".equals(content)) {
reqMessage = MessageUtils.initText(toUserName, fromUserName, MessageUtils.secondMenu());
} else {
reqMessage = MessageUtils.initText(toUserName, fromUserName, MessageUtils.menuText());
}
} else {
String eventType = map.get("Event");
if (MessageUtils.MESSAGE_SUBSCRIBE.equals(eventType)) {
// 关注时创建菜单
AccessToken accessToken = TokenUtils.getAccessToken();
/*System.out.println("票据:" + accessToken.getToken());
System.out.println("时效:" + accessToken.getExpiresIn());*/
String menu = JSONObject.fromObject(TokenUtils.InitMenu()).toString();
System.out.println(menu);
int result = TokenUtils.CreateMenu(accessToken.getToken(), menu);
if (result == 0) {
System.out.println("创建菜单成功");
} else {
System.out.println("错误码:" + result);
}
reqMessage = MessageUtils.initText(toUserName, fromUserName, MessageUtils.menuText());
}
else if (MessageUtils.MESSAGE_CLICK.equals(eventType)) {
reqMessage = MessageUtils.initText(toUserName, fromUserName, MessageUtils.menuText());
} else if (MessageUtils.MESSAGE_VIEW.equals(eventType)) {
String url = map.get("EventKey");
reqMessage = MessageUtils.initText(toUserName, fromUserName, url);
} else if (MessageUtils.MESSAGE_UNSUBSCRIBE.equals(eventType)) {
reqMessage = MessageUtils.initText(toUserName, fromUserName, "取消关注");
} else {
String key = map.get("EventKey");
reqMessage = MessageUtils.initText(toUserName, fromUserName, key);
}
}
} catch (Exception e) {
e.printStackTrace();
}
return reqMessage;
}
- 消息回复:
微信公众号的消息都是以XML的格式发送的,不管是客户发送给微信公众号还是微信公众号发给客户,都是如此,除文本消息外,其他的都要获取相应的数据进行封装。
1. MessageUtils:
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_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 String initText(String toUserName,String fromUserName,String content){
TextMessage text = new TextMessage();
text.setFromUserName(toUserName);
text.setToUserName(fromUserName);
text.setMsgType(MessageUtils.MESSAGE_TEXT);
text.setCreateTime(new Date().getTime());
text.setContent(content);
return textMessageToxml(text);
}• /**
• 主菜单
/
public static String menuText(){
StringBuffer sb = new StringBuffer();
sb.append("欢迎您的关注,请按照菜单提示进行操作:\n\n");
sb.append("1、家学通介绍\n");
sb.append("2、学校风采\n\n");
sb.append("回复任意调出此菜单。");
return sb.toString();
}
/• 关键字1
/
public static String firstMenu(){
StringBuffer sb = new StringBuffer();
sb.append("此公众号主要用于家长能够及时了解学生在校学习情况");
return sb.toString();
}
/• 关键字2
/
public static String secondMenu(){
StringBuffer sb = new StringBuffer();
sb.append("学校在近期的活动,如运动会,演讲,竞赛等");
return sb.toString();
}
/• 消息请求为XML格式,将XML转为map集合
*/
public static Map<String, String> xmlToMap(HttpServletRequest request) throws IOException, DocumentException{
Map<String, String> map = new HashMap<String ,String>();
SAXReader Reader = new SAXReader();
InputStream inputStream = request.getInputStream();
Document document = Reader.read(inputStream);
Element root = document.getRootElement();
List list = root.elements();
for (Element element : list) {
map.put(element.getName(), element.getText());
}
inputStream.close();
return map;• }
/*
• 响应为XML格式,将响应转为XML格式
*/
public static String textMessageToxml(TextMessage textmessage) {
XStream stream = new XStream();
stream.alias("xml", textmessage.getClass());
return stream.toXML(textmessage);
}
- 菜单创建:
我这里就不说菜单有什么类型了,对于一个web应用来说,一个view类型的菜单就是一个web项目的入口。在入口中又很多事情可以做,比如网页开发权限的获取,用户信息的获取。还有就是菜单创建的位置,当时我学的时候为这个东西苦恼了很久,最后觉得在用户关注时进行菜单创建比较好,当然这里只是提供我的建议而已,还有很多其他途径。
代码中替换的都是微信提供的接口
2. /**
• 发起GET请求
*/
public static JSONObject doGetStr(String url) {
HttpClientBuilder builder = HttpClientBuilder.create();
HttpGet httpGet = new HttpGet(url);
JSONObject object = null;
try {
HttpResponse response = builder.build().execute(httpGet);
HttpEntity entity = response.getEntity();
if (null != entity) {
String result = EntityUtils.toString(entity, CHARSET_FORMAT);
object = JSONObject.fromObject(result);
}
} catch (ClientProtocolException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return object;
}• /*
• 发起post请求
*/
public static JSONObject doPostStr(String url, String outstr) {
HttpClientBuilder builder = HttpClientBuilder.create();
HttpPost httpPost = new HttpPost(url);
JSONObject object = null;
httpPost.setEntity(new StringEntity(outstr, CHARSET_FORMAT));
try {
HttpResponse response = builder.build().execute(httpPost);
HttpEntity entity = response.getEntity();
if (null != entity) {
String result = EntityUtils.toString(entity, CHARSET_FORMAT);
object = JSONObject.fromObject(result);
}
} catch (IOException e) {e.printStackTrace();}
return object;
}• /*
• 获取 access_token
*/
public static AccessToken getAccessToken() {
AccessToken token = new AccessToken();
String url = ACCESS_TOKEN_URL.replace("APPID", APPID).replace("APPSECRET", APPSECRET);
JSONObject json = doGetStr(url);if (null != json && json.containsKey("access_token")) {
token.setToken(json.getString("access_token"));
token.setExpiresIn(json.getInt("expires_in"));
}
return token;
}• /*
• 初始化菜单
*/
public static Menu InitMenu() throws UnsupportedEncodingException {
Menu menu = new Menu();ViewButton button1 = new ViewButton();
button1.setName("家学通");
button1.setType("view");
button1.setUrl(getCodeUrl());ClickButton button2 = new ClickButton();
button2.setName("更多");
button2.setType("click");
button2.setKey("11");ClickButton button31 = new ClickButton();
button31.setName("扫码事件");
button31.setType("scancode_push");
button31.setKey("31");ClickButton button32 = new ClickButton();
button32.setName("地理位置");
button32.setType("location_select");
button32.setKey("32");Button button3 = new Button();
button3.setName("菜单");
button3.setSub_button(new Button[] { button31, button32 });menu.setButton(new Button[] { button1, button2, button3 });
return menu;
}• /*
• 创建菜单
*/
public static int CreateMenu(String token, String menu) {
int result = 0;
String url = CREATE_MENU_URL.replace("ACCESS_TOKEN", token);
JSONObject jsonObject = TokenUtils.doPostStr(url, menu);
if (jsonObject != null) {
result = jsonObject.getInt("errcode");
}
return result;
}
- 网页开发:
开始我不得不说一句,微信的网页开发真的很坑,我学的时候真是痛不欲生。
首先就是微信网页开发的配置,配置的是域名,前面不带http或者HTTPS
其次就是微信客户信息的获取,首先获取code,由code获取acces_token,再由access_token获取用户信息,这里面有很多坑,第一个:code必须由请求重定向获取,至于为什么请求转发不行,我也不知道,你可以问下马化腾;第二:code的时效性,五分钟内你获取的只能用一次,有木有很坑;第三,这是最坑的一点,明明你只发送了一次请求,但是他却会发送多次,这就造成了我说的第二个问题,重复使用code造成获取不到用户信息,其实第一次是获取到的,但是被后面的覆盖了,最好的办法是只获取一次就存入session,后面想用的时候全部在session中获得,而且,如果你没有设置session时效,除非你退出web应用,否则session一直有效,至于微信发送多次请求的问题,我觉得除了我用映射工具的原因,一定还有其他因素,希望知道的同学可以告诉我。 - 最后,有关其他功能比如群发消息,支付接口,等等,虽然我没做过,但是我觉得无非是调用接口,另外,希望大家多看微信的开发文档,虽然微信的开发文档真的很烂。