1.流程概述

1.1 申请公众号

1.2 创建带参数的公众号二维码,参数值为scen_id的值

1.3 微信基础配置接口编写,get方式的接口为微信测试接口,必须能正常访问,post方式的接口为扫码回调接口,从请求中获取微信返回的xml包数据,其中eventKey为二维码的scen_id参数值,event为事件类型,subscribe为关注事件,scan为扫码事件,unsubscribe为取消关注事件,根据自己的具体业务需求来处理即可,两个接口得请求路径必须相同

1.4 公众号接口基础配置

2.申请测试公众号

地址:https://mp.weixin.qq.com/debug/cgi-bin/sandbox?t=sandbox/login

访问该地址,直接微信扫码登录即可,扫码成功后进入如下界面:

微信公众号关注自定绑定java 关注公众号接口_微信公众平台

3.获取带参数的公众号二维码

3.1 通过appID和appsecret获取accessToken,测试公众号每天只能获取2000次

以get方式向微信发送请求即可(https请求方式: GET https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=APPID&secret=APPSECRET)

public static String getAccessToken(String appSecret,String appId) {
        String accessTokenUrl = "https://api.weixin.qq.com/cgi-bin/token?grant_type=" + grant_type
                + "&appid=" + appId + "&secret=" + appSecret;

        // 获取accessToken
        String accessTokenBody = cn.hutool.http.HttpUtil.get(accessTokenUrl);

        log.info("获取accessToken{}",accessTokenBody);

        com.alibaba.fastjson.JSONObject jsonObject = com.alibaba.fastjson.JSONObject.parseObject(accessTokenBody);
        String accessToken = null;
        try {
            accessToken = (String) jsonObject.get("access_token");
            log.info("accessToken:{}",accessToken);
        } catch (Exception e) {
            log.error("获取accessToken异常");
        }
        return accessToken;
    }
3.2 通过上一步获取的accessToken创建二维码的ticket

二维码有临时二维码和永久二维码两种,创建ticket的路径都是相同的,只是参数不同而已。

临时二维码请求说明

http请求方式: POST URL:
https://api.weixin.qq.com/cgi-bin/qrcode/create?access_token=TOKEN POST数据格式:json POST数据例子:{“expire_seconds”: 604800, “action_name”:
“QR_SCENE”, “action_info”: {“scene”: {“scene_id”: 123}}} 或者也可以使用以下
POST 数据创建字符串形式的二维码参数:{“expire_seconds”: 604800, “action_name”:
“QR_STR_SCENE”, “action_info”: {“scene”: {“scene_str”: “test”}}

}

永久二维码请求说明

http请求方式: POST URL:
https://api.weixin.qq.com/cgi-bin/qrcode/create?access_token=TOKEN POST数据格式:json POST数据例子:{“action_name”: “QR_LIMIT_SCENE”,
“action_info”: {“scene”: {“scene_id”: 123}}} 或者也可以使用以下 POST
数据创建字符串形式的二维码参数: {“action_name”: “QR_LIMIT_STR_SCENE”, “action_info”:
{“scene”: {“scene_str”: “test”}}}

这里要注意的是:场景scen里的属性scen_id的值,如果是以0开头,会将0忽略,文档里也没有具体说明,测试的结果是这样的,而且值的长度不能过长,否则扫码只会获取到的eventKey会获取不到,或则只有一部分。

