前言
先介绍一下项目场景,主要是跨境收款服务通过微信小程序二维码裂变分享,每个账号有专属邀请二维码,分享出去,有新人扫码入驻,就可以得到优惠卷。会更丰富,不止有裂变模式,核心实现都是生成二维码。对于如何生成微信小程序二维码
官网
获取不限制的小程序码
接口应在服务器端调用,详细说明参见服务端API。
接口说明
接口英文名
getUnlimitedQRCode
功能描述
该接口用于获取小程序码,适用于需要的码数量极多的业务场景。通过该接口生成的小程序码,永久有效,数量暂无限制。 更多用法详见 获取小程序码。
注意事项
- 如果调用成功,会直接返回图片二进制内容,如果请求失败,会返回 JSON 格式的数据。
- POST 参数需要转成 JSON 字符串,不支持 form 表单提交。
- 调用分钟频率受限(5000次/分钟),如需大量小程序码,建议预生成
获取 scene 值
- scene 字段的值会作为 query 参数传递给小程序/小游戏。用户扫描该码进入小程序/小游戏后,开发者可以获取到二维码中的 scene 值,再做处理逻辑。
- 调试阶段可以使用开发工具的条件编译自定义参数 scene=xxxx 进行模拟,开发工具模拟时的 scene 的参数值需要进行 encodeURIComponent
小程序
Page({
onLoad (query) {
// scene 需要使用 decodeURIComponent 才能获取到生成二维码时传入的 scene
const scene = decodeURIComponent(query.scene)
}
})
小游戏
// 在首次启动时通过 wx.getLaunchOptionsSync 接口获取
const {query} = wx.getLaunchOptionsSync()
const scene = decodeURIComponent(query.scene)
// 或者在 wx.onShow 事件中获取
wx.onShow(function ({query}) {
// scene 需要使用 decodeURIComponent 才能获取到生成二维码时传入的 scene
const scene = decodeURIComponent(query.scene)
})
调用方式
HTTPS 调用
POST https://api.weixin.qq.com/wxa/getwxacodeunlimit?access_token=ACCESS_TOKEN
第三方调用
- 调用方式以及出入参和HTTPS相同,仅是调用的token不同
- 该接口所属的权限集id为:17、58
- 服务商获得其中之一权限集授权后,可通过使用authorizer_access_token代商家进行调用
请求参数
| 属性 | 类型 | 必填 | 说明 | |||||
|---|---|---|---|---|---|---|---|---|
| ** | access_token | string | 是 | 接口调用凭证,该参数为 URL 参数,非 Body 参数。使用getAccessToken 或者 authorizer_access_token | ||||
| ** | scene | string | 是 | 最大32个可见字符,只支持数字,大小写英文以及部分特殊字符:!#$&'()*+,/:;=?@-._~,其它字符请自行编码为合法字符(因不支持%,中文无法使用 urlencode 处理,请使用其他编码方式) | ||||
| ** | page | string | 否 | 默认是主页,页面 page,例如 pages/index/index,根路径前不要填加 /,不能携带参数(参数请放在scene字段里),如果不填写这个字段,默认跳主页面。scancode_time为系统保留参数,不允许配置 | ||||
| ** | check_path | bool | 否 | 默认是true,检查page 是否存在,为 true 时 page 必须是已经发布的小程序存在的页面(否则报错);为 false 时允许小程序未发布或者 page 不存在, 但page 有数量上限(60000个)请勿滥用。 | ||||
| ** | env_version | string | 否 | 要打开的小程序版本。正式版为 "release",体验版为 "trial",开发版为 "develop"。默认是正式版。 | ||||
| ** | width | number | 否 | 默认430,二维码的宽度,单位 px,最小 280px,最大 1280px | ||||
| ** | auto_color | bool | 否 | 自动配置线条颜色,如果颜色依然是黑色,则说明不建议配置主色调,默认 false | ||||
| ** | line_color | object | 否 | 默认是{"r":0,"g":0,"b":0} 。auto_color 为 false 时生效,使用 rgb 设置颜色 例如 {"r":"xxx","g":"xxx","b":"xxx"} 十进制表示 | ||||
| ** | 属性 | 类型 | 必填 | 说明 | ||||
| - | -- | -- | -- | -- | ||||
| ** | is_hyaline | bool | 否 | 默认是false,是否需要透明底色,为 true 时,生成透明底色的小程序 |
返回参数
| 属性 | 类型 | 说明 |
|---|---|---|
| buffer | buffer | 图片 Buffer |
| errcode | number | 错误码 |
| errmsg | string | 错误信息 |
调用示例
示例说明: HTTPS调用
请求数据示例
{
"page": "pages/index/index",
"scene": "a=1",
"check_path": true,
"env_version": "release"
}
返回数据示例
图片二进制
示例说明: 云函数调用
请求数据示例
const cloud = require('wx-server-sdk')
cloud.init({
env: cloud.DYNAMIC_CURRENT_ENV,
})
exports.main = async (event, context) => {
try {
const result = await cloud.openapi.wxacode.getUnlimited({
"page": 'pages/index/index',
"scene": 'a=1',
"checkPath": true,
"envVersion": 'release'
})
return result
} catch (err) {
return err
}
}
返回数据示例
图片二进制
错误码
| 错误码 | 错误码取值 | 解决方案 |
|---|---|---|
| -1 | system error | 系统繁忙,此时请开发者稍候再试 |
| 40001 | invalid credential access_token isinvalid or not latest | 获取 access_token 时 AppSecret 错误,或者 access_token 无效。请开发者认真比对 AppSecret 的正确性,或查看是否正在为恰当的公众号调用接口 |
| 40129 | invalid scene | 最大32个可见字符,只支持数字,大小写英文以及部分特殊字符:!#$&'()*+,/:;=?@-._~,其它字符请自行编码为合法字符(因不支持%,中文无法使用 urlencode 处理,请使用其他编码方式) |
| 41030 | invalid page | page路径不正确:根路径前不要填加 /,不能携带参数(参数请放在scene字段里),需要保证在现网版本小程序中存在,与app.json保持一致。 设置check_path=false可不检查page参数。 |
| 85096 | not allow include scancode_time field | scancode_time为系统保留参数,不允许配置 |
| 40097 | invalid args | 参数错误 |
| 40169 | invalid length for scene or thedata is not json string | scene 不合法 |
获取稳定版接口调用凭据
接口应在服务器端调用,详细说明参见服务端API。
接口说明
接口英文名
getStableAccessToken
功能描述
- 获取小程序全局后台接口调用凭据,有效期最长为7200s,开发者需要进行妥善保存;
- 有两种调用模式: 1. 普通模式,
access_token有效期内重复调用该接口不会更新access_token,绝大部分场景下使用该模式;2. 强制刷新模式,会导致上次获取的access_token失效,并返回新的access_token; - 该接口调用频率限制为 1万次 每分钟,每天限制调用 50万 次;
- 与getAccessToken获取的调用凭证完全隔离,互不影响。该接口仅支持
POST JSON形式的调用; - 如使用云开发,可通过云调用免维护
access_token调用; - 如使用云托管,也可以通过微信令牌/开放接口服务免维护
access_token调用;
调用方式
HTTPS 调用
POST https://api.weixin.qq.com/cgi-bin/stable_token
请求参数
| 属性 | 类型 | 必填 | 说明 |
|---|---|---|---|
| grant_type | string | 是 | 填写 client_credential |
| appid | string | 是 | 账号唯一凭证,即 AppID,可在「微信公众平台 - 设置 - 开发设置」页中获得。(需要已经成为开发者,且账号没有异常状态) |
| secret | string | 是 | 账号唯一凭证密钥,即 AppSecret,获取方式同 appid |
| force_refresh | boolean | 否 | 默认使用 false。1. force_refresh = false 时为普通调用模式,access_token 有效期内重复调用该接口不会更新 access_token;2. 当force_refresh = true 时为强制刷新模式,会导致上次获取的 access_token 失效,并返回新的 access_token |
返回参数
| 属性 | 类型 | 说明 |
|---|---|---|
| access_token | string | 获取到的凭证 |
| expires_in | number | 凭证有效时间,单位:秒。目前是7200秒之内的值。 |
其他说明
access_token 的存储与更新
access_token的存储空间至少要保留 512 个字符;- 建议开发者仅在
access_token泄漏时使用强制刷新模式,该模式限制每天20次。考虑到数据安全,连续使用该模式时,请保证调用时间隔至少为30s,否则不会刷新; - 在普通模式调用下,平台会提前5分钟更新
access_token,即在有效期倒计时5分钟内发起调用会获取新的access_token。在新旧access_token交接之际,平台会保证在5分钟内,新旧access_token都可用,这保证了用户业务的平滑过渡; 根据此特性可知,任意时刻发起调用获取到的access_token有效期至少为 5 分钟,即expires_in>= 300;
最佳实践
- 在使用getAccessToken时,平台建议开发者使用中控服务来统一获取和刷新
access_token。有此成熟方案的开发者依然可以复用方案并通过普通模式来调用本接口,另外请将发起接口调用的时机设置为上次获取到的access_token有效期倒计时5分钟之内即可; - 根据以上特性,为减少其他开发者构建中控服务的开发成本,在普通调用模式下,平台建议开发者将每次获取的
access_token在本地建立中心化存储使用,无须考虑并行调用接口时导致意外情况发生,仅须保证至少每5分钟发起一次调用并覆盖本地存储。同时,该方案也支持各业务独立部署使用,即无须中心化存储也可以保证服务正常运行;
access_token 泄漏紧急处理
- 使用强制刷新模式以间隔30s发起两次调用可将已经泄漏的
access_token立即失效,同时正常的业务请求可能会返回错误码40001(access_token过期),请妥善使用该策略。其次,需要立即排查泄漏原因,加以修正,必要时可以考虑重置appsecret;
调用示例
示例说明: 普通模式,获取当前有效调用凭证
请求数据示例
POST https://api.weixin.qq.com/cgi-bin/stable_token
请求示例1(不传递force_refresh,默认值为false):
{
"grant_type": "client_credential",
"appid": "APPID",
"secret": "APPSECRET"
}
请求示例2(设置force_refresh为false):
{
"grant_type": "client_credential",
"appid": "APPID",
"secret": "APPSECRET",
"force_refresh": false
}
返回数据示例
返回示例1:
{
"access_token":"ACCESS_TOKEN",
"expires_in":7200
}
返回示例2:
{
"access_token":"ACCESS_TOKEN",
"expires_in":345
}
示例说明: 强制刷新模式,慎用,连续使用需要至少间隔30s
请求数据示例
POST https://api.weixin.qq.com/cgi-bin/stable_token
{
"grant_type": "client_credential",
"appid": "APPID",
"secret": "APPSECRET",
"force_refresh": true
}
返回数据示例
{
"access_token":"ACCESS_TOKEN",
"expires_in":7200
}
错误码
| 错误码 | 错误码取值 | 解决方案 |
|---|---|---|
| -1 | system error | 系统繁忙,此时请开发者稍候再试 |
| 0 | ok | ok |
| 40002 | invalid grant_type | 不合法的凭证类型 |
| 40013 | invalid appid | 不合法的 AppID ,请开发者检查 AppID 的正确性,避免异常字符,注意大小写 |
| 40125 | invalid appsecret | 无效的appsecret,请检查appsecret的正确性 |
| 40164 | invalid ip not in whitelist | 将ip添加到ip白名单列表即可 |
| 41002 | appid missing | 缺少 appid 参数 |
| 41004 | appsecret missing | 缺少 secret 参数 |
| 43002 | require POST method | 需要 POST 请求 |
| 45009 | reach max api daily quota limit | 调用超过天级别频率限制。可调用clear_quota接口恢复调用额度。 |
| 45011 | api minute-quota reach limit mustslower retry next minute | API 调用太频繁,请稍候再试 |
| 89503 | 此次调用需要管理员确认,请耐心等候 | |
| 89506 | 该IP调用求请求已被公众号管理员拒绝,请24小时后再试,建议调用前与管理员沟通确认 | |
| 89507 | 该IP调用求请求已被公众号管理员拒绝,请1小时后再试,建议调用前与管理员沟通确认 |
代码展示
package com.cogo.user.service.wechat;
import cn.vbill.middleware.oss.sdk.config.response.OssResMessage;
import com.cogo.common.util.ResponseUtil;
import com.cogo.common.util.UUIDUtils;
import com.cogo.user.common.config.WechatConfig;
import com.cogo.user.common.exception.BusinessException;
import com.cogo.user.common.util.OssSdkUtil;
import com.cogo.user.common.util.RSAUtil;
import com.cogo.user.dto.wechat.WechatFile;
import com.cogo.user.dto.wechat.WechatParamRedis;
import com.cogo.user.dto.wechat.WechatSession;
import com.cogo.user.dto.wechat.WechatToken;
import com.cogo.user.dto.wechat.WxCodeErrResp;
import com.cogo.user.response.wechat.WechatResp;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.suixingpay.ace.common.json.JsonUtil;
import com.suixingpay.ace.data.api.Response;
import com.suixingpay.ace.redis.IRedisOperater;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.protocol.HTTP;
import org.apache.http.util.EntityUtils;
import org.bouncycastle.util.encoders.Base64;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import javax.annotation.Resource;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
/**
* @author 86156
* @project cogo-user
* @date 2024-02-21
* @desc 微信小程序:https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/qrcode-link/qr-code/getUnlimitedQRCode.html
*/
@Service
@Slf4j
public class WechatService {
@Resource
private WechatConfig wechatConfig;
@Resource
private OssSdkUtil ossSdkUtil;
@Resource
private IRedisOperater iRedisOperater;
private final static String WECHAT_PARAM = "applet_WeChat_param:";
private final static String WECHAT_FILE = "applet_WeChat_file:";
private final static String WECHAT_TOKEN = "applet_WeChat_token:";
private static final String DEFAULT_CHARSET = "UTF-8";
private static final String CONTENT_TYPE = "application/json";
/**
* 获取微信token
*
* @return
* @throws
*/
private String getAccessToken(String key) {
String token = iRedisOperater.get(WECHAT_TOKEN + key);
if (StringUtils.isNotBlank(token)) {
return token;
}
WechatToken wechatToken = new WechatToken();
wechatToken.setGrant_type(wechatConfig.getGrant_type());
wechatToken.setAppid(wechatConfig.getWxAppId());
wechatToken.setSecret(wechatConfig.getWxSecret());
String json = JsonUtil.objectToJson(wechatToken);
String response = null;
try {
response = post(wechatConfig.getVxTokenUrl(), json);
} catch (Exception e) {
log.error("获取微信公众号Access_token异常e:{}", e.getMessage());
throw new BusinessException("获取微信公众号Access_token异常!");
}
log.info("获取微信公众号原始数据:{}", response);
WechatSession wechatSession = JsonUtil.jsonToObject(response, WechatSession.class);
log.info("获取微信公众号token信息:{}", wechatSession);
iRedisOperater.setex(WECHAT_TOKEN + key, wechatSession.getAccess_token(), 120 * 60);
return wechatSession.getAccess_token();
}
public String post(String url, String json) {
CloseableHttpClient httpClient = HttpClients.createDefault();
HttpPost httpPost = new HttpPost(url);
String responseBody = null;
try {
// 设置请求体,这里以JSON字符串为例
StringEntity entity = new StringEntity(json, DEFAULT_CHARSET);
entity.setContentType(CONTENT_TYPE);
entity.setContentEncoding(DEFAULT_CHARSET);
httpPost.setEntity(entity);
// 执行请求
HttpResponse response = httpClient.execute(httpPost);
// 打印响应内容
responseBody = EntityUtils.toString(response.getEntity());
return responseBody;
} catch (Exception e) {
log.error("获取小程序token请求异常", e);
} finally {
try {
httpClient.close();
} catch (IOException e) {
log.error("获取小程序token请求异常", e);
}
return responseBody;
}
}
/**
* 附件上传
*
* @param key
* @param multipartFile
* @return
*/
public Response uploadFile(String key, MultipartFile multipartFile, String fileName) {
//解密
key = RSAUtil.decrypt(key);
String object = iRedisOperater.get(WECHAT_PARAM + key);
log.info("小程序附件上传key:{},获取redis:{}", key, object);
if (StringUtils.isBlank(object)) {
return ResponseUtil.fail("小程序附件上传key过期!");
}
WechatParamRedis wechatRedis = JsonUtil.jsonToObject(object, WechatParamRedis.class);
Map<String, String> map = wechatRedis.getMap();
//图片格式
String imgAccept = map.get("imgAccept");
//除了图片的文件格式
String fileAccept = map.get("fileAccept");
//文件大小
Integer maxSize = Integer.parseInt(map.get("maxSize"));
//文件格式
List<String> list = new ArrayList<>();
List<String> imgAccepts = Arrays.asList(imgAccept.split(","));
List<String> fileAccepts = Arrays.asList(fileAccept.split(","));
list.addAll(fileAccepts);
list.addAll(imgAccepts);
// //附件名称
// String fileName = multipartFile.getOriginalFilename();
//附件大小
long fileSize = multipartFile.getSize();
log.info("key:{}上传文件名称:{};文件大小:{}", key, fileName, fileSize);
// 校验文件后缀
String prefix = fileName.substring(fileName.lastIndexOf(".") + 1);
if (StringUtils.isBlank(prefix)) {
return ResponseUtil.fail(fileName + "文件格式不支持");
}
prefix = prefix.toLowerCase();
//文件格式
if (!list.contains(prefix)) {
return ResponseUtil.fail(fileName + "文件格式不支持");
}
// 文件大小校验
if (maxSize * 1024 * 1024 < fileSize) {
return ResponseUtil.fail(fileName + "文件过大");
}
OssResMessage ossResMessage = null;
try {
ossResMessage = ossSdkUtil.uploadFile(multipartFile.getInputStream(), fileName);
} catch (Exception e) {
log.error("文件异常", e);
return ResponseUtil.fail(fileName + "上传失败");
}
if (null == ossResMessage || !ossResMessage.getSuccess()) {
return ResponseUtil.fail(fileName + "上传失败");
}
String url = ossResMessage.getPublicCdnHttp();
List<String> files = new ArrayList<>();
WechatFile wechatFile = WechatFile.builder().fileName(fileName).url(url).build();
files.add(JsonUtil.objectToJson(wechatFile));
iRedisOperater.lpush(WECHAT_FILE + key, files);
return Response.success(url);
}
/**
* 获取小程序码
*
* @param
* @return
* @throws Exception
*/
public Response getQRCode(Map<String, String> map) {
String key = map.get("key");
if (StringUtils.isBlank(key)) {
key = UUIDUtils.getUUID();
//获取accessToken
String token = getAccessToken(key);
String url = String.format(wechatConfig.getUnlimitedQRCodeURL(), token);
Map<String, Object> body = new HashMap<>();
// 场景码,与前端约定,最终是需要前端解析
body.put("scene", key);
// 正式版为 "release",体验版为 "trial",开发版为 "develop"。默认是正式版。
body.put("env_version", wechatConfig.getEnv_version());
// 默认是主页,页面 page,例如 pages/index/index,根路径前不要填加 /
body.put("page", map.get("page"));
// 默认是true,检查page 是否存在,为 true 时 page 必须是已经发布的小程序存在的页面(否则报错);为 false 时允许小程序未发布或者 page 不存在, 但page 有数量上限(60000个)请勿滥用。
// body.put("check_path", Boolean.FALSE);
body.put("check_path", Boolean.valueOf(wechatConfig.getCheck_path()));
byte[] bytes = getBytes(url, body);
String base = "data:image/png;base64," + Base64.toBase64String(bytes);
WechatParamRedis wechatRedis = WechatParamRedis.builder().key(key).map(map).code(base).build();
iRedisOperater.setex(WECHAT_PARAM + key, JsonUtil.objectToJson(wechatRedis), 60 * 10);
return Response.success(WechatResp.builder().code(base).key(key).map(map).build());
}
String object = iRedisOperater.get(WECHAT_PARAM + key);
if (StringUtils.isBlank(object)) {
log.info("生成小程序码过期重新生成1");
//获取accessToken
String token = getAccessToken(key);
String url = String.format(wechatConfig.getUnlimitedQRCodeURL(), token);
Map<String, Object> body = new HashMap<>();
// 场景码,与前端约定,最终是需要前端解析
body.put("scene", key);
// 正式版为 "release",体验版为 "trial",开发版为 "develop"。默认是正式版。
body.put("env_version", wechatConfig.getEnv_version());
// 默认是主页,页面 page,例如 pages/index/index,根路径前不要填加 /
body.put("page", map.get("page"));
// 默认是true,检查page 是否存在,为 true 时 page 必须是已经发布的小程序存在的页面(否则报错);为 false 时允许小程序未发布或者 page 不存在, 但page 有数量上限(60000个)请勿滥用。
// body.put("check_path", Boolean.FALSE);
body.put("check_path", Boolean.valueOf(wechatConfig.getCheck_path()));
byte[] bytes = getBytes(url, body);
String base = "data:image/png;base64," + Base64.toBase64String(bytes);
WechatParamRedis wechatRedis = WechatParamRedis.builder().key(key).map(map).code(base).build();
iRedisOperater.setex(WECHAT_PARAM + key, JsonUtil.objectToJson(wechatRedis), 60 * 10);
return Response.success(WechatResp.builder().code(base).key(key).map(map).build());
}
WechatParamRedis redis = JsonUtil.jsonToObject(object, WechatParamRedis.class);
if (Objects.isNull(redis)) {
log.info("生成小程序码过期重新生成2");
key = UUIDUtils.getUUID();
//获取accessToken
String token = getAccessToken(key);
String url = String.format(wechatConfig.getUnlimitedQRCodeURL(), token);
Map<String, Object> body = new HashMap<>();
// 场景码,与前端约定,最终是需要前端解析
body.put("scene", key);
// 正式版为 "release",体验版为 "trial",开发版为 "develop"。默认是正式版。
body.put("env_version", wechatConfig.getEnv_version());
// 默认是主页,页面 page,例如 pages/index/index,根路径前不要填加 /
body.put("page", map.get("page"));
// 默认是true,检查page 是否存在,为 true 时 page 必须是已经发布的小程序存在的页面(否则报错);为 false 时允许小程序未发布或者 page 不存在, 但page 有数量上限(60000个)请勿滥用。
// body.put("check_path", Boolean.FALSE);
body.put("check_path", Boolean.valueOf(wechatConfig.getCheck_path()));
byte[] bytes = getBytes(url, body);
String base = "data:image/png;base64," + Base64.toBase64String(bytes);
WechatParamRedis wechatRedis = WechatParamRedis.builder().key(key).map(map).code(base).build();
iRedisOperater.setex(WECHAT_PARAM + key, JsonUtil.objectToJson(wechatRedis), 60 * 10);
return Response.success(WechatResp.builder().code(base).key(key).map(map).build());
}
return Response.success(WechatResp.builder().code(redis.getCode()).key(redis.getKey()).map(redis.getMap()).build());
}
/**
* 获取图片字节流
*
* @param url
* @param body
* @return
*/
private byte[] getBytes(String url, Map<String, Object> body) {
byte[] bytes = null;
try {
CloseableHttpResponse response = null;
try {
ObjectMapper objectMapper = new ObjectMapper();
HttpPost httpPost = new HttpPost(url);
httpPost.setHeader(HTTP.CONTENT_TYPE, "application/json");
httpPost.setEntity(new StringEntity(objectMapper.writeValueAsString(body),
ContentType.create("text/json", "UTF-8")));
response = HttpClients.createDefault().execute(httpPost);
String contentType = response.getHeaders("Content-Type")[0].getValue();
if (response.getStatusLine().getStatusCode() == 200) {
HttpEntity entity = response.getEntity();
if ("image/jpeg".equals(contentType)) {
if (entity != null) {
//这里不一定需要转为byte,根据你的项目自己选择
bytes = readStream(entity.getContent());
}
} else {
String content = EntityUtils.toString(entity, "UTF-8");
WxCodeErrResp errResp = JsonUtil.jsonToObject(content, WxCodeErrResp.class);
if (errResp.getErrcode() != null) {
if (errResp.getErrcode().equals("42001")) {
String token = getRefreshAccessToken((String) body.get("scene"));
String refreshUrl = String.format(wechatConfig.getUnlimitedQRCodeURL(), token);
Map<String, Object> refreshBody = new HashMap<>();
// 场景码,与前端约定,最终是需要前端解析
refreshBody.put("scene", body.get("scene"));
// 正式版为 "release",体验版为 "trial",开发版为 "develop"。默认是正式版。
refreshBody.put("env_version", wechatConfig.getEnv_version());
// 默认是主页,页面 page,例如 pages/index/index,根路径前不要填加 /
refreshBody.put("page", body.get("page"));
// 默认是true,检查page 是否存在,为 true 时 page 必须是已经发布的小程序存在的页面(否则报错);为 false 时允许小程序未发布或者 page 不存在, 但page 有数量上限(60000个)请勿滥用。
refreshBody.put("check_path", Boolean.valueOf(wechatConfig.getCheck_path()));
bytes = getBytes(refreshUrl, refreshBody);
}
}
}
}
} finally {
if (response != null) {
response.close();
}
}
} catch (Exception e) {
log.error("调用getWxCode Exception,body:{},错误信息:{}", body, e);
}
return bytes;
}
/**
* 获取附件
*
* @param key
* @return
*/
public Response getFile(String key) {
//解密
key = RSAUtil.decrypt(key);
Long l = iRedisOperater.llen(WECHAT_FILE + key);
if (Objects.isNull(l)) {
return Response.success();
}
if (l.intValue() == 0) {
return Response.success();
}
List<WechatFile> files = new ArrayList<>();
for (int a = 1; a <= l.intValue(); a++) {
List<WechatFile> list = iRedisOperater.lpop(WECHAT_FILE + key);
files.addAll(list);
}
log.info("获取附件数量:{},移除数量:{},附件:{}", files.size(), files.size(), files);
return Response.success(files);
}
/**
* 获取参数
*
* @param key
* @return
*/
public Response getWechatParams(String key) {
//解密
key = RSAUtil.decrypt(key);
String object = iRedisOperater.get(WECHAT_PARAM + key);
log.info("获取小程序附件key:{},获取redis:{}", key, object);
WechatParamRedis wechatRedis = new WechatParamRedis();
if (StringUtils.isBlank(object)) {
return Response.failOf("获取参数key过期!");
}
wechatRedis = JsonUtil.jsonToObject(object, WechatParamRedis.class);
return Response.success(wechatRedis);
}
/**
* 得到图片字节流 数组大小
*/
public static byte[] readStream(InputStream inStream) throws Exception {
ByteArrayOutputStream outStream = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int len = -1;
while ((len = inStream.read(buffer)) != -1) {
outStream.write(buffer, 0, len);
}
outStream.close();
inStream.close();
return outStream.toByteArray();
}
/**
* 重新获取微信token
*
* @return
* @throws
*/
private String getRefreshAccessToken(String key) {
WechatToken wechatToken = new WechatToken();
wechatToken.setGrant_type(wechatConfig.getGrant_type());
wechatToken.setAppid(wechatConfig.getWxAppId());
wechatToken.setSecret(wechatConfig.getWxSecret());
String json = JsonUtil.objectToJson(wechatToken);
String response = null;
try {
response = post(wechatConfig.getVxTokenUrl(), json);
} catch (Exception e) {
log.error("获取微信公众号Access_token异常e:{}", e.getMessage());
throw new BusinessException("获取微信公众号Access_token异常!");
}
log.info("获取微信公众号原始数据:{}", response);
WechatSession wechatSession = JsonUtil.jsonToObject(response, WechatSession.class);
log.info("获取微信公众号token信息:{}", wechatSession);
iRedisOperater.setex(WECHAT_TOKEN + key, wechatSession.getAccess_token(), 120 * 60);
return wechatSession.getAccess_token();
}
}
#配置
package com.cogo.user.common.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
* @author 86156
* @project cogo-user
* @date 2024-02-21
* @desc 微信小程序:https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/qrcode-link/qr-code/getUnlimitedQRCode.html
*/
@Data
@Component
@ConfigurationProperties(prefix = "wechat")
public class WechatConfig {
//微信开发者wxAppId
private String wxAppId;
//微信开发者wxSecret
private String wxSecret;
//微信获取token路径
private String vxTokenUrl;
//获取微信不限制小程序码路径
private String unlimitedQRCodeURL;
//正式版为 "release",体验版为 "trial",开发版为 "develop"。默认是正式版。
private String env_version;
// 默认是true,检查page 是否存在,为 true 时 page 必须是已经发布的小程序存在的页面(否则报错)
//为 false 时允许小程序未发布或者 page 不存在, 但page 有数量上限(60000个)请勿滥用。
private String check_path;
//填写 client_credential
private String grant_type;
}
package com.cogo.user.common.util;
/**
* @program: cross-border-service
* @description:
* @author: qin_cong@suixingpay.com
* @create: 2019-08-05 09:54
**/
import lombok.extern.slf4j.Slf4j;
import org.apache.http.HttpEntity;
import org.apache.http.HttpMessage;
import org.apache.http.NameValuePair;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpRequestBase;
import org.apache.http.client.utils.URLEncodedUtils;
import org.apache.http.config.SocketConfig;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.DefaultConnectionReuseStrategy;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.DefaultConnectionKeepAliveStrategy;
import org.apache.http.impl.client.DefaultHttpRequestRetryHandler;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.message.BasicHeader;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.util.EntityUtils;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.TimeUnit;
/**
* @program: cbmsc
* @description:
* @author: qin_cong@suixingpay.com
* @create: 2019-08-02 16:48
**/
@Slf4j
public class HttpClientUtil {
private static final String DEFAULT_CHARSET = "UTF-8";
private static final String QUESTION_CHAR = "?";
private static final String AND_CHAR = "&";
private static final RequestConfig config = RequestConfig.custom()
// 设置从连接池获取连接的等待超时时间
.setConnectionRequestTimeout(10000)
// 设置连接超时间
.setConnectTimeout(40000)
// 设置等待数据超时间时间
.setSocketTimeout(40000)
//
.build();
private static final SocketConfig socketConfig = SocketConfig.custom().setTcpNoDelay(true).build();
private static final CloseableHttpClient client = HttpClientBuilder.create()
//
.setDefaultSocketConfig(socketConfig)
// 设置整个连接池的最大连接数
.setMaxConnTotal(1000)
// 每个路由最大连接数
.setMaxConnPerRoute(200)
//
.setDefaultRequestConfig(config)
// 连接池不是共享模式
.setConnectionManagerShared(false)
// 连接重用策略,即是否能keepAlive
.setConnectionReuseStrategy(DefaultConnectionReuseStrategy.INSTANCE)
// 长连接配置,即获取长连接生产多长时间, 返回值小于等于0时,会无限期保持
.setKeepAliveStrategy(DefaultConnectionKeepAliveStrategy.INSTANCE)
//.setSSLHostnameVerifier((a, b) -> true)
// 定期回收空闲连接
.evictIdleConnections(300, TimeUnit.SECONDS)
// 定期回收过期连接
.evictExpiredConnections()
// 连接存活时间,如果不设置,则根据长连接信息决定
.setConnectionTimeToLive(300, TimeUnit.SECONDS)
// 设置重试次数,默认是3次,当前是禁用掉
.setRetryHandler(new DefaultHttpRequestRetryHandler(0, false))
.build();
static {
// 程序关闭时关闭连接池
Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {
@Override
public void run() {
try {
client.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}));
}
/**
* 设置http headers
*
* @param message
* @param headers
*/
private static void setHeaders(HttpMessage message, Map<String, String> headers) {
if (message == null) {
return;
}
if (headers != null && !headers.isEmpty()) {
for (Entry<String, String> entry : headers.entrySet()) {
message.addHeader(new BasicHeader(entry.getKey(), entry.getValue()));
}
}
// message.setHeader(HTTP.CONN_KEEP_ALIVE, String.valueOf(TIME_TO_LIVE));
}
/**
* 执行请求
*
* @param request
* @return 返回执行结果
* @throws IOException
*/
private static String execute(HttpRequestBase request) throws IOException {
try (CloseableHttpResponse response = client.execute(request);) {
HttpEntity he = response.getEntity();
if (he != null && he.isStreaming()) {
return EntityUtils.toString(he, DEFAULT_CHARSET);
}
} catch (IOException e) {
log.error(e.getMessage(), e);
throw e;
}
return null;
}
private static List<NameValuePair> toNameValuePairList(Map<String, String> parameters) {
List<NameValuePair> pairs = null;
if (null != parameters) {
pairs = new ArrayList<>(parameters.size());
for (Entry<String, String> entry : parameters.entrySet()) {
NameValuePair p = new BasicNameValuePair(entry.getKey(), entry.getValue());
pairs.add(p);
}
}
return pairs;
}
public static String post(String url, String msg) throws IOException {
return post(url, msg, null);
}
/**
* POST 请求
*
* @param url
* @param msg
* @param headers
* @return
* @throws IOException
*/
public static String post(String url, String msg, Map<String, String> headers) throws IOException {
HttpPost request = new HttpPost(url);
setHeaders(request, headers);
HttpEntity entity = new StringEntity(msg, DEFAULT_CHARSET);
request.setEntity(entity);
return execute(request);
}
public static String post(String url, Map<String, String> parameters) throws IOException {
return post(url, parameters, null);
}
/**
* POST 请求
*
* @param url
* @param parameters 表单参数
* @param headers
* @return
* @throws IOException
*/
public static String post(String url, Map<String, String> parameters, Map<String, String> headers)
throws IOException {
HttpPost request = new HttpPost(url);
setHeaders(request, headers);
List<NameValuePair> pairs = toNameValuePairList(parameters);
if (null != pairs && !pairs.isEmpty()) {
UrlEncodedFormEntity entity = new UrlEncodedFormEntity(pairs, DEFAULT_CHARSET);
request.setEntity(entity);
}
return execute(request);
}
public static String get(String url)
throws IOException {
return get(url, null, null);
}
/**
* GET 请求
*
* @param url
* @param parameters
* @param headers
* @return
* @throws IOException
*/
public static String get(String url, Map<String, String> parameters, Map<String, String> headers)
throws IOException {
List<NameValuePair> pairs = toNameValuePairList(parameters);
String params = null;
if (null != pairs && !pairs.isEmpty()) {
params = URLEncodedUtils.format(pairs, DEFAULT_CHARSET);
}
StringBuilder urlBuilder = new StringBuilder(url);
if (null != params && !params.isEmpty()) {
if (url.contains(QUESTION_CHAR)) {
urlBuilder.append(AND_CHAR);
} else {
urlBuilder.append(QUESTION_CHAR);
}
urlBuilder.append(params);
}
HttpGet request = new HttpGet(urlBuilder.toString());
setHeaders(request, headers);
return execute(request);
}
}
package com.cogo.user.common.util;
import com.cogo.user.common.exception.RsaException;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.binary.Base64;
import javax.crypto.Cipher;
import java.security.Key;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.HashMap;
import java.util.Map;
@Slf4j
public class RSAUtil {
/**
* 前端
* 公钥
*/
public static final String GCFS_REMOTE_PUBLIC_KEY =
/**
* 前端
* 私钥
*/
private static final String GCFS_NATIVE_PRIVATE_KEY =
/**
* 后端
* 公钥
*/
public static final String GLCS_REMOTE_PUBLIC_KEY =
/**
* 后端
* 私钥
*/
public static final String GLCS_NATIVE_PRIVATE_KEY =
/**
* 加密方式
*/
public static final String RSA_ALGORITHM = "RSA";
/**
* 加密
*
* @param message
* @return
*/
public static String encrypt(String message) {
try {
log.info("message:{}, 开始加密!", message);
Cipher cipher = Cipher.getInstance("RSA");
byte[] keyBytes = Base64.decodeBase64(GCFS_REMOTE_PUBLIC_KEY);
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(keyBytes);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
PublicKey key = keyFactory.generatePublic(keySpec);
cipher.init(Cipher.ENCRYPT_MODE, key);
byte[] bytes = cipher.doFinal(message.getBytes());
String s = Base64.encodeBase64String(bytes);
return s;
} catch (Exception e) {
e.printStackTrace();
log.error("message:{},加密失败,原因:{}", message, e.getMessage());
throw new RsaException(e.getMessage());
}
}
public static String encrypt(String message,String secretKey) {
try {
log.info("message:{}, 开始加密!", message);
Cipher cipher = Cipher.getInstance("RSA");
byte[] keyBytes = Base64.decodeBase64(secretKey);
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(keyBytes);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
PublicKey key = keyFactory.generatePublic(keySpec);
cipher.init(Cipher.ENCRYPT_MODE, key);
byte[] bytes = cipher.doFinal(message.getBytes());
String s = Base64.encodeBase64String(bytes);
return s;
} catch (Exception e) {
e.printStackTrace();
log.error("message:{},加密失败,原因:{}", message, e.getMessage());
throw new RsaException(e.getMessage());
}
}
/**
* 解密
*
* @param message
* @return
*/
public static String decrypt(String message) {
try {
log.info("message:{}, 开始解密!", message);
byte[] content = message.getBytes();
Cipher cipher = Cipher.getInstance("RSA");
byte[] keyBytes = Base64.decodeBase64(GLCS_NATIVE_PRIVATE_KEY);
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(keyBytes);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
PrivateKey key = keyFactory.generatePrivate(keySpec);
cipher.init(Cipher.DECRYPT_MODE, key);
byte[] responseByte = cipher.doFinal(Base64.decodeBase64(content));
String s = new String(responseByte);
return s;
} catch (Exception e) {
e.printStackTrace();
log.error("解密失败",e);
log.error("message:{},解密失败,原因:{}", message, e.getMessage());
throw new RsaException(e.getMessage());
}
}
public static String decrypt(String message,String secretKey) {
try {
log.info("message:{}, 开始解密!", message);
byte[] content = message.getBytes();
Cipher cipher = Cipher.getInstance("RSA");
byte[] keyBytes = Base64.decodeBase64(secretKey);
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(keyBytes);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
PrivateKey key = keyFactory.generatePrivate(keySpec);
cipher.init(Cipher.DECRYPT_MODE, key);
byte[] responseByte = cipher.doFinal(Base64.decodeBase64(content));
String s = new String(responseByte);
return s;
} catch (Exception e) {
e.printStackTrace();
log.error("解密失败",e);
log.error("message:{},解密失败,原因:{}", message, e.getMessage());
throw new RsaException(e.getMessage());
}
}
/**
* @Description 获取公钥key和私钥key
* @Param keySize初始化长度
* @Return
* @Author wen_jf@suixingpay.com
* @Date 2019/3/19 17:13
**/
public static Map<String, String> createKeys(int keySize) {
//为RSA算法创建一个KeyPairGenerator对象
KeyPairGenerator kpg;
try {
kpg = KeyPairGenerator.getInstance(RSA_ALGORITHM);
} catch (NoSuchAlgorithmException e) {
throw new IllegalArgumentException("No such algorithm-->[" + RSA_ALGORITHM + "]");
}
//初始化KeyPairGenerator对象,密钥长度
kpg.initialize(keySize);
//生成密匙对
KeyPair keyPair = kpg.generateKeyPair();
//得到公钥
Key publicKey = keyPair.getPublic();
String publicKeyStr = Base64.encodeBase64String(publicKey.getEncoded());
//得到私钥
Key privateKey = keyPair.getPrivate();
String privateKeyStr = Base64.encodeBase64String(privateKey.getEncoded());
Map<String, String> keyPairMap = new HashMap<String, String>();
keyPairMap.put("publicKey", publicKeyStr);
keyPairMap.put("privateKey", privateKeyStr);
return keyPairMap;
}
// public static void main(String[] args) {
// System.out.println(createKeys(1024));
//
//// String encrypt = RSAUtil.encrypt("cbmp");
//// System.out.println(encrypt);
//// String decrypt = RSAUtil.decrypt(encrypt);
//// System.out.println(decrypt);
//
// }
}
package com.cogo.user.dto.wechat;
import lombok.Data;
/**
* @author 86156
* @project cogo-user
* @date 2024-03-21
* @desc token
*/
@Data
public class WechatToken {
private String grant_type;
private String appid;
private String secret;
private String force_refresh;
}
package com.cogo.user.dto.wechat;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* @author 86156
* @project cogo-user
* @date 2024-02-22
* @desc 微信二维码
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class WechatCode {
/**
* 小程序生成二维码地址:
* https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/qrcode-link/qr-code/getUnlimitedQRCode.html
*/
//默认是主页,页面 page,例如 pages/index/index,根路径前不要填加 /,不能携带参数
// (参数请放在scene字段里),如果不填写这个字段,默认跳主页面。
// scancode_time为系统保留参数,不允许配置
@ApiModelProperty("嵌套二维码页面")
private String page;
//最大32个可见字符,只支持数字,大小写英文以及部分特殊字符:!#$&'()*+,/:;=?@-._~,
// 其它字符请自行编码为合法字符(因不支持%,中文无法使用 urlencode 处理,请使用其他编码方式)
@ApiModelProperty("最大32个可见字符,只支持数字,大小写英文以及部分特殊字符:!#$&'()*+,/:;=?@-._~")
private String scene;
//默认是true,检查page 是否存在,为 true 时 page 必须是已经发布的小程序存在的页面(否则报错);
// 为 false 时允许小程序未发布或者 page 不存在, 但page 有数量上限(60000个)请勿滥用
@ApiModelProperty("检查page是否存在 true:存在 false:不存在")
private Boolean check_path;
@ApiModelProperty("正式版为:release,体验版为:trial,开发版为:develop")
private String env_version;
//默认430,二维码的宽度,单位 px,最小 280px,最大 1280px
@ApiModelProperty("二维码的宽度")
private Integer width;
//自动配置线条颜色,如果颜色依然是黑色,则说明不建议配置主色调,默认 false
@ApiModelProperty("自动配置线条颜色")
private Boolean auto_color;
//默认是{"r":0,"g":0,"b":0} 。auto_color 为 false 时生效,
// 使用 rgb 设置颜色 例如 {"r":"xxx","g":"xxx","b":"xxx"} 十进制表示
@ApiModelProperty("设置颜色")
private Object line_color;
//默认是false,是否需要透明底色,为 true 时,生成透明底色的小程序
@ApiModelProperty("是否需要透明底色")
private Boolean is_hyaline;
}
package com.cogo.user.dto.wechat;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* @author 86156
* @project cogo-user
* @date 2024-02-22
* @desc 微信附件
*/
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class WechatFile {
@ApiModelProperty("file路径")
private String url;
@ApiModelProperty("文件名称")
private String fileName;
}
package com.cogo.user.dto.wechat;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
import java.util.Map;
/**
* @author 86156
* @project cogo-user
* @date 2024-02-22
* @desc 微信二维码
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class WechatParamRedis {
@ApiModelProperty("key")
private String key;
@ApiModelProperty("参数")
private Map<String, String> map;
@ApiModelProperty("二维码")
private String code;
}
package com.cogo.user.dto.wechat;
import lombok.Data;
/**
* @author 86156
* @project cogo-user
* @date 2024-02-21
* @desc token
*/
@Data
public class WechatSession {
private String expires_in;
private String access_token;
}
package com.cogo.user.dto.wechat;
import lombok.Data;
/**
* @author 86156
* @project cogo-user
* @date 2024-02-26
* @desc
*/
@Data
public class WxCodeErrResp {
private String errcode;
private String errmsg;
}
JDK自带的URLConnection方式
package com.cogo.user.service.wechat;
import com.alibaba.fastjson.JSONObject;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.io.PrintWriter;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.Map;
/**
* @author 86156
* @project cogo-user
* @date 2024-02-28
* @desc
*/
public class UrlConnectionUtils {
public byte[] getWechatQrcodeByHttpURL(String url, Map<String, Object> body) {
HttpURLConnection httpURLConnection = null;
try {
httpURLConnection = (HttpURLConnection) new URL(url).openConnection();
httpURLConnection.setRequestMethod("POST");
// 发送POST请求必须设置如下两行
httpURLConnection.setDoOutput(true);
httpURLConnection.setDoInput(true);
// 获取URLConnection对象对应的输出流
PrintWriter printWriter = new PrintWriter(httpURLConnection.getOutputStream());
// 发送请求参数
printWriter.write(JSONObject.toJSONString(body));
// flush输出流的缓冲
printWriter.flush();
//开始获取数据
try (InputStream inputStream = httpURLConnection.getInputStream();
ByteArrayOutputStream out = new ByteArrayOutputStream()) {
byte[] buffer = new byte[1024];
int len = -1;
while ((len = inputStream.read(buffer)) != -1) {
out.write(buffer, 0, len);
}
return out.toByteArray();
}
} catch (Exception e) {
e.printStackTrace();
} finally {
if (httpURLConnection != null) {
httpURLConnection.disconnect();
}
}
return null;
}
}
okhttp3方式
package com.cogo.user.service.wechat;
import com.alibaba.fastjson.JSONObject;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import java.io.IOException;
import java.util.Map;
/**
* @author 86156
* @project cogo-user
* @date 2024-02-28
* @desc
*/
public class Okhttp3Utils {
public byte[] getWechatQrcodeByOkhttp3(String url, Map<String, Object> body) {
OkHttpClient client = new OkHttpClient().newBuilder().build();
okhttp3.MediaType mediaType = okhttp3.MediaType.parse("application/json");
RequestBody requestBody = RequestBody.create(mediaType, JSONObject.toJSONString(body));
Request request = new Request.Builder().url(url).method("POST", requestBody).build();
try {
Response response = client.newCall(request).execute();
if (response.isSuccessful()) {
return response.body().bytes();
}
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
}
Maven依赖:
<dependency>
<groupId>com.konghq</groupId>
<artifactId>unirest-java</artifactId>
<version>3.14.4</version>
</dependency>
RestTemplate方式
package com.cogo.user.service.wechat;
import com.suixingpay.ace.common.json.JsonUtil;
import lombok.SneakyThrows;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.web.client.RestTemplate;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
/**
* @author 86156
* @project cogo-user
* @date 2024-02-28
* @desc
*/
public class RestTemplateUtils {
@SneakyThrows
public static void main(String[] args) {
Map<String, String> param = new HashMap<>();
param.put("scene", "a=1");
RestTemplate restTemplate = new RestTemplate();
HttpHeaders headers = new HttpHeaders();
headers.add("Accept", "image/jpeg");
String url = "https://api.weixin.qq.com/wxa/getwxacodeunlimit?access_token%s";
ResponseEntity<byte[]> entity = restTemplate.exchange(url, HttpMethod.POST,
new HttpEntity<String>(JsonUtil.objectToJson(param), headers), byte[].class, new Object[0]);
if (entity.getStatusCode().is2xxSuccessful()) {
byte[] data = entity.getBody();
try (ByteArrayInputStream inputStream = new ByteArrayInputStream(data);
FileOutputStream fos = new FileOutputStream(new File("D:\Users\23569\Downloads\output.jpg"))) {
int len = 0;
byte[] buf = new byte[1024];
while ((len = inputStream.read(buf, 0, 1024)) != -1) {
fos.write(buf, 0, len);
}
fos.flush();
} catch (IOException e) {
e.printStackTrace();
}
} else {
System.out.println("Failed to download image");
}
}
}
使用AppSecret重置 API 调用次数
接口应在服务器端调用,详细说明参见服务端API。
接口说明
接口英文名
clearQuotaByAppSecret
功能描述
本接口用于清空公众号/小程序等接口的每日调用接口次数
注意事项
1、该接口通过appsecret调用,解决了accesss_token耗尽无法调用重置 API 调用次数 的情况
2、每个账号每月使用重置 API 调用次数 与本接口共10次清零操作机会,清零生效一次即用掉一次机会;
3、由于指标计算方法或统计时间差异,实时调用量数据可能会出现误差,一般在1%以内
4、该接口仅支持POST调用
5、如果要清除getComponentAccessToken调用次数或者以服务商身份代商家清除公众号或者小程序调用次数,则需要使用clearComponentQuotaByAppSecret
调用方式
HTTPS 调用
POST https://api.weixin.qq.com/cgi-bin/clear_quota/v2
请求参数
| 属性 | 类型 | 必填 | 说明 |
|---|---|---|---|
| appid | string | 是 | 要被清空的账号的appid |
| appsecret | string | 是 | 唯一凭证密钥,即 AppSecret,获取方式同 appid |
返回参数
| 属性 | 类型 | 说明 |
|---|---|---|
| errcode | number | 错误码 |
| errmsg | string | 错误信息 |
调用示例
示例说明: post请求
请求数据示例
POST https://api.weixin.qq.com/cgi-bin/clear_quota/v2?appid=wx888888888888&appsecret=xxxxxxxxxxxxxxxxxxxxxxxx
返回数据示例
{
"errcode": 0,
"errmsg": "ok"
}
错误码
| 错误码 | 错误码取值 | 解决方案 |
|---|---|---|
| -1 | system error | 系统繁忙,此时请开发者稍候再试 |
| 40013 | invalid appid | 不合法的 AppID ,请开发者检查 AppID 的正确性,避免异常字符,注意大小写 |
| 41004 | appsecret missing | 缺少 secret 参数 |
| 41002 | appid missing | 缺少 appid 参数 |
| 48006 | forbid to clear quota because of reaching the limit | api 禁止清零调用次数,因为清零次数达到上限 |
注意事项
1.access_token的获取有效期7200秒必须缓存--accesss_token调用次数有限制\
2.接口未走鉴权,后端给前段返回一个key 前段加密key取值等逻辑--具体看业务逻辑及设计
3.oss公司封装的,直接传byte[]即可,非常方便!
4.清除命令 curl -X POST 'api.weixin.qq.com/cgi-bin/cle…'