181 lines
6.2 KiB
Java
181 lines
6.2 KiB
Java
|
|
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)
|
|||
|
|
{
|
|||
|
|
JSONObject requestBody = new JSONObject();
|
|||
|
|
requestBody.put("scene", scene);
|
|||
|
|
requestBody.put("page", page);
|
|||
|
|
// 新页面尚未随小程序版本发布时也允许先生成码;扫码仍会进入正式版小程序。
|
|||
|
|
requestBody.put("check_path", false);
|
|||
|
|
requestBody.put("env_version", "release");
|
|||
|
|
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;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|