String ticketUrl = "https://api.weixin.qq.com/cgi-bin/qrcode/create?access_token=" + accessToken;

        JSONObject ticketParam = JSONUtil.createObj();
        JSONObject scene = JSONUtil.createObj();
        String key = "1" + GenerationSequenceUtil.getRandNum(8);
        scene.put("scene_id", key);

        log.info("scene:{}",scene);

        JSONObject actionInfo = JSONUtil.createObj();
        actionInfo.put("scene",scene);
        ticketParam.put("action_info", actionInfo);
        ticketParam.put("action_name", "QR_SCENE");
        ticketParam.put("expire_seconds", expireTime);

        log.info("ticketParam:{}",ticketParam);

        // ticketParam转json字符串
        String ticketParamToString = com.alibaba.fastjson.JSONObject.toJSONString(ticketParam);

        // 获取ticket
        String ticketBody = cn.hutool.http.HttpUtil.post(ticketUrl,ticketParamToString);

        log.info("获取ticketBody{}",ticketBody);

        com.alibaba.fastjson.JSONObject ticketContent = com.alibaba.fastjson.JSONObject.parseObject(ticketBody);

        String ticket = (String) ticketContent.get("ticket");
3.3 拼接url,返回url到前端获取二维码

上一步拿到的ticket直接拼接为该url参数即可。https://mp.weixin.qq.com/cgi-bin/showqrcode?ticket=TICKE

3.4获取二维码的完整代码

工具类:

package com.school.util;

import cn.hutool.http.HttpUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.school.base.Constant;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;

@Slf4j
@Configuration
public class QRCodeUtil {

    private static final String grant_type = "client_credential";

    private static final Integer expireTime = 24*60*60*1000;


    public static JSONObject createQRCode(String userId,String appId,String appSecret) {
        String accessToken = getAccessToken(appSecret,appId);

        String ticketUrl = "https://api.weixin.qq.com/cgi-bin/qrcode/create?access_token=" + accessToken;

        log.info("ticketUrl:{}",ticketUrl);

        JSONObject ticketParam = JSONUtil.createObj();
        JSONObject scene = JSONUtil.createObj();
      	// 生成一个随机值
        String key = "1" + GenerationSequenceUtil.getRandNum(8);
        scene.put("scene_id", key);

        log.info("scene:{}",scene);

        JSONObject actionInfo = JSONUtil.createObj();
        actionInfo.put("scene",scene);
        ticketParam.put("action_info", actionInfo);
        ticketParam.put("action_name", "QR_SCENE");
        ticketParam.put("expire_seconds", expireTime);

        log.info("ticketParam:{}",ticketParam);

        // ticketParam转json字符串
        String ticketParamToString = com.alibaba.fastjson.JSONObject.toJSONString(ticketParam);

        // 获取ticket
        String ticketBody = cn.hutool.http.HttpUtil.post(ticketUrl,ticketParamToString);

        log.info("获取ticketBody{}",ticketBody);

        com.alibaba.fastjson.JSONObject ticketContent = com.alibaba.fastjson.JSONObject.parseObject(ticketBody);

        String ticket = (String) ticketContent.get("ticket");

        String QRCodeUrl = "https://mp.weixin.qq.com/cgi-bin/showqrcode?ticket=" + ticket;

        JSONObject returnJson = JSONUtil.createObj();

        returnJson.put("url",QRCodeUrl);
        returnJson.put("key",key);
        return returnJson;
    }

    public static String getAccessToken(String appSecret,String appId) {
        String accessTokenUrl = "https://api.weixin.qq.com/cgi-bin/token?grant_type=" + grant_type
                + "&appid=" + appId + "&secret=" + appSecret;

        // 获取accessToken
        String accessTokenBody = cn.hutool.http.HttpUtil.get(accessTokenUrl);

        log.info("获取accessToken{}",accessTokenBody);

        com.alibaba.fastjson.JSONObject jsonObject = com.alibaba.fastjson.JSONObject.parseObject(accessTokenBody);
        String accessToken = null;
        try {
            accessToken = (String) jsonObject.get("access_token");
            log.info("accessToken:{}",accessToken);
        } catch (Exception e) {
            log.error("获取accessToken异常");
        }
        return accessToken;
    }



}

接口:

