微信公众号
注册微信公众号
微信公众号平台
微信公众号测试
新建微信公众号后台项目
注入依赖
<!-- lombok:get set方法的注解 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- 输出日志 -->
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
<!-- mybati-plus-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>LATEST</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>LATEST</version>
</dependency>
<!-- 微信公众号回传的xml转换-->
<dependency>
<groupId>com.thoughtworks.xstream</groupId>
<artifactId>xstream</artifactId>
<version>1.3.1</version>
<type>jar</type>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>dom4j</groupId>
<artifactId>dom4j</artifactId>
<version>1.6.1</version>
<exclusions>
<exclusion>
<groupId>xml-apis</groupId>
<artifactId>xml-apis</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- json格式的转换-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.54</version>
</dependency>
<!-- mapper的xml文件的配置 -->
<resources>
<resource>
<directory>src/main/resources</directory>
<filtering>true</filtering>
</resource>
<resource>
<directory>src/main/java</directory>
<includes>
<include>**/*.xml</include>
</includes>
</resource>
</resources>
验证服务器
- 控制器
@Slf4j
@RestController
@RequestMapping("/")
public class wxpublicController {
@GetMapping("/")
public String checkSign(HttpServletRequest request, HttpServletResponse response) {
try {
String signature = request.getParameter("signature");
String timestamp = request.getParameter("timestamp");
String nonce = request.getParameter("nonce");
String echostr = request.getParameter("echostr");
log.info("本身" + signature);
if (SignUtil.checkSignature(signature, timestamp, nonce)) {
return echostr;
}
} catch (Exception e) {
log.error("验证公众号token失败", e);
}
return null;
}
- 工具类
public class SignUtil {
private static String token = "asdasd";// 与微信公众号上的token一致,是服务器令牌(token),这里写什么。服务器就填什么
/**
* 校验签名
*
* @param signature 签名
* @param timestamp 时间戳
* @param nonce 随机数
* @return 布尔值
*/
public static boolean checkSignature(String signature, String timestamp, String nonce) {
String checktext = null;
if (null != signature) {
//对ToKen,timestamp,nonce 按字典排序
String[] paramArr = new String[]{token, timestamp, nonce};
Arrays.sort(paramArr);
//将排序后的结果拼成一个字符串
String content = paramArr[0].concat(paramArr[1]).concat(paramArr[2]);
try {
MessageDigest md = MessageDigest.getInstance("SHA-1");
//对接后的字符串进行sha1加密
byte[] digest = md.digest(content.toString().getBytes());
checktext = byteToStr(digest);
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
}
//将加密后的字符串与signature进行对比
return checktext != null ? checktext.equals(signature.toUpperCase()) : false;
}
/**
* 将字节数组转化我16进制字符串
*
* @param byteArrays 字符数组
* @return 字符串
*/
private static String byteToStr(byte[] byteArrays) {
String str = "";
for (int i = 0; i < byteArrays.length; i++) {
str += byteToHexStr(byteArrays[i]);
}
return str;
}
/**
* 将字节转化为十六进制字符串
*
* @param myByte 字节
* @return 字符串
*/
private static String byteToHexStr(byte myByte) {
char[] Digit = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'};
char[] tampArr = new char[2];
tampArr[0] = Digit[(myByte >>> 4) & 0X0F];
tampArr[1] = Digit[myByte & 0X0F];
String str = new String(tampArr);
return str;
}
xml转换工具
/*
* 组装文本消息
*/
public static Map<String, String> xmlToMap(HttpServletRequest request ) throws DocumentException, IOException
{
Map<String,String> map = new HashMap<String, String>();
SAXReader reader = new SAXReader();
InputStream ins = request.getInputStream();
Document doc = reader.read(ins);
Element root = doc.getRootElement();
List<Element> list = root.elements();
for (Element e : list) {
map.put(e.getName(), e.getText());
}
ins.close();
return map;
}
消息接口
- 控制器
@PostMapping("/")
public void doPost(HttpServletRequest request, HttpServletResponse response)throws IOException, ServletException {
request.setCharacterEncoding("utf-8");
response.setCharacterEncoding("utf-8");
log.info("公众号-->事件推送");
PrintWriter out = response.getWriter();
try {//reMap存储微信服务器回传的信息经过工具转换成map格式
Map<String,String> reqMap = MessageUtil.xmlToMap(request);
//reqMap传入getResponse工具判断信息类型
String xml = messageUtil.getResponse(reqMap);
out.print(xml);
out.flush();
out.close();
} catch (Exception e) {
e.printStackTrace();
out.close();
}
}
- 工具类
public String getResponse(Map<String, String> reqMap)throws DocumentException,IOException{
BaseMessage msg=null;
String openId=reqMap.get("FromUserName");
String msgType=reqMap.get("MsgType");
String eventType=reqMap.get("Event");
switch (msgType){
case "text"://文本消息
msg=dealTextMessage(reqMap);
break;
case "image"://图片消息
break;
case "voice"://语音消息
break;
case "video"://视频消息
break;
case "music"://音乐消息
break;
case "news"://图文消息
break;
case "line"://链接消息
break;
case "location"://地理位置消息
break;
case "shortvideo"://短视频消息
break;
case "event"://事件消息
switch (eventType){
case "subscribe"://关注事件
UnionIdInfo unionIdInfo = unionIdUtil.getUnionId(openId);
Wrapper wrapper = new QueryWrapper<FollowPeople>()
.select()
.eq("open_id",unionIdInfo.getOpenid())
.eq("union_id",unionIdInfo.getUnionid())
.eq("is_follow",1);
FollowPeople list = followPeopleMapper.selectOne(wrapper);
if(list!=null){
//TODO 发送小程序连接
msg=dealTextMessage(reqMap);
break;
}else{
Wrapper wrapper1 = new QueryWrapper<FollowPeople>()
.select()
.eq("open_id",unionIdInfo.getOpenid())
.eq("union_id",unionIdInfo.getUnionid())
.eq("is_follow",0);
FollowPeople list1 = followPeopleMapper.selectOne(wrapper1);
if (list1!=null){
FollowPeople followPeople = new FollowPeople();
followPeople.setOpenId(unionIdInfo.getOpenid());
followPeople.setUnionId(unionIdInfo.getUnionid());
followPeople.setIsFollow("1");
followPeopleMapper.update(followPeople,wrapper1);
msg=dealTextMessage(reqMap);
break;
}else {
FollowPeople followPeople1 = new FollowPeople();
followPeople1.setOpenId(unionIdInfo.getOpenid());
followPeople1.setUnionId(unionIdInfo.getUnionid());
followPeople1.setIsFollow("1");
followPeopleMapper.insert(followPeople1);
//TODO 发送小程序连接
msg=dealTextMessage(reqMap);
break;
}
}
case "unsubscribe"://取关事件
Wrapper wrapperUpdate = new QueryWrapper<FollowPeople>()
.select()
.eq("open_id",openId)
.eq("is_follow",1);
FollowPeople followPeople = new FollowPeople();
followPeople.setIsFollow("0");
followPeopleMapper.update(followPeople,wrapperUpdate);
break;
case "SCAN"://用户扫码已关注事件
msg=dealTextMessage(reqMap);
break;
case "LOCATION"://上报地理位置事件
break;
case "CLICK"://自定义菜单事件
break;
case "VIEW"://点击菜单跳转链接时的事件
break;
default :
break;
}
break;
default :
break;
}
//把消息对象处理为xml数据包
if(msg!=null){
return benToxml(msg);
}
return "null";
}
//处理文本消息
private static BaseMessage dealTextMessage(Map<String, String> reqMap) {
TextMessage tm=new TextMessage(reqMap,"啥玩意?");
return tm;
}
//转换xml格式
private static String benToxml(BaseMessage msg) {
XStream xStream = new XStream();
xStream.processAnnotations(TextMessage.class);
xStream.processAnnotations(VideoMessage.class);
xStream.processAnnotations(VoiceMessage.class);
xStream.processAnnotations(MusicMessage.class);
xStream.processAnnotations(NewsMessage.class);
xStream.processAnnotations(ImageMessage.class);
String xml=xStream.toXML(msg);
return xml;
}
- 实体类
- 底层实体类
@Data
public class BaseMessage {
private String ToUserName;
private String FromUserName;
private Long CreateTime;
private String MsgType;
private String Content;
}
- 文本消息实体类
@Data
public class TextMessage extends BaseMessage{
private String Content;
private String MsgId;
}
- 图片消息实体类
@Setter
@Getter
@XStreamAlias("xml")
public class ImageMessage extends BaseMessage {
private String mediaId;
public ImageMessage(Map<String, String> reqMap,String mediaId) {
super(reqMap);
this.setMsgType("image");
this.mediaId=mediaId;
}
}
- 语音消息实体类
@Getter
@Setter
@XStreamAlias("xml")
public class VoiceMessage extends BaseMessage{
private String mediaId;
public VoiceMessage(Map<String, String> reqMap,String mediaId) {
super(reqMap);
this.setMsgType("voice");
this.mediaId=mediaId;
}
}
- 音乐消息实体类
@Getter
@Setter
@XStreamAlias("xml")
public class MusicMessage extends BaseMessage{
private Music Music;
public MusicMessage(Map<String, String> reqMap,Music music) {
super(reqMap);
this.setMsgType("music");
this.Music=music;
}
}
@Setter
@Getter
public class Music {
private String musicURL;
private String hQMusicURL;
private String title;
private String description;
private String thumbMediaId;
public Music(String musicURL, String hQMusicURL, String title, String description, String thumbMediaId) {
super();
this.musicURL = musicURL;
this.hQMusicURL = hQMusicURL;
this.title = title;
this.description = description;
this.thumbMediaId = thumbMediaId;
}
}
- 新闻消息实体类
@Getter
@Setter
@XStreamAlias("xml")
public class NewsMessage extends BaseMessage {
private List<Article> Article=new ArrayList<>();
private String ArticleContent;
public NewsMessage(Map<String, String> reqMap,List<Article> article,String ArticleContent) {
super(reqMap);
this.setMsgType("news");
this.Article=article;
this.ArticleContent=ArticleContent;
}
}
@Setter
@Getter
public class Article {
private String title;
private String description;
private String picURL;
private String URL;
public Article(String title, String description, String picURL, String URL) {
this.title = title;
this.description = description;
this.picURL = picURL;
this.URL = URL;
}
}
获取accessToken
//yml配置文件
@ConfigurationProperties("app.config")
@Slf4j
public class AccessTokenUtil {
@Autowired
private Environment env;
public Map<String,Object> getAccessToken() throws IOException {
try{
//获取yml的appId和AppSecret
String appId = env.getProperty("app.config.appId");
String appSecret = env.getProperty("app.config.appSecret");
///获取accessToken
String accessTokenUrl = "https://api.weixin.qq.com/cgi-bin/token?" +
"grant_type=client_credential"+
"&appid="+appId+
"&secret="+appSecret;//链接
URL url = new URL(accessTokenUrl);
URLConnection yc = url.openConnection();
BufferedReader in = new BufferedReader(new InputStreamReader(yc.getInputStream(), "utf-8"));
String inputLine = null;
StringBuffer json1 = new StringBuffer();
while ((inputLine = in.readLine()) != null) {
json1.append(inputLine);
}
JSONObject json = JSONObject.parseObject(json1.toString());
Map<String,Object> map = new HashMap<>();
map.put("access_token",json.getString("access_token"));
map.put("expires_in",json.getString("expires_in"));
map.put("getTokenTime",System.currentTimeMillis());
return map;
} catch (Exception e) {
e.printStackTrace();
}
Map<String,Object> map = new HashMap<>();
map.put("result","获取token失败");
return map;
}
}
获取unionID
@Autowired
private AccessTokenUtil accessTokenUtil;
public UnionIdInfo getUnionId(String fromUserName) throws DocumentException, IOException
{
//获取unionid
Map<String,Object> map = accessTokenUtil.getAccessToken();
if(map.get("access_token")==null){
map = accessTokenUtil.getAccessToken();
}else {
long totalSeconds = (System.currentTimeMillis() - Long.valueOf(String.valueOf(map.get("getTokenTime"))).longValue()) / 1000;
long expiresIn = Long.valueOf(String.valueOf(map.get("expires_in"))).longValue();
//判断access_token是否失效
if(totalSeconds>expiresIn){
map = accessTokenUtil.getAccessToken();
}else {
String requestUrl = "https://api.weixin.qq.com/cgi-bin/user/info?"+
"access_token="+map.get("access_token")+
"&openid="+fromUserName+
"&lang=zh_CN";
URL url = new URL(requestUrl);
URLConnection yc = url.openConnection();
BufferedReader in = new BufferedReader(new InputStreamReader(yc.getInputStream(), "utf-8"));
String inputLine = null;
StringBuffer json = new StringBuffer();
while ((inputLine = in.readLine()) != null) {
json.append(inputLine);
}
JSONObject json1 = JSONObject.parseObject(json.toString());
UnionIdInfo unionIdInfo = new UnionIdInfo();
unionIdInfo.setUnionid(json1.getString("unionid"));
unionIdInfo.setSubscribe(json1.getString("subscribe"));
unionIdInfo.setOpenid(json1.getString("openid"));
unionIdInfo.setNickname(json1.getString("nickname"));
switch (json1.getString("sex")){
case "1":unionIdInfo.setSex("男");break;
case "2":unionIdInfo.setSex("女");break;
default:unionIdInfo.setSex("未知");break;
}
unionIdInfo.setCity(json1.getString("city"));
unionIdInfo.setCountry(json1.getString("country"));
unionIdInfo.setProvince(json1.getString("province"));
unionIdInfo.setLanguage(json1.getString("language"));
unionIdInfo.setSubscribe_time(json1.getString("subscribe_time"));
unionIdInfo.setSubscribe_scene(json1.getString("subscribe_scene"));
return unionIdInfo;
}
}
return null;
}
参数 | 说明 |
subscribe | 用户是否订阅该公众号标识,值为0时,代表此用户没有关注该公众号,拉取不到其余信息。 |
openid | 用户的标识,对当前公众号唯一 |
nickname | 用户的昵称 |
sex | 用户的性别,值为1时是男性,值为2时是女性,值为0时是未知 |
city | 用户所在城市 |
country | 用户所在国家 |
province | 用户所在省份 |
language | 用户的语言,简体中文为zh_CN |
headimgurl | 用户头像,最后一个数值代表正方形头像大小(有0、46、64、96、132数值可选,0代表640*640正方形头像),用户没有头像时该项为空。若用户更换头像,原有头像URL将失效。 |
subscribe_time | 用户关注时间,为时间戳。如果用户曾多次关注,则取最后关注时间 |
unionid | **只有在用户将公众号绑定到微信开放平台帐号后,才会出现该字段。 ** |
remark | 公众号运营者对粉丝的备注,公众号运营者可在微信公众平台用户管理界面对粉丝添加备注 |
groupid | 用户所在的分组ID(兼容旧的用户分组接口) |
tagid_list | 用户被打上的标签ID列表 |
subscribe_scene | 返回用户关注的渠道来源,ADD_SCENE_SEARCH 公众号搜索,ADD_SCENE_ACCOUNT_MIGRATION 公众号迁移,ADD_SCENE_PROFILE_CARD 名片分享,ADD_SCENE_QR_CODE 扫描二维码,ADD_SCENE_PROFILE_LINK 图文页内名称点击,ADD_SCENE_PROFILE_ITEM 图文页右上角菜单,ADD_SCENE_PAID 支付后关注,ADD_SCENE_WECHAT_ADVERTISEMENT 微信广告,ADD_SCENE_OTHERS 其他 |
qr_scene | 二维码扫码场景(开发者自定义) |
qr_scene_str | 二维码扫码场景描述(开发者自定义) |
内网穿透
下载ngrok
打开exe文件
输入命令 ngrok http localhost:8080
测试URL链接
**token随意填写,注意与后端的验证token一致 **
公众号配置服务器
**配置成功后,在ip白名单里面添加服务器ip **