全文目录,一步到位
1.前言简介
本文是微信小程序
公共类
以下功能均使用 此文章 代码
- 获取
accessToken
手机号
小程序二维码
openId与UnionId
最下面有传送门, 传送到每个功能 避免多次封装
远程调用均使用restTemplate (springboot自带, 操作简单)
使用其他请随意…
1.1 专栏传送门
2. 微信小程序公用功能
2.1 配置准备工作
ps: 初次搭建小程序这步骤不要忘记了
2.1.1 配置文件准备(单体放yml
中 微服务放配置中心
)
(
特别注意
这里面包含大部分微信小程序配置)
appid
和secret
填写上
ps: 里面部分配置可以删除
- 过期时间
- 启动执行
- 登录url
- 其他业务 如获取手机号, 获取小程序二维码等
# 微信核心参数
wechat:
# 小程序
mini-app:
# 请求url
requestUrl: /wxMiniLogin
# 请求方法
requestMethod: POST
# appid
appId: ?
# app秘钥
appSecret: ?
# 微信基础请求网址
wxBaseRequestUrl: https://api.weixin.qq.com
# 获取access_token必备参数
grantType: client_credential
# 微信获取access_token的url
aTokenUrl: ${wechat.mini-app.wxBaseRequestUrl}/cgi-bin/token?grant_type=${wechat.mini-app.grantType}&appid=%s&secret=%s
# 要分钟(尽量少于120分钟) 一天上限2000次 这里一天最多请求24次 可增加定时刷新缓存等
expiredTime: 70
# 是否执行服务启动执行
serverStartAutoRun: false
# 微信登录提供的接口(GET)
wxLoginUrlTemplate: ${wechat.mini-app.wxBaseRequestUrl}/sns/jscode2session?appid=%s&secret=%s&js_code=%s&grant_type=authorization_code
# 微信获取手机号接口
wxGetPhoneUrl: ${wechat.mini-app.wxBaseRequestUrl}/wxa/business/getuserphonenumber?access_token=%s
#微信获取二维码(不限制)post请求
wxACodeUnLimitUrl: ${wechat.mini-app.wxBaseRequestUrl}/wxa/getwxacodeunlimit?access_token=%s
#微信获取二维码(限制)post请求
wxACodeUrl: ${wechat.mini-app.wxBaseRequestUrl}/wxa/getwxacode?access_token=%s
# 微信获取二维码(限制)post请求
wxAQrcodeUrl: ${wechat.mini-app.wxBaseRequestUrl}/cgi-bin/wxaapp/createwxaqrcode?access_token=%s
2.1.2 获取配置文件中的小程序配置
使用@ConfigurationProperties注解对应
- 具体介绍我没有写: 找了一篇写的比较全的链接
- => 传送门: @ConfigurationProperties使用方式
- 当然 可以使用
@Value
替换上面这种(二选一
)代码如下, 名字对应配置文件名称, 注释自己加一下吧
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
* 小程序获取配置类
*
* @author pzy
* @version 0.1.0
* @description TODO
*/
@Slf4j
@Data
@Component
@ConfigurationProperties(prefix = "wechat.mini-app")
public class WechatConfigProperties {
private String requestUrl;
private String requestMethod;
private String appId;
private String appSecret;
private String wxBaseRequestUrl;
private String grantType;
private String aTokenUrl;
private Integer expiredTime;
private Boolean serverStartAutoRun;
private String wxLoginUrlTemplate;
/**
* 获取手机号
*/
private String wxGetPhoneUrl;
/**
* 二维码无限制url(主)
*/
private String wxACodeUnLimitUrl;
private String wxACodeUrl;
private String wxAQrcodeUrl;
/**
* 生成微信登录请求地址
*
* @param code code
* @return 请求地址
*/
public String getWxLoginUrl(String code) {
return String.format(wxLoginUrlTemplate, appId, appSecret, code);
}
/**
* 生成微信ACCESS_TOKEN请求地址
* <p>
* 模板样式: https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=%s&secret=%s
*
* @return 请求地址
*/
public String getATokenUrl() {
return String.format(aTokenUrl, appId, appSecret);
}
/**
* 获取手机号url post请求
*
* @param accessToken
* @return
*/
public String getPhoneUrl(String accessToken) {
return String.format(wxGetPhoneUrl, accessToken);
}
/**
* 获取不限制的微信二维码url
*
* @param accessToken
* @return
*/
public String getWxACodeUnLimitUrl(String accessToken) {
return String.format(wxACodeUnLimitUrl, accessToken);
}
/**
* 获取(限制一)的微信二维码url
*
* @param accessToken
* @return
*/
public String getWxACodeUrl(String accessToken) {
return String.format(wxACodeUrl, accessToken);
}
/**
* 获取(限制二)的微信二维码url
*
* @param accessToken
* @return
*/
public String getWxAQrcodeUrl(String accessToken) {
return String.format(wxAQrcodeUrl, accessToken);
}
}
2.1.3 设置redis配置
spring:
redis:
# 地址
host: 192.168.1.130
# 端口,默认为6379
port: 6379
# 数据库索引
database: 0
# 密码
password: 123456
# 连接超时时间
timeout: 10s
lettuce:
pool:
# 连接池中的最小空闲连接
min-idle: 5
# 连接池中的最大空闲连接
max-idle: 10
# 连接池的最大数据库连接数
max-active: 50
# 连接池最大阻塞等待时间(使用负值表示没有限制)
max-wait: 5000ms
2.2 创建不同功能工具类
2.2.1 创建微信服务工具类WechatServiceUtils
RestTemplate 直接可以
使用
WechatConfigProperties 请见2.1.2
RedisCache 请见2.2.2
/**
* 微信服务工具类
*
* @author pzy
* @version 0.1.0
* @description: TODO
*/
@Slf4j
@RequiredArgsConstructor
@Component
public class WechatServiceUtils {
/**
* 远程调用
*/
private final RestTemplate restTemplate;
/**
* redis缓存
*/
private final RedisCache redisCache;
/**
* 微信统一配置
*/
private final WechatConfigProperties wechatConfigProperties;
/**
* 1. 获取微信登录认证信息
*
* @param wxCommonReqDto
* @return
*/
public Map<String, String> getWxMiniAuth(WxCommonReqDto wxCommonReqDto) {
String code = wxCommonReqDto.getWxCode();
//秘钥
String encryptedIv = wxCommonReqDto.getIv();
//加密数据
String encryptedData = wxCommonReqDto.getEncryptedData();
JSONObject wxAuthResponse = null;
String sessionKey = "";
String openid = "";
try {
//1. 想微信服务器发送请求获取用户信息
String url = wechatConfigProperties.getWxLoginUrl(code);
log.info("===> 请求微信url是: {}", url);
//2. 远程调用微信接口
String res = restTemplate.getForObject(url, String.class);
wxAuthResponse = JSONObject.parseObject(res);
//3. 解析返回参数 报错则进行对应处理
/*校验1: wx请求不是null*/
if (wxAuthResponse != null) {
/*校验2: 响应对象是否正确*/
CheckUtils.responseCheck(wxAuthResponse);
//3.1 获取session_key和openid
sessionKey = wxAuthResponse.getString("session_key");
openid = wxAuthResponse.getString("openid");
log.info("===> openid: {}", openid);
/*校验3: 响应信息是否正常*/
if (StringUtils.isBlank(sessionKey) || StringUtils.isBlank(openid)) {
log.error("小程序授权失败,session_key或open_id是空!");
throw new ServiceException("抱歉, 小程序授权失败,缺少关键返回参数!");
}
log.info("===> 微信回调信息: {}", wxAuthResponse);
}
} catch (Exception e) {
e.printStackTrace();
throw new ServiceException("抱歉, 小程序授权失败!");
}
//4. 获取微信信息并制作token
/*校验:(选用)如果获取union_id 需要进行解密*/
Map<String, String> map = new HashMap<>();
String token = "";
if (StringUtils.isNotBlank(encryptedIv) && StringUtils.isNotBlank(encryptedData)) {
String decryptResult = "";
try {
//如果没有绑定微信开放平台,解析结果是没有unionid的。
decryptResult = AESUtils.decrypt(sessionKey, encryptedIv, encryptedData);
if (StringUtils.hasText(decryptResult)) {
//如果解析成功,获取token
map.put("type", String.valueOf(1));
map.put("decryptResult", decryptResult);
}
} catch (Exception e) {
e.printStackTrace();
throw new ServiceException("微信登录失败!");
}
} else {
//如果前端只传wxCode 没有unionId的需求 只要openId的
map.put("type", String.valueOf(2));
map.put("decryptResult", openid);
}
/*校验: 数据为空的情况*/
if (StringUtils.isBlank(map.get("type")) || StringUtils.isBlank(map.get("decryptResult"))) {
throw new ServiceException(ResponseEnum.A10007);
}
return map;
}
/**
* 2. 获取缓存中的AccessToken
* <p>
* 没有从微信拉取[可配合定时]
*
* @return accessToken
*/
public String getRedisCacheAccessToken() {
/*校验: 缓存中有accessToken的key*/
if (redisCache.hasKey(CacheConstants.WX_ACCESS_TOKEN)) {
log.info("二级缓存数据取出accessToken成功!");
return redisCache.getCacheObject(CacheConstants.WX_ACCESS_TOKEN);
}
//这里不用三目(不好看~~)
return getWxMiniAccessToken();
}
/**
* 3. 访问微信官方获取两小时的 accessToken
*
* @return accessToken
*/
public String getWxMiniAccessToken() {
Map<String, String> query = new HashMap<>();
query.put("grant_type", wechatConfigProperties.getGrantType());//client_credential
query.put("secret", wechatConfigProperties.getAppSecret());
query.put("appid", wechatConfigProperties.getAppId());
try {
String aTokenUrl = wechatConfigProperties.getATokenUrl();
// ResponseEntity<JSONObject> responseEntity = restTemplate.postForEntity(aTokenUrl, query, JSONObject.class);
ResponseEntity<JSONObject> responseEntity = restTemplate.getForEntity(aTokenUrl, JSONObject.class, query);
HttpStatus statusCode = responseEntity.getStatusCode(); //状态码
// System.out.println(responseEntity.getHeaders());//获取到头信息
/*校验: 如果接口成功 200*/
if (Objects.equals(statusCode.value(), 200)) {
JSONObject responseJsonBody = responseEntity.getBody();//响应体
log.info("[请求微信小程序官方接口] => 获取accessToken请求成功返回值:{}", responseJsonBody);
if (responseJsonBody == null) {
log.info("微信小程序获取accessToken请求返回result是null!");
throw new ServiceException(ResponseEnum.A10005);
}
//获取accessToken
String accessToken = responseJsonBody.getString("access_token");
if (StringUtils.isBlank(accessToken)) {
log.info("微信小程序获取accessToken请求返回access_token是null!");
throw new ServiceException(ResponseEnum.A10005);
}
//放入缓存中
redisCache.setCacheObject(CacheConstants.WX_ACCESS_TOKEN, accessToken, wechatConfigProperties.getExpiredTime(), TimeUnit.MINUTES);
return accessToken;
} else {
log.error("微信HttpStatus的StatusCode不是200 {}", statusCode.value());
throw new ServiceException(ResponseEnum.A10005);
}
} catch (Exception e) {
e.printStackTrace();
log.info("微信小程序获取accessToken请求异常信息 {}", e.getMessage());
throw new ServiceException(ResponseEnum.A10005);
}
}
/**
* 错误码 错误描述 解决方案
* -1 system error [系统繁忙,此时请开发者稍候再试]
* 40029 code 无效 [js_code无效]
* 45011 api minute-quota reach limit mustslower retry [next minute API 调用太频繁,请稍候再试]
* 40013 invalid appid [请求appid身份与获取code的小程序appid不匹配]
* 错误码
*
* @param code js_code
* @return phone
*/
public String getPhoneByCode(String code) {
String phoneUrl = wechatConfigProperties.getPhoneUrl(getRedisCacheAccessToken());
Map<String, Object> map = new HashMap<>();
map.put("code", code);
JSONObject jsonObject = sendPostRestTemplate(phoneUrl, map, JSONObject.class);
System.out.println(jsonObject);
if (jsonObject.containsKey("errcode")) {
/*如果异常码是0 说明正常*/
if (!Objects.equals(String.valueOf(jsonObject.get("errcode")), "0")) {
log.error("===> 获取手机号的异常信息 : {}", jsonObject + "");
throw new ServiceException("获取失败: " + jsonObject.get("errmsg"), (Integer) jsonObject.get("errcode"));
}
}
JSONObject phoneInfo = jsonObject.getJSONObject("phone_info");
return phoneInfo.getString("phoneNumber");
}
/**
* 生成小程序带参数二维码
* https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/qr-code/wxacode.getUnlimited.html#HTTPS%20%E8%B0%83%E7%94%A8
*/
@SneakyThrows
public InputStream getUnlimitedWxQrCode(WxCodeUnlimitedReqDTO wxCodeUnlimitedReqDTO, String accessToken) {
Map<String, Object> params = new HashMap<>();
params.put("scene", wxCodeUnlimitedReqDTO.getScene());
params.put("page", wxCodeUnlimitedReqDTO.getPage());
params.put("path", wxCodeUnlimitedReqDTO.getPage());
params.put("env_version", wxCodeUnlimitedReqDTO.getEnvVersion());
params.put("width", wxCodeUnlimitedReqDTO.getWidth());
params.put("auto_color", wxCodeUnlimitedReqDTO.getAutoColor());//自动配置线条颜色
ResponseEntity<byte[]> response = restTemplate.postForEntity(wechatConfigProperties.getWxACodeUnLimitUrl(accessToken), JSON.toJSONString(params), byte[].class);
System.out.println(JSON.toJSONString(params));
byte[] buffer = response.getBody();
// System.out.println(Base64.getEncoder().encodeToString(buffer));
assert buffer != null;
return new ByteArrayInputStream(buffer);
}
/**
* 远程调用 restTemplate方法 post请求
*
* @param url
* @param body
* @return
*/
public <T> T sendPostRestTemplate(String url, Map<String, Object> body, Class<T> responseType) {
return restTemplate.exchange(url, HttpMethod.POST, new HttpEntity<>(body, null), responseType).getBody();
}
}
2.2.2 创建RedisCache
redis的
增删改查
操作 记得配置序列化与反序列化
文章传送门: ===> redis高级(序列化与反序列化) <===
@Component
public class RedisCache {
@Autowired
public RedisTemplate redisTemplate;
/**
* 缓存基本的对象,Integer、String、实体类等
*
* @param key 缓存的键值
* @param value 缓存的值
*/
public <T> void setCacheObject(final String key, final T value) {
redisTemplate.opsForValue().set(key, value);
}
/**
* 缓存基本的对象,Integer、String、实体类等
*
* @param key 缓存的键值
* @param value 缓存的值
* @param timeout 时间
* @param timeUnit 时间颗粒度
*/
public <T> void setCacheObject(final String key, final T value, final Integer timeout, final TimeUnit timeUnit) {
redisTemplate.opsForValue().set(key, value, timeout, timeUnit);
}
/**
* 设置有效时间
*
* @param key Redis键
* @param timeout 超时时间
* @return true=设置成功;false=设置失败
*/
public boolean expire(final String key, final long timeout) {
return expire(key, timeout, TimeUnit.SECONDS);
}
/**
* 设置有效时间
*
* @param key Redis键
* @param timeout 超时时间
* @param unit 时间单位
* @return true=设置成功;false=设置失败
*/
public boolean expire(final String key, final long timeout, final TimeUnit unit) {
return Boolean.TRUE.equals(redisTemplate.expire(key, timeout, unit));
}
/**
* 获取有效时间
*
* @param key Redis键
* @return 有效时间
*/
public long getExpire(final String key) {
if (StringUtils.isBlank(key)) {
throw new IllegalArgumentException("key cannot be null");
}
return redisTemplate.getExpire(key);
}
/**
* 判断 key是否存在
*
* @param key 键
* @return true 存在 false不存在
*/
public Boolean hasKey(String key) {
return redisTemplate.hasKey(key);
}
/**
* 获得缓存的基本对象。
*
* @param key 缓存键值
* @return 缓存键值对应的数据
*/
public <T> T getCacheObject(final String key) {
ValueOperations<String, T> operation = redisTemplate.opsForValue();
return operation.get(key);
}
/**
* 删除单个对象
*
* @param key
*/
public boolean deleteObject(final String key) {
return Boolean.TRUE.equals(redisTemplate.delete(key));
}
/**
* 删除集合对象
*
* @param collection 多个对象
* @return
*/
public boolean deleteObject(final Collection collection) {
return redisTemplate.delete(collection) > 0;
}
/**
* 缓存List数据
*
* @param key 缓存的键值
* @param dataList 待缓存的List数据
* @return 缓存的对象
*/
public <T> long setCacheList(final String key, final List<T> dataList) {
Long count = redisTemplate.opsForList().rightPushAll(key, dataList);
return count == null ? 0 : count;
}
/**
* 获得缓存的list对象
*
* @param key 缓存的键值
* @return 缓存键值对应的数据
*/
public <T> List<T> getCacheList(final String key) {
return redisTemplate.opsForList().range(key, 0, -1);
}
/**
* 缓存Set
*
* @param key 缓存键值
* @param dataSet 缓存的数据
* @return 缓存数据的对象
*/
public <T> BoundSetOperations<String, T> setCacheSet(final String key, final Set<T> dataSet) {
BoundSetOperations<String, T> setOperation = redisTemplate.boundSetOps(key);
Iterator<T> it = dataSet.iterator();
while (it.hasNext()) {
setOperation.add(it.next());
}
return setOperation;
}
/**
* 获得缓存的set
*
* @param key
* @return
*/
public <T> Set<T> getCacheSet(final String key) {
return redisTemplate.opsForSet().members(key);
}
/**
* 缓存Map
*
* @param key
* @param dataMap
*/
public <T> void setCacheMap(final String key, final Map<String, T> dataMap) {
if (dataMap != null) {
redisTemplate.opsForHash().putAll(key, dataMap);
}
}
/**
* 获得缓存的Map
*
* @param key
* @return
*/
public <T> Map<String, T> getCacheMap(final String key) {
return redisTemplate.opsForHash().entries(key);
}
/**
* 往Hash中存入数据
*
* @param key Redis键
* @param hKey Hash键
* @param value 值
*/
public <T> void setCacheMapValue(final String key, final String hKey, final T value) {
redisTemplate.opsForHash().put(key, hKey, value);
}
/**
* 获取Hash中的数据
*
* @param key Redis键
* @param hKey Hash键
* @return Hash中的对象
*/
public <T> T getCacheMapValue(final String key, final String hKey) {
HashOperations<String, String, T> opsForHash = redisTemplate.opsForHash();
return opsForHash.get(key, hKey);
}
/**
* 获取多个Hash中的数据
*
* @param key Redis键
* @param hKeys Hash键集合
* @return Hash对象集合
*/
public <T> List<T> getMultiCacheMapValue(final String key, final Collection<Object> hKeys) {
return redisTemplate.opsForHash().multiGet(key, hKeys);
}
/**
* 删除Hash中的某条数据
*
* @param key Redis键
* @param hKey Hash键
* @return 是否成功
*/
public boolean deleteCacheMapValue(final String key, final String hKey) {
return redisTemplate.opsForHash().delete(key, hKey) > 0;
}
/**
* 获得缓存的基本对象列表(全部的key)
*
* @param pattern 字符串前缀
* @return 对象列表
*/
public Collection<String> keys(final String pattern) {
return redisTemplate.keys(pattern);
}
}
2.2.3 AESUtils工具类
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.binary.Base64;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import javax.validation.constraints.NotNull;
import java.security.Security;
import java.security.spec.AlgorithmParameterSpec;
/**
* @author pzy
* @description ok
* @version 0.1.0
*/
@Slf4j
public class AESUtils {
// 加密模式
private static final String ALGORITHM = "AES/CBC/PKCS7Padding";
private static final String CHARSET_NAME = "UTF-8";
private static final String AES_NAME = "AES";
//解决java.security.NoSuchAlgorithmException: Cannot find any provider supporting AES/CBC/PKCS7Padding
static {
Security.addProvider(new BouncyCastleProvider());
}
/**
* 解密
*
* @param content 目标密文
* @param key 秘钥
* @param iv 偏移量
* @return 解密信息
*/
public static String decrypt(@NotNull String content, @NotNull String key, @NotNull String iv) {
try {
Cipher cipher = Cipher.getInstance(ALGORITHM);
byte[] sessionKey = java.util.Base64.getDecoder().decode(key);
SecretKeySpec keySpec = new SecretKeySpec(sessionKey, AES_NAME);
byte[] ivByte = java.util.Base64.getDecoder().decode(iv);
AlgorithmParameterSpec paramSpec = new IvParameterSpec(ivByte);
cipher.init(Cipher.DECRYPT_MODE, keySpec, paramSpec);
return new String(cipher.doFinal(Base64.decodeBase64(content)), CHARSET_NAME);
} catch (Exception e) {
log.error("解密失败:{}", e);
e.printStackTrace();
}
return StringUtils.EMPTY;
}
public static void main(String[] args) {
//接口传入的参数
String sessionKey = "";
String encryptedData = "";
String iv = "";
String decrypt = AESUtils.decrypt(encryptedData, sessionKey, iv);
System.out.println("解密后:" + decrypt);
}
}
2.3 各个功能传送门
2.3.1 获取accessToken
官方文档地址
: => 获取接口调用凭据 <=
文章传送门
: => 微信小程序01: springboot获取accessToken方式 <=
2.3.2 获取手机号
官方文档地址
: => 获取手机号 <=
文章传送门
: => 微信小程序02: 使用微信快速验证组件code获取手机号
2.3.3 获取小程序二维码(不限制)
官方文档地址
: => 获取不限制的小程序码 <=
文章传送门
:
2.3.4 获取openId与unionId
官方文档地址
: => 小程序登录 <=
文章传送门
:
3. 文章的总结
3.1 本文总结
本文使用的技术栈
springboot
相关操作restTemplate
远程调用使用方式- redisTemplate
操作redis
的操作- redis的
序列化与反序列化
3.2 本文统一说明
本篇涵盖大部分的微信小程序操作(无支付), 统一封装
在2.3
中统一传送到具体功能配置
避免多次配置,传送门内的功能细节不在这篇介绍
特别注意
: 文章传送门会在近期完善
, 这是本专栏的第一篇
, 之后也会围绕此篇进行更新, 接入支付等等
作者: pingzhuyan