|
|
@@ -14,8 +14,15 @@ import org.springframework.web.client.RestTemplate;
|
|
|
import com.etotem.cfc.service.api.WechatServiceInterface;
|
|
|
|
|
|
import javax.annotation.PostConstruct;
|
|
|
+import java.io.ByteArrayInputStream;
|
|
|
+import java.io.File;
|
|
|
+import java.io.IOException;
|
|
|
+import java.io.InputStream;
|
|
|
+import java.io.OutputStream;
|
|
|
import java.io.UnsupportedEncodingException;
|
|
|
import java.net.URLEncoder;
|
|
|
+import java.net.HttpURLConnection;
|
|
|
+import java.net.URL;
|
|
|
import java.nio.charset.StandardCharsets;
|
|
|
import java.util.Base64;
|
|
|
import java.util.HashMap;
|
|
|
@@ -57,6 +64,10 @@ public class WechatService implements WechatServiceInterface {
|
|
|
|
|
|
private final RestTemplate restTemplate = new RestTemplate();
|
|
|
|
|
|
+ // ===== access_token 缓存 =====
|
|
|
+ private volatile String cachedAccessToken;
|
|
|
+ private volatile long tokenExpireTime;
|
|
|
+
|
|
|
/**
|
|
|
* 判断是否测试模式
|
|
|
*/
|
|
|
@@ -200,9 +211,32 @@ public class WechatService implements WechatServiceInterface {
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * 获取access_token
|
|
|
+ * 获取access_token(带缓存,TTL 7000秒,提前200秒刷新)
|
|
|
*/
|
|
|
- private String getAccessToken() {
|
|
|
+ public String getAccessToken() {
|
|
|
+ // 测试模式:每次都重新获取
|
|
|
+ if (testMode) {
|
|
|
+ return doGetAccessToken();
|
|
|
+ }
|
|
|
+
|
|
|
+ long now = System.currentTimeMillis();
|
|
|
+ if (cachedAccessToken != null && now < tokenExpireTime) {
|
|
|
+ return cachedAccessToken;
|
|
|
+ }
|
|
|
+
|
|
|
+ synchronized (this) {
|
|
|
+ if (cachedAccessToken != null && now < tokenExpireTime) {
|
|
|
+ return cachedAccessToken;
|
|
|
+ }
|
|
|
+ String token = doGetAccessToken();
|
|
|
+ // 微信access_token有效期7200秒,提前200秒过期缓存
|
|
|
+ cachedAccessToken = token;
|
|
|
+ tokenExpireTime = now + 7000 * 1000;
|
|
|
+ return token;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private String doGetAccessToken() {
|
|
|
try {
|
|
|
String url = String.format("https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=%s&secret=%s",
|
|
|
appid, secret);
|
|
|
@@ -222,6 +256,116 @@ public class WechatService implements WechatServiceInterface {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * 文本内容安全检测(msg_sec_check)
|
|
|
+ * @param content 要检测的文本内容,长度不超过500K字节
|
|
|
+ * @return true=安全, false=包含敏感信息
|
|
|
+ */
|
|
|
+ public boolean checkText(String content) {
|
|
|
+ if (testMode) {
|
|
|
+ log.info("测试模式:跳过文本内容检测");
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (content == null || content.trim().isEmpty()) {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ String accessToken = getAccessToken();
|
|
|
+ String url = "https://api.weixin.qq.com/wxa/msg_sec_check?access_token=" + accessToken;
|
|
|
+
|
|
|
+ JSONObject requestBody = new JSONObject();
|
|
|
+ requestBody.put("content", content);
|
|
|
+
|
|
|
+ org.springframework.http.HttpHeaders headers = new org.springframework.http.HttpHeaders();
|
|
|
+ headers.setContentType(org.springframework.http.MediaType.APPLICATION_JSON);
|
|
|
+ org.springframework.http.HttpEntity<String> entity = new org.springframework.http.HttpEntity<>(
|
|
|
+ requestBody.toJSONString(), headers);
|
|
|
+
|
|
|
+ ResponseEntity<String> response = restTemplate.exchange(url,
|
|
|
+ org.springframework.http.HttpMethod.POST, entity, String.class);
|
|
|
+ JSONObject json = JSON.parseObject(response.getBody());
|
|
|
+ int errcode = json.getInteger("errcode");
|
|
|
+
|
|
|
+ if (errcode == 0) {
|
|
|
+ return true;
|
|
|
+ } else if (errcode == 87014) {
|
|
|
+ log.warn("文本内容安全检测发现敏感信息: errcode={}", errcode);
|
|
|
+ return false;
|
|
|
+ } else {
|
|
|
+ log.warn("文本内容安全检测返回异常: errcode={}, errmsg={}", errcode, json.getString("errmsg"));
|
|
|
+ // API 异常时 fail-open,不阻塞上传
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.warn("文本内容安全检测调用失败(fail-open): {}", e.getMessage());
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 图片内容安全检测(img_sec_check)
|
|
|
+ * @param imageBytes 图片二进制数据,格式支持PNG/JPEG/JPG/GIF
|
|
|
+ * @return true=安全, false=包含敏感内容
|
|
|
+ */
|
|
|
+ public boolean checkImage(byte[] imageBytes) {
|
|
|
+ if (testMode) {
|
|
|
+ log.info("测试模式:跳过图片内容检测");
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (imageBytes == null || imageBytes.length == 0) {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ String accessToken = getAccessToken();
|
|
|
+ String url = "https://api.weixin.qq.com/wxa/img_sec_check?access_token=" + accessToken;
|
|
|
+
|
|
|
+ // 使用 HttpURLConnection 发送 multipart/form-data
|
|
|
+ HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection();
|
|
|
+ conn.setRequestMethod("POST");
|
|
|
+ conn.setConnectTimeout(10000);
|
|
|
+ conn.setReadTimeout(30000);
|
|
|
+
|
|
|
+ String boundary = "----WebKitFormBoundary" + UUID.randomUUID().toString().replace("-", "");
|
|
|
+ conn.setRequestProperty("Content-Type", "multipart/form-data; boundary=" + boundary);
|
|
|
+ conn.setDoOutput(true);
|
|
|
+
|
|
|
+ OutputStream os = conn.getOutputStream();
|
|
|
+ // media field
|
|
|
+ os.write(("--" + boundary + "\r\n").getBytes(StandardCharsets.UTF_8));
|
|
|
+ os.write("Content-Disposition: form-data; name=\"media\"; filename=\"check.jpg\"\r\n".getBytes(StandardCharsets.UTF_8));
|
|
|
+ os.write("Content-Type: image/jpeg\r\n\r\n".getBytes(StandardCharsets.UTF_8));
|
|
|
+ os.write(imageBytes);
|
|
|
+ os.write(("\r\n--" + boundary + "--\r\n").getBytes(StandardCharsets.UTF_8));
|
|
|
+ os.flush();
|
|
|
+ os.close();
|
|
|
+
|
|
|
+ InputStream is = conn.getInputStream();
|
|
|
+ byte[] responseBytes = is.readAllBytes();
|
|
|
+ is.close();
|
|
|
+ String responseBody = new String(responseBytes, StandardCharsets.UTF_8);
|
|
|
+
|
|
|
+ JSONObject json = JSON.parseObject(responseBody);
|
|
|
+ int errcode = json.getInteger("errcode");
|
|
|
+
|
|
|
+ if (errcode == 0) {
|
|
|
+ return true;
|
|
|
+ } else if (errcode == 87014) {
|
|
|
+ log.warn("图片内容安全检测发现敏感内容: errcode={}", errcode);
|
|
|
+ return false;
|
|
|
+ } else {
|
|
|
+ log.warn("图片内容安全检测返回异常: errcode={}, errmsg={}", errcode, json.getString("errmsg"));
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.warn("图片内容安全检测调用失败(fail-open): {}", e.getMessage());
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
/**
|
|
|
* 生成小程序二维码(wxacode)
|
|
|
* @param scene 场景参数(最大32个可见字符)
|