/**
     * 获取公众号二维码
     */
    @ApiOperation(value = "获取公众号二维码")
    @GetMapping("/createQRCode")
    @Pass
    public ResponseModel<cn.hutool.json.JSONObject> createQRCode(@RequestParam(name = "userId",required = false) String userId) {

        cn.hutool.json.JSONObject qrCode = QRCodeUtil.createQRCode(userId,appId,appSecret);
        String key = (String) qrCode.get("key");
        if (userId != null) {
            redisTemplate.opsForValue().set(key + ":userId",userId);
        } else {
            redisTemplate.opsForValue().set(key, 0);
        }
        return ResponseHelper.succeed(qrCode);
    }

解释:这里返回的是二维码和一个随机值,然后用redis以该随机值为key,value初始值为0存储到redis中,到这里二维码的创建就完成了,存储随机值的目的是,用户扫码之后,我们在微信调用回调接口时,处理业务逻辑之后,是将结果返回给微信,无法返回到前端,这里就需要前端根据刚才的随机值来获取扫码结果,我们将扫码后的登录结果存储到redis中即可

4.编写微信基础配置接口

前面已经将带参数的二维码生成了,这里需要两个接口,而且路径必须相同,一个是get方式的,这个接口是用于微信调用,测试接口是否畅通,如果该接口未通,测试公众号这边的基础配置就会配置失败,另一个为post请求,微信会给改接口发送xml包,接口接收到后根据xml包数据进行业务处理

/***
     * 微信服务器触发get请求用于检测签名
     * @return
     */
    @GetMapping("/receiveWx")
    @ResponseBody
    @Pass
    public void handleWxCheckSignature(HttpServletRequest request, HttpServletResponse response) throws IOException {
        response.getWriter().println(request.getParameter("echostr"));
    }
