1、前期准备
公众号和小程序相互关联
准备公众号文章
注册公众号测试号
微信静默授权的独立html 文件
2: 小程序代码
webview页面代码
小程序首页代码
3:后端代码
1:增加公众号配置项
2:读取公众号配置项
3:增加微信公众号工具类
4:增加微信用户基本信息类
5:增加跳转html页面的接口
6:HttpClientUtil网络请求工具类
7:使用步骤
4:上线配置
这篇文章讲的是uniapp微信小程序【引导用户关注微信公众号】,【判断用户是否关注公众号】,用的是后端是 java springBoot,不是自己想要的网友可以关闭页面了。
需求背景:客户的下单系统原本是建立在微信公众号的,现在要求换成小程序,但涉及到消息推送的还是用公众号的推送,所以要求用户进入小程序的时候判断是否已经关注公众号了,没有关注就提示,并引导用户关注。
一收到需求,鸭蛋,没弄过,纯纯小白,第一件事当然就是找度娘了,查出的资料五花八门,各种各样,各种不完整,缺失关键代码和步骤,尤其【C】SDN,一大堆都是抄的,真TM的痛苦,没办法,我太菜了。
现在说一下我的实现方式:
引导关注公众号:微信小程序有个自带的引导用户关注微信公众号的组件,official-account,使用简单,就是限制太多了,必须指定方式打开小程序才显示,完全不符合客户需求,后来我是跳转到公众号的一篇文章,里面有公众号卡片和二维码,让用户进行关注。
判断是否关注公众号:网上基本清一色就是建表存关注的用户信息,然后定时拉取更新表。还有就是主动拉取微信公众号全部关注的用户,然后通过unionid进行判断是否关注。我一看就觉得完全没法用,公众号的用户体量几千万,要是每次判断都是去拉取全部用户信息,然后再一个个对比,这时间得多久,性能也会吃力。其实来来回回主要的难点就是在于小程序怎么获取到用户在公众号的openId,我的做法是跳转一个空白登录页,获取到微信公众的loginCode后,后台解析获取到对应的openId,再用公众号的openId去判断是否已经关注,然后再跳转回对应功能页面,对于用户来说,就是多了一次跳转,时间也就是1秒多这样,当然不是在首页进行操作,具体可以认真看完。
1:一个微信公众号,一个微信小程序,以及他们的appid和AppSecret(怎么拿到自己百度吧,也不是很难)。
2:微信公众号的一篇引导关注文章。
3:一个独立的html文件,里面是写微信公众号静默登录代码的。
4:在微信公众号后台管理那里,注册一个测试号,用作等会静默登录测试用。
5:把自己的微信号都添加成公众号和小程序的开发者。
搜索微信公众平台,扫码登录选择公众号,进入公众号管理后台
在微信管理后台把自己的微信号加入到公众号的开发者里面。
小程序如果你能登录到管理后台,说明你已经是项目成员,不然让管理员把你加到项目成员里面就可以了。
首先是小程序和公众号必须是同一个主体下的,并相互关联,如果不相互关联,小程序的web-view组件是打不开公众号的文章的,比如出现下面这种情况。
官方文档链接:小程序webview访问公众号文章提示非业务域名 | 微信开放社区
》》搜索微信公众平台,扫码登录选择公众号,进入公众号管理后台,进行关联小程序。
》》搜索微信公众平台,扫码登录选择小程序,进入小程序管理后台,左下角设置,进行关联公众号。
微信公众号发一篇引导关注文章,放个二维码,公众号卡片啥的,然后去复制这篇文章的链接,这一步看似简单,实则有坑,按常规的步骤复制,得到是一个短链接,比如下面的步骤得出链接是:https://mp.weixin.qq.com/s/7wYtVJc6XXXXXXXXXXXX...
这样的链接你在微信开发者工具是完全没有问题的,一到真机上面就不行。
我们必须复制出一个长链接,有参数的。首先电脑端微信打开公众号主页,找到对应文章,右键选择默认浏览器打开,然后再复制浏览器上的链接,就会得到一个长链接,比如:
https://mp.weixin.qq.com/s?__biz=MjM5&mid=28192&idx=1....
后面带有_biz,mid这种参数的
拿到引导关注的文章链接,先找个地方保存好,后面要用。
接下来是注册一个测试公众号,搜索微信公众平台,扫码登录选择公众号,进入公众号管理后台。
里面步骤
1【记下测试号的appid和appsecret】
2【设置JS接口安全域名】
3【用自己微信扫码关注测试号】
4【体验接口权限表里面找到网页授权获取用户基本信息,右边修改授权回调页面域名】
两个域名(js接口域名和授权回调域名)也可以保持一致。
测试号的域名可以直接填本机ip加端口号,记住不要加http或https开头,比如 192.168.0.41:8080
新建一个文本文件,把下面代码放进去,然后文件重新名,后缀改成html即可,找个地方放好。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width" />
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
</head>
<body>
</body>
<!-- 引入微信js库 -->
<script src="https://res.wx.qq.com/open/js/jweixin-1.6.0.js"></script>
<script>
/**
* 代码说明:
* 1:进入这个页面后,执行window.onload方法,然后调用getUrlCode方法判断有没有code参数
* 没有code参数,重定向到微信授权链接,此链接无感静默授权,完成后会调指定的回调链接,也就是REDIRECT_URI参数那里。
* 如果有code参数,说明是授权完成,回调后再次进入这个页面的,此时的code应该就是wx.login方法获得的code。
* 2:获得code后,wx.miniProgram.redirectTo关闭当前页面,跳转到小程序里面的webview组件页面,/pages/webView/webView页面里面只有一个webview组件,
* 我们传入url和微信登录code参数进行跳转,此时webview组件打开公众号文章,并获得了此用户在公众号的登录code。
**/
window.onload = ()=>{
const { code, state } = getUrlCode()
if (code) {
const subscribeUrl = `url=${encodeURIComponent('前面准备的公众号文章链接')}&title=${decodeURIComponent('小程序webView页面的标题,此参数不是必须')}&wxPublicLoginCode=${encodeURIComponent(code)}`
wx.miniProgram.redirectTo({
url: `/pages/webView/webView?${subscribeUrl}`
})
} else {
window.location.href = 'https://open.weixin.qq.com/connect/oauth2/authorize?appid=APPID&redirect_uri=REDIRECT_URI&response_type=code&scope=snsapi_base&state=STATE#wechat_redirect'
.replace('APPID', '你的公众号appid,现在开发先用测试号的appid')
.replace('REDIRECT_URI', '微信授权完后的回调页面链接,别着急,慢慢看完文章')
.replace('STATE', '0') // 进入回调链接后的附带参数,看个人需要吧,有需要就传,比如你这个页面被多个业务场景使用,但每个业务场景回调的页面不一样,至于怎么用,动动脑子
}
}
// 截取url中的code方法
function getUrlCode() {
const url = location.search
const theRequest = new Object()
if (url.indexOf("?") != -1) {
const str = url.substr(1)
const strs = str.split("&")
for (let i = 0; i < strs.length; i++) {
theRequest[strs[i].split("=")[0]] = strs[i].split("=")[1]
}
}
return theRequest
}
</script>
</html>
具体思路是:用户通过webview组件访问我们写的html页面,首次进入,页面会重定向到微信授权,自动授权完成后,再次自动重定向回此html页面,此页面拿到code,然后关闭此html页面,跳转微信公众号文章页面,用户进行关注,然后用户点击返回的时候,我们通过拿到的code去获取微信公众号的openId,进而通过openId判断用户是否已关注。流程图大致如下吧。
如果要了解微信网页授权,也就是html页面里面的window.location.href重定向授权链接参数,可以看下面的文档,这里不详细说明了。
官方的网页授权文档:网页授权 | 微信开放文档
我这边的做法是用户表里面有个字段记录微信公众号的openId,用户登录进入首页后,如果此字段没有值,则说明此账号没有绑定过微信公众号。这里可能有人会说,如果这个账号在其他微信号上面登录呢,也就是在A微信登录,并关注公众号,然后又去B微信登录,此时表里的字段存的是A微信的openId,B微信实则没有关注,这里其实就要看对应系统业务需要了,我这边是要关注公众号后,然后利用公众号推消息,所以我的小程序里面会有一个页面是用来专门处理换绑,也就是从A微信的关注换绑到B微信的关注,但现在写教程,我就不说具体业务了,只说怎么拿到openId,然后判断有没有关注,如果你是需要每次都判断当前微信有没有关注,那把具体入口写在登录页吧,比如用户点击登录跳转登录页,你可能是跳转/pages/login页面,那改成直接跳转webview页面,然后访问我们写的html授权页面,授权完成拿到code后,html页面代码里面的wx.miniProgram.redirectTo跳转到小程序登录页去,那你小程序的登录页也就拿到code了,从而可以判断是否已经关注。
回归正题
我们登录成功进入首页,判断openId如果没有值,则则给出关注提示。点击提示跳转到webview组件页面,访问我们写的html页面,一连串执行后,用户点击返回会回到首页,首页接收微信公众号的登录code,并调接口进行解析。如果有openId,我们也可以发出请求,查此openId的微信号有没有关注公众号。
记得在pages文件配置上这个页面
<template>
<view>
<web-view :src="url"></web-view>
</view>
</template>
<script>
export default {
data() {
return {
url: '',
wxPublicLoginCode: void (0)
}
},
onLoad(options) {
// 获取到要访问的链接,并decodeURIComponent解码
this.url = decodeURIComponent(options.url)
// webview页面标题
if (options.title) {
uni.setNavigationBarTitle({
title: decodeURIComponent(options.title)
})
}
// 微信公众号登录code
this.wxPublicLoginCode = decodeURIComponent(options.wxPublicLoginCode || '')
},
// 页面卸载事件, 用户在微信公众号文章页面点击返回触发
onUnload () {
const pages = getCurrentPages()
if (pages.length > 1) {
const { route } = pages[pages.length - 2]
// 判断路由栈里面当前页面的上一个页面是不是首页,如果是则设置缓存
if (route === 'pages/index/index') {
this.wxPublicLoginCode && uni.setStorageSync('wxPublicLoginCode', this.wxPublicLoginCode)
}
}
}
}
</script>
1:判断登录后有表里没有微信公众号的openId,如果有onLoad方法请求后端,获取微信是否关注公众号,如果没有,显示提示。如果表里没有微信公众号的openId,直接显示提示。
2:点击提示跳转webview页面。此链接打开的是我们写的那个html页面,本地写成自己的ip,上线后用线上域名。后端部分后面再说。
注意:http://192.168.0.41:8080/api/express/member/wxPublicLogin是访问我们写的html页面的链接,那么前面我们写的html文件里面,REDIRECT_URI的值也是这个链接。
uni.navigateTo({
url: `/pages/webView/webView?url=${encodeURIComponent('http://192.168.0.41:8080/api/express/member/wxPublicLogin')}`
})
3:跳转webview页面后,一系列执行,进入公众号文章页面。关不关注那是用户的事,我们只负责引导。
4:点击左上角返回,会设置缓存code,然后回到首页。code是在webview页面的onUnload方法设置的。首页的onShow方法写上接收缓存。
onShow () {
const wxPublicLoginCode = uni.getStorageSync('wxPublicLoginCode')
if (wxPublicLoginCode) {
uni.removeStorageSync('wxPublicLoginCode')
this.onBindingWxPublic(wxPublicLoginCode)
}
}
onBindingWxPublic方法是把code传回后端,然后解析出openId,然后查询是否已关注公众号,关注了就记录openId到表里面,没有关注就不管了,接口返回 boolean 是否已关注就行了。
#微信公众号的appid,开发阶段就填测试号的,上线再填真正公众号的
wx.public.appid=xxxxx549xxxxxxxxx
#微信公众号的appsecret,开发阶段就填测试号的,上线再填真正公众号的
wx.public.secret=xxxxxxxf60xx7bc0xxxxxxxxxxxxxx
#这是我们写的html页面位置,本地开发就先写死,上线后把文件放到服务器上面,再把这里的配置路径改掉
wx.public.loginFilePath=C:/Users/user1/Desktop/wx_public_login.html
建一个WxPublicProperties类
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
@Configuration
@ConfigurationProperties(prefix = "wx.public")
public class WxPublicProperties {
/**
* 微信公众号的appid
*/
private String appid;
/**
* 微信公众号的Secret
*/
private String secret;
/**
* 微信公众号的登录文件路径
*/
private String loginFilePath;
public String getAppid() {
return appid;
}
public void setAppid(String appid) {
this.appid = appid;
}
public String getSecret() {
return secret;
}
public void setSecret(String secret) {
this.secret = secret;
}
public String getLoginFilePath() {
return loginFilePath;
}
public void setLoginFilePath(String loginFilePath) {
this.loginFilePath = loginFilePath;
}
}
类名:WxPublicUtil,里面的HttpClientUtil类放在最后面
import com.alibaba.fastjson.JSONObject;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.Objects;
@Component
public class WxPublicUtil {
private final static Logger log = LoggerFactory.getLogger(WxMiniAppUtil.class);
private WxPublicProperties wxPublicProperties;
/**
* 获取用户微信公众号openId
* @param wxPublicLoginCode 微信公众号登录code
* @return 用户公众号openId
*/
public String getOpenId(String wxPublicLoginCode) {
try {
// 拼接url发起请求到微信认证服务器获取access_token等信息
String tokenUrl = "https://api.weixin.qq.com/sns/oauth2/access_token?appid=APPID&secret=APPSECRET&code=LOGIN_CODE&grant_type=authorization_code"
.replace("APPID", this.wxPublicProperties.getAppid())
.replace("APPSECRET", this.wxPublicProperties.getSecret())
.replace("LOGIN_CODE", wxPublicLoginCode);
String accessTokenResult = HttpClientUtil.doGet(tokenUrl);
log.info("获取微信公众号open_id请求结果如下,loginCode:{},requestResult:{}", wxPublicLoginCode, accessTokenResult);
if (Objects.isNull(accessTokenResult)) {
return null;
}
JSONObject tokenObj = JSONObject.parseObject(accessTokenResult);
if (StringUtils.isNotBlank(tokenObj.getString("errcode"))) {
return null;
}
return tokenObj.getString("openid");
} catch (Exception e) {
log.error("获取微信公众号open_id失败", e);
return null;
}
}
/**
* 获取用户基本信息
* @param openId 微信公众号openId
*
*/
public WxUserInfoVO getUserBaseInfo(String openId) {
if (StringUtils.isBlank(openId)) {
return null;
}
String accessToken = this.getAccessToken();
if (StringUtils.isBlank(accessToken)) {
return null;
}
try {
String requestUrl = "https://api.weixin.qq.com/cgi-bin/user/info?access_token=ACCESS_TOKEN&openid=OPENID&lang=zh_CN"
.replace("ACCESS_TOKEN", accessToken)
.replace("OPENID", openId);
String requestResult = HttpClientUtil.doGet(requestUrl);
log.info("获取微信用户基本信息请求结果如下: {}", requestResult);
if (Objects.isNull(requestResult)) {
return null;
}
JSONObject resultObject = JSONObject.parseObject(requestResult);
if (StringUtils.isNotBlank(resultObject.getString("errcode"))) {
return null;
}
return JSONObject.parseObject(requestResult, WxUserInfoVO.class);
} catch (Exception e) {
log.error("获取微信公众号access_token失败", e);
return null;
}
}
/**
* 获取access_token
*/
private String getAccessToken() {
try {
String requestUrl = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=APPID&secret=APPSECRET"
.replace("APPID", this.wxPublicProperties.getAppid())
.replace("APPSECRET", this.wxPublicProperties.getSecret());
String accessTokenResult = HttpClientUtil.doGet(requestUrl);
log.info("获取微信公众号access_token请求结果如下: {}", accessTokenResult);
if (Objects.isNull(accessTokenResult)) {
return null;
}
JSONObject tokenObj = JSONObject.parseObject(accessTokenResult);
if (StringUtils.isNotBlank(tokenObj.getString("errcode"))) {
return null;
}
return tokenObj.getString("access_token");
} catch (Exception e) {
log.error("获取微信公众号access_token失败", e);
return null;
}
}
@Autowired
public void setWxPublicProperties(WxPublicProperties wxPublicProperties) {
this.wxPublicProperties = wxPublicProperties;
}
}
类名:WxUserInfoVO,里面有个微信公众号最近一次关注的时间subscribeTime,那个时间戳要转换成long再乘以1000才是完整时间,有需要这个时间的话,注意一下。
get和set方法我去掉了,自己加一下吧。
import java.io.Serializable;
/**
* 微信用户信息
*/
public class WxUserInfoVO implements Serializable {
private static final long serialVersionUID = 1L;
/**
* openId
*/
private String openid;
/**
* 昵称
*/
private String nickname;
/**
* 性别
*/
private String sex;
/**
* 语言
*/
private String language;
/**
* 省份
*/
private String province;
/**
* 城市
*/
private String city;
/**
* 区县
*/
private String country;
/**
* 头像
*/
private String headimgurl;
/**
* 是否已关注
*/
private Integer subscribe;
/**
* 关注时间
*/
private String subscribeTime;
/**
* unionid
*/
private String unionid;
}
记得先用@Autowired注入WxPublicProperties,这个接口是读取我们写的html页面,并返回,相当于是用户调用这个接口是进入我们写的html页面。IOUtils.toByteArray是把输入流转成字节,如果没有这个工具类,就手写吧,实在不懂百度【java输入流转byte字节数组】。
这个接口记得不要被权限拦截,也就是没有登录也可以访问。
/**
* 公众号无感登录
*/
@RequestMapping(value = "/member/wxPublicLogin", method = RequestMethod.GET)
public void wxPublicLogin(HttpServletResponse response) throws IOException {
try(
FileInputStream fileInputStream = new FileInputStream(this.wxPublicProperties.getLoginFilePath());
){
response.getOutputStream().write(IOUtils.toByteArray(fileInputStream));
}
}
import org.apache.http.*;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.HttpRequestRetryHandler;
import org.apache.http.client.ResponseHandler;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.client.protocol.HttpClientContext;
import org.apache.http.config.ConnectionConfig;
import org.apache.http.config.Registry;
import org.apache.http.config.RegistryBuilder;
import org.apache.http.config.SocketConfig;
import org.apache.http.conn.ConnectTimeoutException;
import org.apache.http.conn.ConnectionKeepAliveStrategy;
import org.apache.http.conn.socket.ConnectionSocketFactory;
import org.apache.http.conn.socket.PlainConnectionSocketFactory;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.message.BasicHeaderElementIterator;
import org.apache.http.protocol.HTTP;
import org.apache.http.protocol.HttpContext;
import org.apache.http.ssl.SSLContexts;
import org.apache.http.util.EntityUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.net.ssl.SSLException;
import javax.net.ssl.SSLHandshakeException;
import java.io.IOException;
import java.io.InterruptedIOException;
import java.net.UnknownHostException;
import java.nio.charset.CodingErrorAction;
public class HttpClientUtil {
private final static Logger log = LoggerFactory.getLogger(HttpClientUtil.class);
/**
* 连接池最大连接数
*/
private static final int MAX_TOTAL_CONNECTIONS = 200;
/**
* 设置每个路由上的默认连接个数
*/
private static final int DEFAULT_MAX_PER_ROUTE = 20;
/**
* 请求的请求超时时间 单位:毫秒
*/
private static final int REQUEST_CONNECTION_TIMEOUT = 8 * 1000;
/**
* 请求的等待数据超时时间 单位:毫秒
*/
private static final int REQUEST_SOCKET_TIMEOUT = 8 * 1000;
/**
* 请求的连接超时时间 单位:毫秒
*/
private static final int REQUEST_CONNECTION_REQUEST_TIMEOUT = 5 * 1000;
/**
* 连接闲置多久后需要重新检测 单位:毫秒
*/
private static final int VALIDATE_AFTER_IN_ACTIVITY = 2 * 1000;
/**
* 关闭Socket时,要么发送完所有数据,要么等待多少秒后,就关闭连接,此时socket.close()是阻塞的 单位秒
*/
// private static final int SOCKET_CONFIG_SO_LINGER = 60;
/**
* 接收数据的等待超时时间,即读超时时间,单位ms
*/
private static final int SOCKET_CONFIG_SO_TIMEOUT = 5 * 1000;
/**
* 重试次数
*/
private static int RETRY_COUNT = 5;
/**
* 声明为 static volatile,会迫使线程每次读取时作为一个全局变量读取
*/
private static volatile CloseableHttpClient httpClient = null;
/**
* @param uri
* @return String
* @description get请求方式
* @author: long.he01
*/
public static String doGet(String uri) {
String responseBody;
HttpGet httpGet = new HttpGet(uri);
try {
httpGet.setConfig(getRequestConfig());
responseBody = executeRequest(httpGet);
} catch (IOException e) {
log.error("httpclient doGet方法异常, 请求地址:{}", uri);
throw new RuntimeException("httpclient doGet方法异常 ", e);
} finally {
httpGet.releaseConnection();
}
return responseBody;
}
/**
* @return RequestConfig
* @description: 获得请求配置信息
*/
private static RequestConfig getRequestConfig() {
RequestConfig defaultRequestConfig = RequestConfig.custom()
.setExpectContinueEnabled(true)
.build();
return RequestConfig.copy(defaultRequestConfig).setSocketTimeout(REQUEST_CONNECTION_TIMEOUT)
.setConnectTimeout(REQUEST_SOCKET_TIMEOUT)
.setConnectionRequestTimeout(REQUEST_CONNECTION_REQUEST_TIMEOUT).build();
}
/**
* @param method
* @return String
* @throws IOException
* @description 通用执行请求方法
*/
private static String executeRequest(HttpUriRequest method) throws IOException {
ResponseHandler<String> responseHandler = new ResponseHandler<String>() {
@Override
public String handleResponse(final HttpResponse response) throws IOException {
int status = response.getStatusLine().getStatusCode();
String result;
if (status >= HttpStatus.SC_OK && status < HttpStatus.SC_MULTIPLE_CHOICES) {
HttpEntity entity = response.getEntity();
result = entity != null ? EntityUtils.toString(entity) : null;
EntityUtils.consume(entity);
return result;
} else {
throw new ClientProtocolException("Unexpected response status: " + status);
}
}
};
String result = getHttpClientInstance().execute(method, responseHandler);
return result;
}
/**
* @return CloseableHttpClient
* @description 单例获取httpclient实例
*/
private static CloseableHttpClient getHttpClientInstance() {
if (httpClient == null) {
synchronized (CloseableHttpClient.class) {
if (httpClient != null) {
return httpClient;
}
ConnectionKeepAliveStrategy myStrategy = new ConnectionKeepAliveStrategy() {
@Override
public long getKeepAliveDuration(HttpResponse response, HttpContext context) {
HeaderElementIterator it = new BasicHeaderElementIterator(
response.headerIterator(HTTP.CONN_KEEP_ALIVE));
while (it.hasNext()) {
HeaderElement he = it.nextElement();
String param = he.getName();
String value = he.getValue();
if (value != null && param.equalsIgnoreCase("timeout")) {
return Long.parseLong(value) * 1000;
}
}
return 60 * 1000;// 如果没有约定,则默认定义时长为60s
}
};
httpClient = HttpClients.custom().setConnectionManager(initConfig()).setKeepAliveStrategy(myStrategy)
.setRetryHandler(getRetryHandler()).build();
}
}
return httpClient;
}
/**
* @return HttpRequestRetryHandler
* @description :获取重试handler
*/
private static HttpRequestRetryHandler getRetryHandler() {
// 请求重试处理
return new HttpRequestRetryHandler() {
@Override
public boolean retryRequest(IOException exception, int executionCount, HttpContext context) {
if (executionCount >= RETRY_COUNT) {
// 假设已经重试了5次,就放弃
return false;
}
if (exception instanceof NoHttpResponseException) {
// 假设server丢掉了连接。那么就重试
return true;
}
if (exception instanceof SSLHandshakeException) {
// 不要重试SSL握手异常
return false;
}
if (exception instanceof InterruptedIOException) {
// 超时
return false;
}
if (exception instanceof UnknownHostException) {
// 目标server不可达
return false;
}
if (exception instanceof ConnectTimeoutException) {
// 连接被拒绝
return false;
}
if (exception instanceof SSLException) {
// SSL握手异常
return false;
}
HttpRequest request = HttpClientContext.adapt(context).getRequest();
// 假设请求是幂等的,就再次尝试
return !(request instanceof HttpEntityEnclosingRequest);
}
};
}
/**
* @return PoolingHttpClientConnectionManager
* @description 初始化连接池等配置信息
*/
private static PoolingHttpClientConnectionManager initConfig() {
Registry<ConnectionSocketFactory> socketFactoryRegistry = RegistryBuilder.<ConnectionSocketFactory>create()
.register("http", PlainConnectionSocketFactory.INSTANCE)
.register("https", new SSLConnectionSocketFactory(SSLContexts.createSystemDefault())).build();
PoolingHttpClientConnectionManager connManager = new PoolingHttpClientConnectionManager(socketFactoryRegistry);
SocketConfig socketConfig = SocketConfig.custom().setTcpNoDelay(true).setSoReuseAddress(true)
.setSoTimeout(SOCKET_CONFIG_SO_TIMEOUT)
.setSoKeepAlive(true)
.build();
connManager.setDefaultSocketConfig(socketConfig);
connManager.setValidateAfterInactivity(VALIDATE_AFTER_IN_ACTIVITY);
ConnectionConfig connectionConfig = ConnectionConfig.custom().setMalformedInputAction(CodingErrorAction.IGNORE)
.setUnmappableInputAction(CodingErrorAction.IGNORE).setCharset(Consts.UTF_8).build();
connManager.setDefaultConnectionConfig(connectionConfig);
connManager.setDefaultMaxPerRoute(DEFAULT_MAX_PER_ROUTE);
connManager.setMaxTotal(MAX_TOTAL_CONNECTIONS);
return connManager;
}
}
首先前端是调/member/wxPublicLogin接口进入html页面,然后html页面拿到code返回到具体功能页,这个时候就看你业务了,公众号的登录code都拿到了,你想怎么干就怎么干了。
用code换取openId,并用openId获取用户基本信息。记得先@Autowired注入WxPublicUtil,userBaseInfo里面就有openId等关注信息。字段subscribe = 1 就是已关注,0就是未关注。
WxUserInfoVO userBaseInfo = this.wxPublicUtil.getUserBaseInfo(this.wxPublicUtil.getOpenId('这里是获取到的code'));
3:微信公众号增加获取access_token接口ip白名单,把服务器的ip加上去。
如果获取access_token出现这个错误就是ip白名单没有配置 。复制错误信息里面ip去配置就行了。
{"errcode":40164,"errmsg":"invalid ip 47.113.116.141 ipv6 ::ffff:47.113.116.141, not in whitelist rid: 67120c51-61a9d8f0-27e7af1f"}
4:登录小程序后台管理,把服务器域名和业务域名设置上,就是配置上服务器的域名,不然webview打不开我们写的html页面,如果webview页面打不开公众号文章页面,就去看官方文档吧。
官方文档链接:小程序webview访问公众号文章提示非业务域名 | 微信开放社区
那个html页面是个无感登录页面,我们在这个页面获取到的code,去换取微信用户信息的时候,只能拿到openId和关注时间,其他信息(昵称,头像等)都拿不到,如果想要获取到其他信息,html文件里面的window.location.href重定向到微信授权链接,scope参数换成snsapi_userinfo,这样获取到的code就能拿到其他信息,不过这样话,进入这个html页面,会弹出用户信息授权确认的,比如下面这个。
应该差不多了吧,可能会有些遗漏,毕竟步骤是在是太多了,总得就是本地用测试号,上线换正式公众号。 值得注意的是,通过那个html页面拿到的登录code是有时效的,只有几分钟,如果用户在公众号文章页面停留太久,那这个code可能就过期了。具体怎么解决,是重新再跳一次无感登录页面,还是提示用户,你看着办吧。
目前上线后存在一个问题,电脑版小程序在html页面拿到登录code后跳转微信文章页面,有大概率跳不过去,就下面这个代码有可能没有反应,安卓和ios都没有问题,不知道啥情况,有解决的可以留言一下。
终于码字完了
码字不易,于你有利,勿忘点赞