feat: 完善户外招聘会签到详情及小程序码
This commit is contained in:
@@ -0,0 +1,180 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user