All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.hn.utils.weixin.miniprogram.WxMiniProgram Maven / Gradle / Ivy

There is a newer version: 1.0.18
Show newest version
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; } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy