com.hn.utils.weixin.miniprogram.WxMiniProgram Maven / Gradle / Ivy
package com.hn.utils.weixin.miniprogram;
import cn.hutool.cache.CacheUtil;
import cn.hutool.cache.impl.TimedCache;
import cn.hutool.core.date.DateUnit;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.ContentType;
import cn.hutool.http.HttpRequest;
import cn.hutool.http.HttpResponse;
import cn.hutool.http.HttpUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import cn.hutool.log.Log;
import cn.hutool.log.LogFactory;
import com.hn.config.HnConfigUtils;
import com.hn.config.exception.ConfigException;
import com.hn.utils.AssertUtils;
import com.hn.utils.weixin.exception.WxException;
import com.sun.org.apache.xerces.internal.impl.dv.util.Base64;
import lombok.Data;
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.security.spec.AlgorithmParameterSpec;
import java.util.HashMap;
import java.util.Map;
/**
* 描述: 微信小程序接口
*
* @author fei
*/
public class WxMiniProgram {
private static final Log log = LogFactory.get();
/**
* 配置前缀名
*/
public static final String CONFIG_KEY = "wx.smallProgram";
/**
* 登录凭证校验 url
*/
private static final String WEB_ACCESS_TOKEN_URL = "https://api.weixin.qq.com/sns/jscode2session";
/**
* 获取小程序全局唯一后台接口调用凭据 url
*/
private static final String ACCESS_TOKEN_URL = "https://api.weixin.qq.com/cgi-bin/token";
/**
* 获取小程序二维码,适用于需要的码数量较少的业务场景 url
*/
private static final String CREATE_WXAQRCODE_URL = "https://api.weixin.qq.com/cgi-bin/wxaapp/createwxaqrcode?access_token=ACCESS_TOKEN";
/**
* 获取小程序码,适用于需要的码数量较少的业务场景 url
*/
private static final String GET_WXACODE_URL = "https://api.weixin.qq.com/wxa/getwxacode?access_token=ACCESS_TOKEN";
/**
* 获取小程序码,适用于需要的码数量极多的业务场景
*/
private static final String GET_WXACODE_UNLIMIT_URL = "https://api.weixin.qq.com/wxa/getwxacodeunlimit?access_token=ACCESS_TOKEN";
/**
* access_token的有效期目前为2个小时
* 创建缓存,默认7200秒过期
*/
private TimedCache timedCache = CacheUtil.newTimedCache(7200 * DateUnit.SECOND.getMillis());
private Param param;
private WxMiniProgram(Param param) {
this.param = param;
}
public static WxMiniProgram create(Param param) {
return new WxMiniProgram(param);
}
public static WxMiniProgram create(String scene) {
return new WxMiniProgram(getRequestParam(scene));
}
public static WxMiniProgram create() {
return new WxMiniProgram(getRequestParam(""));
}
/**
* 登录凭证校验
* 通过 wx.login 接口获得临时登录凭证 code 后传到开发者服务器调用此接口完成登录流程
*
* @param jsCode 登录时获取的 code
* @return {@link Code2Session.Result}
*/
public Code2Session.Result code2Session(String jsCode) {
Map paramMap = param.toMap();
paramMap.put("js_code", jsCode);
// 授权类型,此处只需填写 authorization_code
paramMap.put("grant_type", "authorization_code");
String resultStr = HttpUtil.get(WEB_ACCESS_TOKEN_URL, paramMap);
Code2Session.Result result = JSONUtil.toBean(resultStr, Code2Session.Result.class);
log.info("[微信小程序]登录凭证校验 返回结果:{}", resultStr);
Integer errCode = result.getErrcode();
if (ObjectUtil.isNull(errCode)) {
return result;
}
AssertUtils.isTrue(Code2Session.ErrCode.SUCCESS.code.equals(errCode),
WxException.exception(errCode, result.getErrmsg()));
return result;
}
/**
* 获取小程序 accessToken
*
* @return accessToken
*/
public String getAccessToken() {
String appId = param.getAppid();
if (StrUtil.isNotBlank(timedCache.get(appId))) {
return timedCache.get(appId);
}
Map paramMap = param.toMap();
// 授权类型,此处只需填写 client_credential
paramMap.put("grant_type", "client_credential");
String resultStr = HttpUtil.get(ACCESS_TOKEN_URL, paramMap);
AccessToken.Result result = JSONUtil.toBean(resultStr, AccessToken.Result.class);
log.info("[微信小程序]获取小程序AccessToken 返回结果:{}", resultStr);
Integer errCode = result.getErrcode();
if (ObjectUtil.isNull(errCode)) {
timedCache.put(appId, result.getAccess_token());
return result.getAccess_token();
}
AssertUtils.isTrue(AccessToken.ErrCode.SUCCESS.code.equals(errCode),
WxException.exception(errCode, result.getErrmsg()));
timedCache.put(appId, result.getAccess_token());
return result.getAccess_token();
}
/**
* 获取手机号
*
* @param encryptData 加密的数据
* @param iv 偏移量
* @param sessionKey 会话密钥 {@link WxMiniProgram#code2Session(String)}
* @return String 手机号码
*/
public static String getPhoneNumber(String encryptData, String iv, String sessionKey) {
String result = decrypt(encryptData, iv, sessionKey);
JSONObject jsonObject = null;
try {
jsonObject = JSONUtil.parseObj(result);
} catch (Exception e) {
throw new WxException("数据解密失败:json解析失败");
}
return jsonObject.getStr("purePhoneNumber");
}
/**
* 解密数据
*
* @param encryptData 加密的数据
* @param iv 偏移量
* @param sessionKey 会话密钥 {@link WxMiniProgram#code2Session(String)}
* @return String 解密后的数据
*/
public static String decrypt(String encryptData, String iv, String sessionKey) {
try {
AlgorithmParameterSpec ivSpec = new IvParameterSpec(Base64.decode(iv));
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
SecretKeySpec keySpec = new SecretKeySpec(Base64.decode(sessionKey), "AES");
cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec);
// 解析解密后的字符串
return new String(cipher.doFinal(Base64.decode(encryptData)), "UTF-8");
} catch (Exception e) {
log.error(e.getMessage());
throw new WxException("数据解密失败");
}
}
public QRCode qrCode() {
return new QRCode();
}
/**
* 小程序二维码生成
*
* 注意事项
* 接口只能生成已发布的小程序的二维码
* 接口 A 加上接口 C,总共生成的码数量限制为 100,000,请谨慎调用。
* 接口 B 调用分钟频率受限(5000次/分钟),如需大量小程序码,建议预生成。
*/
public class QRCode {
/**
* 接口 A
* 获取小程序码,适用于需要的【码数量较少】的业务场景。通过该接口生成的小程序码,永久有效,有数量限制
*
* @param path 扫码进入的小程序页面路径,最大长度 128 字节,不能为空;对于小游戏,可以只传入 query 部分,来实现【传参效果】,
* 如:传入 "?foo=bar",即可在 wx.getLaunchOptionsSync 接口中的 query 参数获取到 {foo:"bar"}。
* @param width 二维码的宽度,单位 px。最小 280px,最大 1280px ,默认430
* @param savePath 图片保存路径
* @return 图片保存路径
*/
public String get(String path, Integer width, String savePath) {
return get(path, width, null, null, null, savePath);
}
/**
* 接口 A
* 获取小程序码,适用于需要的【码数量较少】的业务场景。通过该接口生成的小程序码,永久有效,有数量限制
*
* @param path 扫码进入的小程序页面路径,最大长度 128 字节,不能为空;对于小游戏,可以只传入 query 部分,来实现传参效果,
* 如:传入 "?foo=bar",即可在 wx.getLaunchOptionsSync 接口中的 query 参数获取到 {foo:"bar"}。
* @param width 二维码的宽度,单位 px。最小 280px,最大 1280px ,默认430
* @param autoColor 自动配置线条颜色,如果颜色依然是黑色,则说明不建议配置主色调,默认 false
* @param lineColor auto_color 为 false 时生效,使用 rgb 设置颜色 例如 {"r":"xxx","g":"xxx","b":"xxx"} 十进制表示
* @param hyaline 是否需要透明底色,为 true 时,生成透明底色的小程序
* @param savePath 图片保存路径
* @return savePath
*/
public String get(String path, Integer width,
Boolean autoColor, String lineColor, Boolean hyaline,
String savePath) {
String url = GET_WXACODE_URL.replace("ACCESS_TOKEN", getAccessToken());
Map paramMap = new HashMap<>();
// 扫码进入的小程序页面路径,最大长度 128 字节,不能为空
paramMap.put("path", path);
// 二维码的宽度,单位 px。最小 280px,最大 1280px ,默认430
paramMap.put("width", width);
// 自动配置线条颜色,如果颜色依然是黑色,则说明不建议配置主色调
paramMap.put("auto_color", autoColor);
// auto_color 为 false 时生效,使用 rgb 设置颜色 例如 {"r":"xxx","g":"xxx","b":"xxx"} 十进制表示
paramMap.put("line_color", lineColor);
// 是否需要透明底色,为 true 时,生成透明底色的小程序码
paramMap.put("is_hyaline", hyaline);
HttpResponse res = HttpRequest.post(url)
.contentType(ContentType.JSON.toString())
.body(JSONUtil.toJsonStr(paramMap)).execute();
JSONObject resultObject = null;
try {
resultObject = JSONUtil.parseObj(res.body());
} catch (Exception e) {
InputStream inputStream = res.bodyStream();
try {
IoUtil.copy(inputStream, new FileOutputStream(FileUtil.touch(savePath)));
log.info("[微信小程序]A获取小程序二维码 生成路径地址:{}", savePath);
} catch (FileNotFoundException e1) {
e1.printStackTrace();
}
}
if (resultObject == null) {
return savePath;
}
log.info("[微信小程序]A获取小程序二维码 返回结果:{}", res.body());
Integer errCode = resultObject.getInt("errcode");
if (ObjectUtil.isNull(errCode)) {
return savePath;
}
AssertUtils.isTrue(AccessToken.ErrCode.SUCCESS.code.equals(errCode),
WxException.exception(errCode, errCode.equals(45029) ? "生成码个数总和到达最大个数限制"
: resultObject.getStr("errmsg")));
return savePath;
}
/**
* 接口 A
* 获取小程序码,适用于需要的【码数量较少】的业务场景。通过该接口生成的小程序码,永久有效,有数量限制
*
* @param path 扫码进入的小程序页面路径,最大长度 128 字节,不能为空;对于小游戏,可以只传入 query 部分,来实现传参效果,
* 如:传入 "?foo=bar",即可在 wx.getLaunchOptionsSync 接口中的 query 参数获取到 {foo:"bar"}。
* @param width 二维码的宽度,单位 px。最小 280px,最大 1280px ,默认430
* @return savePath
*/
public InputStream get(String path, Integer width) {
String url = GET_WXACODE_URL.replace("ACCESS_TOKEN", getAccessToken());
Map paramMap = new HashMap<>();
// 扫码进入的小程序页面路径,最大长度 128 字节,不能为空
paramMap.put("path", path);
// 二维码的宽度,单位 px。最小 280px,最大 1280px ,默认430
paramMap.put("width", width);
// 自动配置线条颜色,如果颜色依然是黑色,则说明不建议配置主色调
// paramMap.put("auto_color", autoColor);
// auto_color 为 false 时生效,使用 rgb 设置颜色 例如 {"r":"xxx","g":"xxx","b":"xxx"} 十进制表示
// paramMap.put("line_color", lineColor);
// 是否需要透明底色,为 true 时,生成透明底色的小程序码
// paramMap.put("is_hyaline", hyaline);
HttpResponse res = HttpRequest.post(url)
.contentType(ContentType.JSON.toString())
.body(JSONUtil.toJsonStr(paramMap)).execute();
JSONObject resultObject = null;
try {
resultObject = JSONUtil.parseObj(res.body());
} catch (Exception e) {
InputStream inputStream = res.bodyStream();
return inputStream;
}
log.info("[微信小程序]A获取小程序二维码 返回结果:{}", res.body());
Integer errCode = resultObject.getInt("errcode");
AssertUtils.isTrue(AccessToken.ErrCode.SUCCESS.code.equals(errCode),
WxException.exception(errCode, errCode.equals(45029) ? "生成码个数总和到达最大个数限制"
: resultObject.getStr("errmsg")));
return null;
}
/**
* 接口 B
* 获取小程序码,适用于需要的【码数量极多】的业务场景。通过该接口生成的小程序码,永久有效,数量暂无限制
*
* @param scene 最大32个可见字符,只支持数字,大小写英文以及部分特殊字符:【参考https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/qr-code/wxacode.getUnlimited.html】,
* 其它字符请自行编码为合法字符(因不支持%,中文无法使用 urlencode 处理,请使用其他编码方式)
* scene 字段的值会作为 query 参数传递给小程序/小游戏。用户扫描该码进入小程序/小游戏后,开发者可以获取到二维码中的 scene 值,再做处理逻辑。
* @param page 必须是已经发布的小程序存在的页面(否则报错),例如 pages/index/index, 根路径前不要填加 /,
* 【不能携带参数(参数请放在scene字段里)】,如果不填写这个字段,默认跳主页面
* @param width 二维码的宽度,单位 px。最小 280px,最大 1280px ,默认430
* @param savePath 图片保存路径
* @return 图片保存路径
*/
public String getUnlimited(String scene, String page, Integer width, String savePath) {
return getUnlimited(scene, page, width, null, null, null, savePath);
}
/**
* 接口 B
* 获取小程序码,适用于需要的【码数量极多】的业务场景。通过该接口生成的小程序码,永久有效,数量暂无限制
*
* @param scene 最大32个可见字符,只支持数字,大小写英文以及部分特殊字符:【参考https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/qr-code/wxacode.getUnlimited.html】,
* 其它字符请自行编码为合法字符(因不支持%,中文无法使用 urlencode 处理,请使用其他编码方式)
* scene 字段的值会作为 query 参数传递给小程序/小游戏。用户扫描该码进入小程序/小游戏后,开发者可以获取到二维码中的 scene 值,再做处理逻辑。
* @param page 必须是已经发布的小程序存在的页面(否则报错),例如 pages/index/index, 根路径前不要填加 /,
* 【不能携带参数(参数请放在scene字段里)】,如果不填写这个字段,默认跳主页面
* @param width 二维码的宽度,单位 px。最小 280px,最大 1280px ,默认430
* @return 图片流
*/
public InputStream getUnlimited(String scene, String page, Integer width) {
return getUnlimited(scene, page, width, null, null, null);
}
/**
* 接口 B
* 获取小程序码,适用于需要的【码数量极多】的业务场景。通过该接口生成的小程序码,永久有效,数量暂无限制
* 示例: 为每个订单生成一个二维码、餐厅的每张餐桌生成一个二维码等
*
* @param scene 最大32个可见字符,只支持数字,大小写英文以及部分特殊字符:【参考https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/qr-code/wxacode.getUnlimited.html】
* 其它字符请自行编码为合法字符(因不支持%,中文无法使用 urlencode 处理,请使用其他编码方式)
* @param page 必须是已经发布的小程序存在的页面(否则报错),例如 pages/index/index, 根路径前不要填加 /,
* 不能携带参数(参数请放在scene字段里),如果不填写这个字段,默认跳主页面
* @param width 二维码的宽度,单位 px。最小 280px,最大 1280px ,默认430
* @param autoColor 自动配置线条颜色,如果颜色依然是黑色,则说明不建议配置主色调,默认 false
* @param lineColor auto_color 为 false 时生效,使用 rgb 设置颜色 例如 {"r":"xxx","g":"xxx","b":"xxx"} 十进制表示
* @param hyaline 是否需要透明底色,为 true 时,生成透明底色的小程序
* @param savePath 图片保存路径
* @return 图片保存路径
*/
public String getUnlimited(String scene, String page, Integer width,
Boolean autoColor, String lineColor, Boolean hyaline,
String savePath) {
InputStream inputStream = getUnlimited(scene, page, width, autoColor, lineColor, hyaline);
try {
IoUtil.copy(inputStream, new FileOutputStream(FileUtil.touch(savePath)));
log.info("[微信小程序]B获取小程序二维码 生成路径地址:{}", savePath);
} catch (FileNotFoundException e1) {
e1.printStackTrace();
}
return savePath;
}
/**
* 接口 B
* 获取小程序码,适用于需要的【码数量极多】的业务场景。通过该接口生成的小程序码,永久有效,数量暂无限制
* 示例: 为每个订单生成一个二维码、餐厅的每张餐桌生成一个二维码等
*
* @param scene 最大32个可见字符,只支持数字,大小写英文以及部分特殊字符:【参考https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/qr-code/wxacode.getUnlimited.html】
* 其它字符请自行编码为合法字符(因不支持%,中文无法使用 urlencode 处理,请使用其他编码方式)
* @param page 必须是已经发布的小程序存在的页面(否则报错),例如 pages/index/index, 根路径前不要填加 /,
* 不能携带参数(参数请放在scene字段里),如果不填写这个字段,默认跳主页面
* @param width 二维码的宽度,单位 px。最小 280px,最大 1280px ,默认430
* @param autoColor 自动配置线条颜色,如果颜色依然是黑色,则说明不建议配置主色调,默认 false
* @param lineColor auto_color 为 false 时生效,使用 rgb 设置颜色 例如 {"r":"xxx","g":"xxx","b":"xxx"} 十进制表示
* @param hyaline 是否需要透明底色,为 true 时,生成透明底色的小程序
* @return 图片流
*/
public InputStream getUnlimited(String scene, String page, Integer width,
Boolean autoColor, String lineColor, Boolean hyaline) {
String url = GET_WXACODE_UNLIMIT_URL.replace("ACCESS_TOKEN", getAccessToken());
Map paramMap = new HashMap<>(10);
paramMap.put("scene", scene);
paramMap.put("page", page);
paramMap.put("width", width);
paramMap.put("auto_color", autoColor);
paramMap.put("line_color", lineColor);
paramMap.put("is_hyaline", hyaline);
log.info("[微信小程序]B获取小程序二维码 请求参数:{}", JSONUtil.toJsonStr(paramMap));
HttpResponse res = HttpRequest.post(url)
.contentType(ContentType.JSON.toString())
.body(JSONUtil.toJsonStr(paramMap)).execute();
InputStream inputStream = res.bodyStream();
if (inputStream != null) {
return inputStream;
}
JSONObject resultObject = JSONUtil.parseObj(res.body());
log.info("[微信小程序]B获取小程序二维码 返回结果:{}", res.body());
Integer errCode = resultObject.getInt("errcode");
AssertUtils.isTrue(AccessToken.ErrCode.SUCCESS.code.equals(errCode),
WxException.exception(errCode, errCode.equals(41030) ? "所传page页面不存在,或者小程序没有发布"
: resultObject.getStr("errmsg")));
return null;
}
/**
* 接口 C
* 获取小程序二维码,适用于需要的【码数量较少】的业务场景。通过该接口生成的小程序码,永久有效,有数量限制
*
* @param path 扫码进入的小程序页面路径,最大长度 128 字节,不能为空;对于小游戏,可以只传入 query 部分,来实现【传参效果】,
* 如:传入 "?foo=bar",即可在 wx.getLaunchOptionsSync 接口中的 query 参数获取到 {foo:"bar"}。
* @param width 二维码的宽度,单位 px。最小 280px,最大 1280px ,默认430
* @param savePath 图片保存路径
* @return 图片保存路径
*/
public String createQRCode(String path, Integer width, String savePath) {
InputStream is = createQRCode(path, width);
try {
IoUtil.copy(is, new FileOutputStream(FileUtil.touch(savePath)));
log.info("[微信小程序]C获取小程序二维码 生成路径地址:{}", savePath);
} catch (FileNotFoundException e1) {
e1.printStackTrace();
}
return savePath;
}
/**
* 接口 C
* 获取小程序二维码,适用于需要的【码数量较少】的业务场景。通过该接口生成的小程序码,永久有效,有数量限制
*
* @param path 扫码进入的小程序页面路径,最大长度 128 字节,不能为空;对于小游戏,可以只传入 query 部分,来实现【传参效果】,
* 如:传入 "?foo=bar",即可在 wx.getLaunchOptionsSync 接口中的 query 参数获取到 {foo:"bar"}。
* @param width 二维码的宽度,单位 px。最小 280px,最大 1280px ,默认430
* @return 图片保存路径
*/
public InputStream createQRCode(String path, Integer width) {
String url = CREATE_WXAQRCODE_URL.replace("ACCESS_TOKEN", getAccessToken());
Map paramMap = new HashMap<>();
paramMap.put("path", path);
// 二维码的宽度,单位 px。最小 280px,最大 1280px ,默认430
paramMap.put("width", width);
HttpResponse res = HttpRequest.post(url)
.contentType(ContentType.JSON.toString())
.body(JSONUtil.toJsonStr(paramMap)).execute();
JSONObject resultObject = null;
try {
resultObject = JSONUtil.parseObj(res.body());
} catch (Exception e) {
return res.bodyStream();
}
log.info("[微信小程序]获取小程序二维码 返回结果:{}", res.body());
Integer errCode = resultObject.getInt("errcode");
if (ObjectUtil.isNotNull(errCode)) {
AssertUtils.isTrue(AccessToken.ErrCode.SUCCESS.code.equals(errCode),
WxException.exception(errCode, errCode.equals(45029) ? "生成码个数总和到达最大个数限制"
: resultObject.getStr("errmsg")));
}
return null;
}
}
/**
* 获取数据库配置参数
*
* @param scene 场景
* @return {@link Param}
*/
private static Param getRequestParam(String scene) {
if (StrUtil.isNotBlank(scene)) {
scene = "-".concat(scene);
}
String appId = AssertUtils.notNull(HnConfigUtils.getConfig(CONFIG_KEY.concat(scene).concat(".appId")),
ConfigException.exception("小程序appId未配置"));
String appSecret = AssertUtils.notNull(HnConfigUtils.getConfig(CONFIG_KEY.concat(scene).concat(".appSecret")),
ConfigException.exception("小程序appSecret未配置"));
Param param = new Param();
param.setAppid(appId);
param.setSecret(appSecret);
return param;
}
@Data
public static class Param {
/**
* 小程序唯一凭证 appId
*/
private String appid;
/**
* 小程序唯一凭证密钥 appSecret
*/
private String secret;
/**
* 参数转map
*
* @return Map
*/
public Map toMap() {
Map paramMap = new HashMap<>(3);
paramMap.put("appid", appid);
paramMap.put("secret", secret);
return paramMap;
}
}
}