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; /** * 微信小程序服务端接口客户端。 * *

该客户端只负责单次远程调用,不记录 appSecret、access token 或微信响应中的敏感内容。

*/ @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 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; } } }