Files
shz-backend/ruoyi-bussiness/src/main/java/com/ruoyi/cms/service/WeChatMiniProgramApiClient.java

182 lines
6.3 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package com.ruoyi.cms.service;
import cn.hutool.http.HttpRequest;
import cn.hutool.http.HttpResponse;
import cn.hutool.http.HttpUtil;
import com.alibaba.fastjson2.JSONObject;
import com.ruoyi.common.exception.ServiceException;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
/**
* 微信小程序服务端接口客户端。
*
* <p>该客户端只负责单次远程调用,不记录 appSecret、access token 或微信响应中的敏感内容。</p>
*/
@Component
public class WeChatMiniProgramApiClient
{
private static final String ACCESS_TOKEN_URL = "https://api.weixin.qq.com/cgi-bin/token";
private static final String UNLIMITED_QR_CODE_URL =
"https://api.weixin.qq.com/wxa/getwxacodeunlimit?access_token=";
private static final int HTTP_TIMEOUT_MILLIS = 15000;
public AccessTokenResult getAccessToken(String appId, String appSecret)
{
Map<String, Object> params = new HashMap<>();
params.put("grant_type", "client_credential");
params.put("appid", appId);
params.put("secret", appSecret);
String body = HttpUtil.get(ACCESS_TOKEN_URL, params, HTTP_TIMEOUT_MILLIS);
JSONObject json = parseJson(body, "获取微信 access token 失败");
String accessToken = json.getString("access_token");
Integer expiresIn = json.getInteger("expires_in");
if (accessToken == null || accessToken.trim().isEmpty()) {
throw weChatError("获取微信 access token 失败", json);
}
return new AccessTokenResult(accessToken, expiresIn == null ? 7200 : expiresIn);
}
public QrCodeImage getUnlimitedQrCode(String accessToken, String scene, String page,
String envVersion)
{
JSONObject requestBody = new JSONObject();
requestBody.put("scene", scene);
requestBody.put("page", page);
// 新页面尚未随小程序版本发布时也允许先生成码;扫码进入配置指定的小程序版本。
requestBody.put("check_path", false);
requestBody.put("env_version", envVersion);
requestBody.put("width", 430);
HttpResponse response = null;
try {
response = HttpRequest.post(UNLIMITED_QR_CODE_URL + accessToken)
.timeout(HTTP_TIMEOUT_MILLIS)
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.body(requestBody.toJSONString())
.execute();
byte[] body = response.bodyBytes();
String contentType = response.header(HttpHeaders.CONTENT_TYPE);
if (response.isOk() && body != null && body.length > 0
&& isImageResponse(contentType, body)) {
return new QrCodeImage(body, normalizeImageContentType(contentType, body));
}
String errorBody = body == null ? "" : new String(body, StandardCharsets.UTF_8);
JSONObject errorJson = parseJson(errorBody, "生成微信小程序码失败");
throw weChatError("生成微信小程序码失败", errorJson);
} finally {
if (response != null) {
response.close();
}
}
}
private boolean isImageResponse(String contentType, byte[] body)
{
return (contentType != null && contentType.toLowerCase().startsWith("image/"))
|| isPng(body) || isJpeg(body);
}
private String normalizeImageContentType(String contentType, byte[] body)
{
if (contentType != null && contentType.toLowerCase().startsWith("image/")) {
int separator = contentType.indexOf(';');
return separator > 0 ? contentType.substring(0, separator) : contentType;
}
return isJpeg(body) ? MediaType.IMAGE_JPEG_VALUE : MediaType.IMAGE_PNG_VALUE;
}
private boolean isPng(byte[] body)
{
return body.length >= 8
&& (body[0] & 0xff) == 0x89
&& body[1] == 0x50
&& body[2] == 0x4e
&& body[3] == 0x47;
}
private boolean isJpeg(byte[] body)
{
return body.length >= 3
&& (body[0] & 0xff) == 0xff
&& (body[1] & 0xff) == 0xd8
&& (body[2] & 0xff) == 0xff;
}
private JSONObject parseJson(String body, String action)
{
try {
JSONObject json = JSONObject.parseObject(body);
if (json == null) {
throw new IllegalArgumentException("empty json");
}
return json;
} catch (Exception exception) {
throw new ServiceException(action + ",微信接口返回了无法识别的响应");
}
}
private ServiceException weChatError(String action, JSONObject json)
{
Integer errorCode = json.getInteger("errcode");
String errorMessage = json.getString("errmsg");
StringBuilder message = new StringBuilder(action);
if (errorCode != null) {
message.append("(错误码:").append(errorCode).append('');
}
if (errorMessage != null && !errorMessage.trim().isEmpty()) {
message.append("").append(errorMessage);
}
return new ServiceException(message.toString());
}
public static class AccessTokenResult
{
private final String accessToken;
private final int expiresIn;
public AccessTokenResult(String accessToken, int expiresIn)
{
this.accessToken = accessToken;
this.expiresIn = expiresIn;
}
public String getAccessToken()
{
return accessToken;
}
public int getExpiresIn()
{
return expiresIn;
}
}
public static class QrCodeImage
{
private final byte[] content;
private final String contentType;
public QrCodeImage(byte[] content, String contentType)
{
this.content = content;
this.contentType = contentType;
}
public byte[] getContent()
{
return content;
}
public String getContentType()
{
return contentType;
}
}
}