写这篇博客时犹豫了好久,因为步骤太多了,上班了也没时间,但是我依然记得当时实现公众号自动回复时的场景,找个案例好
难,也没有一个完整的案例,想了想还是写出来吧,希望能让实现这功能的人少走弯路。
微信公众号平台也有自定义回复消息,比如我在公众号里发送关注你,我们在微信公众号平台设置关键字关注你(就是
有人发送这个关键字就要回复什么内容)设置成回复:**你好,java!**适用于这种固定信息,如果我发送 获取个人信息、我的积
分这种内容就需要动态的数据了,所以要使用我们自己的接口往数据库中进行查询信息。
这个项目实现了简单的回复文字消息,没有 图片、音频等类型的发送、推荐看微信API文档实现,我这也有实现的案例需要的可加我QQ 930496909
首先:
先整理一下大致流程
1.编写java代码,要按照微信公众号提供的API文档来做。
2.下载ngrok工具,假设编写的Java都不会部署到网上(本地运行项目),那我们要想微信能访问我们的java接口我们需要一个工
具(就是将我们本地电脑变成服务器,让别人能来访问我们本地运行的项目说的比较通俗具体可百度~),如果编写的java程序
部署到服务器上就不用啦。
3.在微信公众号平台注册服务号或者订阅号[两者的区别在于服务号收钱功能多,其他区别可自行百度~]我注册的是订阅号,然
后在微信平台进行一些配置,(其实就是让关注公众号的人发送消息后能够对接我们的java接口)
4.进行测试。(我会把源码贴到码云上,可以下载源码https://gitee.com/it_qin/weixintest.git)
接下来就是步骤了。
看一下java结构目录
jar包(因为当时在学校做的还不太会用MAVEN管理,无奈啊,jar包主要就是ssm框架jar包和微信的几个包,我把截图发下,上面
web.xml
encodingFilter
org.springframework.web.filter.CharacterEncodingFilter
encoding
UTF-8
forceEncoding
true
encodingFilter
/*
log4jConfigLocation
classpath:log4j.properties
org.springframework.web.util.Log4jConfigListener
springMvc
org.springframework.web.servlet.DispatcherServlet
contextConfigLocation
classpath:springmvc-servlet.xml
1
springMvc
*.do
contextConfigLocation
classpath:applicationContext-mybatis.xml
org.springframework.web.context.ContextLoaderListener
springmvc-servlet.xml
application/json;charset=UTF-8
text/html;charset=UTF-8
application/json
WriteDateUseDateFormat
applicationContext-mybatis.xml
mybatis-config.xml
database.properties
driver=com.mysql.jdbc.Driver
url=jdbc:mysql://127.0.0.1:3306/ktvsystem?useUnicode=true&characterEncoding=utf-8
user=root
password=admin
minIdle=45
maxIdle=50
initialSize=5
maxActive=100
maxWait=100
removeAbandonedTimeout=240
removeAbandoned=true
log4j.properties
log4j.rootLogger=debug,CONSOLE,file
#log4j.rootLogger=ERROR,ROLLING_FILE
log4j.logger.cn.smbms=debug
log4j.logger.org.apache.ibatis=debug
log4j.logger.org.mybatis.spring=debug
log4j.logger.java.sql.Connection=debug
log4j.logger.java.sql.Statement=debug
log4j.logger.java.sql.PreparedStatement=debug
log4j.logger.java.sql.ResultSet=debug
######################################################################################
# Console Appender \u65e5\u5fd7\u5728\u63a7\u5236\u8f93\u51fa\u914d\u7f6e
######################################################################################
log4j.appender.CONSOLE=org.apache.log4j.ConsoleAppender
log4j.appender.Threshold=debug
log4j.appender.CONSOLE.DatePattern=yyyy-MM-dd
log4j.appender.CONSOLE.Target=System.out
log4j.appender.CONSOLE.layout=org.apache.log4j.PatternLayout
log4j.appender.CONSOLE.layout.ConversionPattern= - (%r ms) - %d{yyyy-M-d HH:mm:ss}%x[%5p](%F:%L) %m%n
######################################################################################
# Rolling File \u6587\u4ef6\u5927\u5c0f\u5230\u8fbe\u6307\u5b9a\u5c3a\u5bf8\u7684\u65f6\u5019\u4ea7\u751f\u4e00\u4e2a\u65b0\u7684\u6587\u4ef6
######################################################################################
#log4j.appender.ROLLING_FILE=org.apache.log4j.RollingFileAppender
#log4j.appender.ROLLING_FILE.Threshold=INFO
#log4j.appender.ROLLING_FILE.File=${baojia.root}/logs/log.log
#log4j.appender.ROLLING_FILE.Append=true
#log4j.appender.ROLLING_FILE.MaxFileSize=5000KB
#log4j.appender.ROLLING_FILE.MaxBackupIndex=100
#log4j.appender.ROLLING_FILE.layout=org.apache.log4j.PatternLayout
#log4j.appender.ROLLING_FILE.layout.ConversionPattern=%d{yyyy-M-d HH:mm:ss}%x[%5p](%F:%L) %m%n
######################################################################################
# DailyRolling File \u6bcf\u5929\u4ea7\u751f\u4e00\u4e2a\u65e5\u5fd7\u6587\u4ef6\uff0c\u6587\u4ef6\u540d\u683c\u5f0f:log2009-09-11
######################################################################################
log4j.appender.file=org.apache.log4j.DailyRollingFileAppender
log4j.appender.file.DatePattern=yyyy-MM-dd
log4j.appender.file.File=${AppInfoSystem.root}/logs/log.log
log4j.appender.file.Append=true
log4j.appender.file.Threshold=debug
log4j.appender.file.layout=org.apache.log4j.PatternLayout
log4j.appender.file.layout.ConversionPattern= - (%r ms) - %d{yyyy-M-d HH:mm:ss}%x[%5p](%F:%L) %m%n
#DWR \u65e5\u5fd7
#log4j.logger.org.directwebremoting = ERROR
#\u663e\u793aHibernate\u5360\u4f4d\u7b26\u7ed1\u5b9a\u503c\u53ca\u8fd4\u56de\u503c
#log4j.logger.org.hibernate.type=DEBUG,CONSOLE
#log4j.logger.org.springframework.transaction=DEBUG
#log4j.logger.org.hibernate=DEBUG
#log4j.logger.org.acegisecurity=DEBUG
#log4j.logger.org.apache.myfaces=TRACE
#log4j.logger.org.quartz=DEBUG
#log4j.logger.com.opensymphony=INFO
#log4j.logger.org.apache.struts2=DEBUG
log4j.logger.com.opensymphony.xwork2=debug
WeiXinController.java
package com.qhk.controller;
import java.util.Map;
import java.util.Random;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import com.qhk.util.CheckUtil;
import com.qhk.util.MessageFormat;
import com.qhk.util.MessageUtil;
@Controller
@RequestMapping("/weixin")
public class WeiXinController {
/**
* 功能:[微信验证 ][2018年2月9日 下午10:01:14][创建人: HongKun.Qin]
*
* @param request
* @param response
* @return
*/
@ResponseBody
@RequestMapping(value = "/message.do",method =RequestMethod.GET)
public String getMessageValidate(HttpServletRequest request, HttpServletResponse response){
String signature = request.getParameter("signature");//微信加密签名,signature结合了开发者填写的token参数和请求中的timestamp参数、nonce参数。
String timestamp = request.getParameter("timestamp");// 时间戳
String nonce = request.getParameter("nonce");// 随机数
String echostr = request.getParameter("echostr");// 随机字符串
if(CheckUtil.checkSignature(signature, timestamp, nonce)){
return echostr;
}
return "";
}
/**
* 功能:[接受消息,并返回消息 ][2018年2月9日 下午10:02:00][创建人: HongKun.Qin]
*
* @param request
* @param response
* @return
* @throws Exception
*/
@ResponseBody
@RequestMapping(value = "/message.do",method =RequestMethod.POST)
public String getMessage(HttpServletRequest request, HttpServletResponse response) throws Exception{
Map map = new MessageFormat().xmlToMap(request);
String fromUserName = map.get("FromUserName");//公众号
String toUserName = map.get("ToUserName");//粉丝号
String msgType = map.get("MsgType");//发送的消息类型[比如 文字,图片,语音。。。]
String content = map.get("Content");//发送的消息内容
String message = null;
System.out.println("fromUserName:"+fromUserName+" ToUserName:"+toUserName+" MsgType:"+msgType+" "+content);
//判断发送的类型是文本
if(MessageUtil.MESSAGE_TEXT.equals(msgType)){
//发送的内容为???时
if("0".equals(content)){
message = MessageFormat.initText(toUserName, fromUserName, MessageUtil.menuText());
}else if("1".equals(content)) {
Random random = new Random();
message = MessageFormat.initText(toUserName, fromUserName, String.format("您本次的验证码为:%s%s%s%s", random.nextInt(10),random.nextInt(10),random.nextInt(10),random.nextInt(10)));//模拟验证码
}else{
message = MessageFormat.initText(toUserName, fromUserName, "功能正在完善中,请按提示信息操作[回复'0'显示主菜单]。");
}
}else if(MessageUtil.MESSAGE_EVENT.equals(msgType)){//验证是关注/取消事件
String eventType = map.get("Event");//获取是关注还是取消
//关注
if(MessageUtil.MESSAGE_SUBSCRIBE.equals(eventType)){
message = MessageFormat.initText(toUserName, fromUserName, "欢迎关注青鸟ktv,回复[0]即可调出功能菜单");
}
}
return message;
}
}
AccessToken.java
package com.qhk.entity;
public class AccessToken {
private String token;
private int expiresIn;
public String getToken() {
return token;
}
public void setToken(String token) {
this.token = token;
}
public int getExpiresIn() {
return expiresIn;
}
public void setExpiresIn(int expiresIn) {
this.expiresIn = expiresIn;
}
}
BaseMessage.java
package com.qhk.entity;
public class BaseMessage {
private String ToUserName;
private String FromUserName;
private long 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 long getCreateTime() {
return CreateTime;
}
public void setCreateTime(long createTime) {
CreateTime = createTime;
}
public String getMsgType() {
return MsgType;
}
public void setMsgType(String msgType) {
MsgType = msgType;
}
}
TextMessage.java
package com.qhk.entity;
public class TextMessage extends BaseMessage{
private String Content;
private String MsgId;
public String getContent() {
return Content;
}
public void setContent(String content) {
Content = content;
}
public String getMsgId() {
return MsgId;
}
public void setMsgId(String msgId) {
MsgId = msgId;
}
}
CheckUtil.java(token注意,我现在是qhk后面微信公众号平台配置也要用这个,自己改了的话就填自己改的)
package com.qhk.util;
import java.io.UnsupportedEncodingException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
public class CheckUtil {
public static final String token = "qhk";//这个地方也要注意
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 temp = getSha1(content.toString());
return temp.equals(signature);
}
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 (NoSuchAlgorithmException e) {
e.printStackTrace();
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
return null;
}
}
MessageFormat.java
package com.qhk.util;
import java.io.IOException;
import java.io.InputStream;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;
import com.qhk.entity.TextMessage;
import com.thoughtworks.xstream.XStream;
/**
* 将发送的消息进行转换
* @author HongKun.Qin
*/
public class MessageFormat {
/**
* xml 转 map
*
* @param request
* @return
* @throws IOException
* @throws DocumentException
*/
public static Map xmlToMap(HttpServletRequest request)
throws IOException, DocumentException {
Map map = new HashMap();
SAXReader reader = new SAXReader();
InputStream ins = request.getInputStream();
Document doc = reader.read(ins);
Element root = doc.getRootElement();
List list = root.elements();
for (Element e : list) {
map.put(e.getName(), e.getText());
}
ins.close();
return map;
}
/**
* 将文本消息转换为xml
*
* @param textMessage
* @return
*/
public static String textMessageToXml(TextMessage textMessage) {
XStream xStream = new XStream();
xStream.alias("xml", textMessage.getClass());
return xStream.toXML(textMessage);
}
public static String initText(String toUserName, String fromUserName,
String content) {
TextMessage text = new TextMessage();
text.setFromUserName(toUserName);
text.setToUserName(fromUserName);
text.setMsgType(MessageUtil.MESSAGE_TEXT);
text.setCreateTime(new Date().getTime());
text.setContent(content);
return textMessageToXml(text);
}
}
MessageUtil.java
package com.qhk.util;
public class MessageUtil {
/**
* 类型
*/
public static final String MESSAGE_TEXT = "text";//文本
public static final String MESSAGE_NEWS = "news";
public static final String MESSAGE_IMAGE = "image";
public static final String MESSAGE_MUSIC = "music";
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 final String MESSAGE_SCANCODE = "scancode_push";
/**
* 功能:[显示的主菜单 ][2018年2月9日 下午9:37:56][创建人: HongKun.Qin]
*
* @return
*/
public static String menuText() {
StringBuffer sb = new StringBuffer();
sb.append("欢迎您关注青鸟KTV,请按照菜单提示进行操作:\n\n");
sb.append("[1].显示短信验证码\n");
sb.append("[2].显示个人信息\n");
sb.append("[3].关于青鸟KTV\n");
sb.append("[4].关于注册成为会员\n\n");
sb.append("回复 \"[0]\" 调出此菜单。");
return sb.toString();
}
}
WeixinUtil.java(APPID、APPSECRET)别忘改成自己的,不然项目也没法运行
package com.qhk.util;
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;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.util.EntityUtils;
import com.qhk.entity.AccessToken;
import net.sf.json.JSONObject;
public class WeixinUtil {
private static final String APPID="";//在基础配置中可查看自己APPID
private static final String APPSECRET="";//在基础配置中可查看自己APPSECRET
private static final String ACCESS_TOKEN_URL="https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=APPID&secret=APPSECRET";
private static final String UPLOAD_URL = "https://api.weixin.qq.com/cgi-bin/media/upload?access_token=ACCESS_TOKEN&type=TYPE";
public static JSONObject doGetStr(String url){
DefaultHttpClient httpClient = new DefaultHttpClient();
HttpGet httpGet=new HttpGet(url);
JSONObject jsonObject = null;
try {
HttpResponse response=httpClient.execute(httpGet);
HttpEntity entity = response.getEntity();
if(entity!=null){
String result = EntityUtils.toString(entity,"UTF-8");
jsonObject = JSONObject.fromObject(result);
}
} catch (ClientProtocolException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
System.out.println(jsonObject);
return jsonObject;
}
/**
*
* @Description: TODO 获取AccessToken
* @param @return
* @return AccessToken
* @throws
* @author qinhongkun
* @date 2017-12-18
*/
public static AccessToken getAccessToken(){
AccessToken token = new AccessToken();
String url = ACCESS_TOKEN_URL.replace("APPID", APPID).replace("APPSECRET", APPSECRET);
JSONObject jsonObject = doGetStr(url);
if(jsonObject!=null){
token.setToken(jsonObject.getString("access_token"));
token.setExpiresIn(jsonObject.getInt("expires_in"));
}
return token;
}
/*
* 文件上传
*/
public static String upload(String filePath, String accessToken,String type) throws IOException, NoSuchAlgorithmException, NoSuchProviderException, KeyManagementException {
System.out.println("filePath:"+filePath);
File file = new File(filePath);
if (!file.exists() || !file.isFile()) {
throw new IOException("文件不存在");
}
String url = UPLOAD_URL.replace("ACCESS_TOKEN", accessToken).replace("TYPE",type);
URL urlObj = new URL(url);
//连接
HttpURLConnection con = (HttpURLConnection) urlObj.openConnection();
con.setRequestMethod("POST");
con.setDoInput(true);
con.setDoOutput(true);
con.setUseCaches(false);
//设置请求头信息
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=\"file\";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;
String result = 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) {
e.printStackTrace();
} finally {
if (reader != null) {
reader.close();
}
}
JSONObject jsonObj = JSONObject.fromObject(result);
System.out.println(jsonObj);
String typeName = "media_id";
if(!"image".equals(type)){
typeName = type + "_media_id";
}
String mediaId = jsonObj.getString(typeName);
return mediaId;
}
}
2.大家现在就是关心ngrok下载地址:https://ngrok.com/download
还有人会问:哎哟我靠,什么是ngrok?
其实不用纠结这是个啥,如果你作为一个Java开发人员是第一次接触微信公众号后台开发,我建议你千万别纠结这到底是个
啥,简单明了的告诉你:微信公众号开发文档中 要求 有服务器 (自己的域名、也可以理解为自己的空间 当然 不是QQ空
间),然后呢,那你就想百度一下了,怎么拥有自己的域名或者说空间,有人推荐你使用百度BEA(好像是,不知道名字有
没有记错),然后微信公众号的技术文档里面推荐你用腾讯的什么什么,总之,收费。诶,对 就是收费。
所以呢,这个ngrok就是免费的。而且运行极其简单,对,不费劲哈。
这个下载之后,暂时放那,别动。
3.注册订阅号 去微信公众号平台注册一个个人的订阅号就行,登录进去,三个箭头分别点击一下将最下面红箭头标出的地方进
配置你的服务器地址 (这是问题1)
配置你的token (这是问题2)
配置你的EncodingAESkey (这不是问题!!)
问题1:
然后你肯定进入了死胡同,想说URL怎么填呢?
你还记得你的ngrok吗?
首先我们先在本地启动项目,(注意代码中需要修改的地方上面我已经写出来,填写自己的APPID和密码)必须是tomcat的8080
端口
这时候就要用到ngrok了
打开你的ngrok文件夹,在包涵ngrok.exe文件的文件夹中运行cmd,然后输入指令:
ngrok -config ngrok.cfg -subdomain qinhongkun 8080
qinhongkun可以随便改,自己填写服务器地址别写错了就行。
如果没有什么问题的话下面这张图就是启动成功的(别关掉这dos窗口)
转回来微信公众号,你的URL就填写ngrok运行后的生成的网址(我已经用红色标记出来了用哪一个都行有两个)/项目名称/控
制器名 比如我的就是 https://qinhongkun.tunnel.echomod.cn/weixintest/weixin/message.do
至于为什么我的项目名是weixintest而不是weixin-demo 那是我之前在eclipse中改了项目名,而tomcat中没有修改过来,
weixin/message.do这控制器方法上面有。
问题2:
token 就要填我们在项目中写的那个,上面我也重点标记了,如果项目中更改了的话,这别忘了修改!
问题3:直接点击随机生成一个就ok了
如果没有问题现在已经能保存了。
我将项目上传到码云,https://gitee.com/it_qin/weixintest.git。
注:因为当时用作项目中有很多功能,所以这是个完整的ssm框架,只不过我把那些都删了,只留下一个简单的微信Demo.
现在去你的公众号测试一下就行了,有什么问题或者建议欢迎评论,确保一天之内回复,不算星期天哦。