/**
     * 微信公众号回调事件
     *
     * @param request
     * @return
     */
    @Pass
    @PostMapping(value = "/receiveWx")
    public void checkWxToken(HttpServletRequest request,HttpServletResponse response) throws IOException, DocumentException {
        Map<String, String> xmlToMap = xmlToMap(request);
        log.info("map:{}",xmlToMap);

        String eventKey = xmlToMap.get("EventKey");

        String event = xmlToMap.get("Event");
        String openId = xmlToMap.get("FromUserName");
        String toUserName = xmlToMap.get("ToUserName");
        String accessToken = QRCodeUtil.getAccessToken(appSecret,appId);

        TextMessage text = new TextMessage();
        text.setFromUserName(toUserName);
        text.setToUserName(openId);
        text.setCreateTime(System.currentTimeMillis() + "");
        text.setMsgType("text");

//        try {
            if (ComUtil.isNotEmpty(event)) {
                if (event.equals("subscribe")) {
                    eventKey = eventKey.replaceAll("qrscene_","");
                    log.info("eventKey:{}",eventKey);
                    Object value = redisTemplate.opsForValue().get(eventKey + ":userId");
                    String userId = null;
                    if (value != null) {
                        userId = (String) value;
                    }
                    User user = userService.getById(userId);
                    if (user != null) {
                        userService.bindWx(userId, openId);
                        // 用户绑定微信成功 赋值
                        redisTemplate.opsForValue().set(userId + ":isBindWx",openId,60,TimeUnit.SECONDS);
                        text.setContent("绑定微信成功");
                    } else {

                        String userName = "书院学员" + GenerationSequenceUtil.unRepeatSixCode();
                        Long id = IdWorker.getId();
                        user = User.builder().id(id)
                                .userName(userName)
                                .avatar("(NULL)")
                                .code(GenerationSequenceUtil.unRepeatSixCode())
                                .openId(openId)
                                .gender(Constant.GenericNumber.NUMBER_ZERO)
                                .status(Constant.GenericNumber.NUMBER_ONE)
                                .createTime(LocalDateTime.now())
                                .build();
                        userService.save(user);

                        // 添加用户学员关系
                        userToRoleService.saveLocal(id, Constant.RolesRelation.Student.getId());
                        text.setContent("注册成功,请再次扫描二维码登录");
                    }
                } else if (event.equals("SCAN")) {

                    eventKey = eventKey.replaceAll("qrscene_","");
                    log.info("eventKey:{}",eventKey);
                    Object value = redisTemplate.opsForValue().get(eventKey + ":userId");
                    String userId = null;
                    if (value != null) {
                        userId = (String) value;
                    }
                    User user = userService.getById(userId);
                    if (user != null) {
                        userService.bindWx(userId, openId);
                        // 用户绑定微信成功 赋值
                        redisTemplate.opsForValue().set(userId + ":isBindWx",openId,60,TimeUnit.SECONDS);

                        text.setContent("绑定微信成功");
                    } else {
                        log.info("accessToken:{}", accessToken);

                        String userUrl = "https://api.weixin.qq.com/cgi-bin/user/info?access_token=" + accessToken + "&openid=" + openId + "&lang=zh_CN";
                        String userJson = HttpUtil.get(userUrl);
                        log.info("获取的userJson{}", userJson);
                        final WxMpController.UserInfo userInfo = JSON.parseObject(userJson, WxMpController.UserInfo.class);
                        log.info("获取的userInfo{}", userInfo);
                        WxUserVo wxUserVo = WxUserVo.builder()
                                .userName("书院学员")
                                .avatar("(NULL)")
                                .wxId(openId)
                                .build();
                        LoginVO loginVO = userService.loginByWxId(wxUserVo, Constant.LoginApp.tenant);
                        log.info("loginVO:{}", loginVO);
                        redisTemplate.opsForValue().set(eventKey, loginVO, 60 * 60, TimeUnit.SECONDS);

                        text.setContent("已登录" + loginVO.getUser().getUserName());
                    }
                } else if (event.equals("unsubscribe")) {
                    User userByOpenId = userService.getUserByOpenId(openId);
                    if (userByOpenId != null) {
                        try {
                            userService.deleteByUserNo(String.valueOf(userByOpenId.getId()));
                        } catch (Exception e) {
                            log.info("删除用户失败");
                        }
                    }
                }
            }

        String message = textMessageToxml(text);
        response.setCharacterEncoding("UTF-8");
        response.getWriter().println(message);
    }



    public Map<String ,String > xmlToMap(HttpServletRequest request) throws IOException, DocumentException {
        HashMap<String, String> map = new HashMap<>();
        SAXReader saxReader = new SAXReader();
        ServletInputStream inputStream = request.getInputStream();
        Document read = saxReader.read(inputStream);
        Element rootElement = read.getRootElement();
        List<Element> elements = rootElement.elements();
        for (Element e:elements) {
            map.put(e.getName(),e.getText());
        }

        inputStream.close();
        return map;
    }

    public String textMessageToxml(TextMessage textMessage) {
        XStream xStream = new XStream();
        xStream.alias("xml", textMessage.getClass());
        return xStream.toXML(textMessage);
    }

5.公众号接口基础配置

URL的域名,如果是http,端口就必须是80,如果是https,端口就必须是443,而且get请求的端口必须是可以正常访问的,否则配置失败;我这里本地调试用的natapp内网穿透,网站内有教程 https://natapp.cn/

微信公众号关注自定绑定java 关注公众号接口_json_02

6.前端调用获取登录结果接口

前端生成二维码之后,轮询该接口即可;或则前后端使用websoket进行通信也行

/**
     * 获取登录返回对象
     */
    @ApiOperation(value = "获取登录返回对象")
    @GetMapping("/getLoginVO")
    @Pass
    public ResponseModel<Object> getLoginVO(@RequestParam(name = "key") String key) {

        Object result = redisTemplate.opsForValue().get(key);
        if (result != null) {
            if (result instanceof String || result instanceof Integer) {
                return ResponseHelper.succeed(Constant.GenericNumber.NUMBER_ZERO);
            } else {
                LoginVO loginVO = (LoginVO) result;
                return ResponseHelper.succeed(loginVO);
            }
        } else {
            return ResponseHelper.succeed(Constant.GenericNumber.NUMBER_ZERO);
        }
    }