微信公众号

注册微信公众号

微信公众号平台

微信公众号测试

新建微信公众号后台项目

注入依赖

<!--    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>

验证服务器

  1. 控制器
@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;
    }
  1. 工具类
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;
    }

消息接口

  1. 控制器
@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();
        }
    }
  1. 工具类
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;
    }
  1. 实体类
  • 底层实体类
@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

java 推送公众号 javalibrary公众号_微信公众号

打开exe文件

java 推送公众号 javalibrary公众号_java 推送公众号_02

输入命令 ngrok http localhost:8080

java 推送公众号 javalibrary公众号_java 推送公众号_03

测试URL链接

java 推送公众号 javalibrary公众号_java 推送公众号_04

**token随意填写,注意与后端的验证token一致 **

公众号配置服务器

java 推送公众号 javalibrary公众号_微信公众号_05

**配置成功后,在ip白名单里面添加服务器ip **