微信小程序自动生成小程序码基于阿里云oss实现并解决多平台access_token失效问题
1、controller
/**
* @param path 必需参数,指定小程序码的路径
* @param width 小程序码的宽度,默认为430
* @param envVersion 可选参数,指定环境版本
* @description: 获取小程序码。该方法调用quesActivityService的getQRCode方法来生成小程序码,根据提供的路径(path)、宽度(width)和环境版本(envVersion)。
* @author: lizhiwei
* @date: 2023/12/15 16:10
* @return: 小程序码图片的Base64编码字符串
*/
@GetMapping("/getQRCode")
public BaseResponse<QRCodeVo> getQRCode(
@RequestParam(required = true) String path,
@RequestParam(defaultValue = "430") String width,
@RequestParam(required = false) String envVersion
) {
QRCodeVo qrCode = quesActivityService.getQRCode(path, width, envVersion);
return ResultUtils.success(qrCode);
}
2、业务层service
QRCodeVo getQRCode(String path, String width, String envVersion);
/**
* @param path 小程序页面路径,必传参数
* @param width 二维码宽度,有默认值
* @param envVersion 环境版本参数,非必传
* @description:获取微信小程序二维码并上传到阿里云OSS。
* @author: lizhiwei
* @date: 2023/12/18 10:08
* @return: 返回阿里云OSS中二维码的URL
**/
@Override
public QRCodeVo getQRCode(String path, String width, String envVersion) {
// 参数校验
if (StringUtils.isBlank(path) || StringUtils.isBlank(width)) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "路径或宽度参数不能为空");
}
//我这里是获取小程序的token(替换成自己的)
String accessToken = getToken.getMiniProgramAccessToken();
//我这里是获取小程序码的url(可以写死:https://api.weixin.qq.com/wxa/getwxacode)
String qrCodeUrl = wxConfig.getQRCodeUrl();
String url = qrCodeUrl + "?access_token=" + accessToken;
JSONObject params = new JSONObject();
params.put("path", path);
params.put("width", width);
if (StringUtils.isNotBlank(envVersion)) {
params.put("env_version", envVersion);
}
byte[] responseBytes;
try {
responseBytes = doPostAndHandleToken(url, params.toJSONString());
// 将二进制数据转换为Base64字符串
String base64Image = Base64.getEncoder().encodeToString(responseBytes);
try (InputStream inputStream = convertBase64ToInputStream(base64Image)) {
String ossUrl = uploadToAliyunOSS(inputStream);
if (StringUtils.isBlank(ossUrl)) {
throw new BusinessException(ErrorCode.SYSTEM_ERROR, "图片上传错误,请联系管理员");
}
return new QRCodeVo(ossUrl);
}
} catch (Exception e) {
log.error("记录日志:{}", e.getMessage());
throw new BusinessException(ErrorCode.SYSTEM_ERROR, "获取小程序码过程中发生错误");
}
}
//这里是检查多平台是否失效的问题,最后我解释一下怎么回事
private byte[] doPostAndHandleToken(String url, String jsonParams) throws IOException {
byte[] responseBytes = HttpClient.doPostJsonForBytes(url, jsonParams, null);
String responseBody = new String(responseBytes, StandardCharsets.UTF_8);
if (responseBody.startsWith("{") && responseBody.endsWith("}")) {
JSONObject jsonObject = JSONObject.parseObject(responseBody);
// 检查是否有错误码
if (jsonObject.containsKey("errcode")) {
int errcode = jsonObject.getIntValue("errcode");
// 根据具体的错误码处理
if (errcode == 40001 || errcode == 42001) {
// 重新获取access_token(这是我自己的方法,你可以换成自己的获取小程序的token方法,相信你有,因为这个不能老是获取,需要服务统一获取调配)
String newAccessToken = getToken.refreshMiniProgramAccessToken();
// 重新构建请求URL
String newUrl = wxConfig.getQRCodeUrl() + "?access_token=" + newAccessToken;
// 重发请求(网络请求的方法没有可以看看我的其他的帖子,有写)
responseBytes = HttpClient.doPostJsonForBytes(newUrl, jsonParams, null);
} else {
// 其他错误处理
throw new BusinessException(ErrorCode.SYSTEM_ERROR, "微信API错误:" + jsonObject.getString("errmsg"));
}
}
}
return responseBytes;
}
/**
* @param base64Str Base64编码的字符串
* @description:将Base64编码的字符串转换为InputStream。
* @author: lizhiwei
* @date: 2023/12/18 10:30
* @return: java.io.InputStream 转换后的InputStream
**/
public InputStream convertBase64ToInputStream(String base64Str) {
// 1. 参数校验:确保Base64编码不为空
if (StringUtils.isBlank(base64Str)) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "Base64编码为空");
}
// 2. Base64解码并转换为InputStream
byte[] bytes = Base64.getDecoder().decode(base64Str);
return new ByteArrayInputStream(bytes);
}
/**
* @param inputStream 文件输入流
* @description:上传文件到阿里云OSS。
* @author: lizhiwei
* @date: 2023/12/18 10:29
* @return: java.lang.String 返回文件在OSS上的URL
**/
private String uploadToAliyunOSS(InputStream inputStream) throws Exception {
// 参数校验:确保输入流不为空
if (inputStream == null) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "文件输入流不能为空");
}
// 1. 获取OSS配置参数
String accessKeyId = aliyunOssConfig.getAccessKeyId();
String accessKeySecret = aliyunOssConfig.getAccessKeySecret();
String bucketName = aliyunOssConfig.getBucketName();
String endPoint = aliyunOssConfig.getEndPoint();
// 2. 调用OSS上传工具上传文件
return ossUtils.fileUploadThroughBytes(inputStream, endPoint, bucketName, accessKeyId, accessKeySecret);
}
3、oss工具类方法
/**
* @param inputStream 文件的输入流
* @param endpoint OSS服务的EndPoint
* @param bucketName OSS的存储桶名称
* @param AccessKeyId 阿里云账号的Access Key ID
* @param AccessKeySecret 阿里云账号的Access Key Secret
* @description: 通过字节流上传文件到OSS。该方法接收文件流以及OSS相关的配置信息,
* 并进行文件上传。在上传之前,会检查必要的参数是否有效。
* @author: lizhiwei
* @date: 2023/12/15 14:00
* @return: 返回图片路径
*/
public String fileUploadThroughBytes(InputStream inputStream, String endpoint, String bucketName, String AccessKeyId, String AccessKeySecret) {
// 参数校验
if (inputStream == null) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "文件输入流不能为空");
}
if (StringUtils.isBlank(endpoint)) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "EndPoint不能为空");
}
if (StringUtils.isBlank(bucketName)) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "Bucket名称不能为空");
}
if (StringUtils.isBlank(AccessKeyId) || StringUtils.isBlank(AccessKeySecret)) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "阿里云账号、密钥为空");
}
Date now = new Date();
SimpleDateFormat sdf = new SimpleDateFormat("yyyyMM");
String folderName = sdf.format(now);
// 获取当前时间戳作为文件名(小程序码固定式jpg 这里就固定下来了)
String objectName = folderName + "/" + System.currentTimeMillis() + ".jpg";
// 创建OSSClient实例
OSS ossClient = null;
try {
ossClient = new OSSClientBuilder().build(endpoint, AccessKeyId, AccessKeySecret);
PutObjectResult result = ossClient.putObject(bucketName, objectName, inputStream);
if (result.getResponse() != null) {
log.error("上传失败:" + result.getResponse().getErrorResponseAsString());
throw new BusinessException(ErrorCode.SYSTEM_ERROR, "阿里云图片上传失败");
}
//返回成功上传图片的路径
return "https://" + bucketName + "." + URI.create(endpoint).getHost() + "/" + objectName;
} catch (Exception e) {
log.error("上传文件时发生错误:" + e.getMessage());
throw new BusinessException(ErrorCode.SYSTEM_ERROR, "上传文件时发生错误");
} finally {
if (ossClient != null) {
ossClient.shutdown();
}
}
}
4、总结思考
1、背景
为什么要解决多平台获取access_token? 因为如果线上正在使用合理期限的token,你此时接到开发任务,如果你没有多余的小程序作为测试开发使用的,你本地获取一下,那么线上的还在合理使用期的access_token 5分钟后就失效了,但是线上我们一般都采用redis 了,所以会引发问题,带一下官方的帖子,可以自行看一下。
https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/mp-access-token/getAccessToken.html
2、解决办法
因为我们没有一个专门测试的小程序,只有两个线上的,都在使用了,所以我们现在采用的办法就是:在每一个使用该方法的地方都多次验证一下,如果过期了重新获取一下新的access_token,因为本地获取也不是很频繁,所以采用这种方法来完成多环境都需要这个access_token的问题,如果你有更好的方法,欢迎来评论区讨论。