|
@@ -1,8 +1,8 @@
|
|
|
package com.zhentao.controller;
|
|
package com.zhentao.controller;
|
|
|
|
|
|
|
|
|
|
+import com.zhentao.service.YidunSecurityService;
|
|
|
import org.slf4j.Logger;
|
|
import org.slf4j.Logger;
|
|
|
import org.slf4j.LoggerFactory;
|
|
import org.slf4j.LoggerFactory;
|
|
|
-import org.springframework.beans.factory.annotation.Value;
|
|
|
|
|
import org.springframework.http.MediaType;
|
|
import org.springframework.http.MediaType;
|
|
|
import org.springframework.web.bind.annotation.PostMapping;
|
|
import org.springframework.web.bind.annotation.PostMapping;
|
|
|
import org.springframework.web.bind.annotation.RequestMapping;
|
|
import org.springframework.web.bind.annotation.RequestMapping;
|
|
@@ -11,8 +11,6 @@ import org.springframework.web.bind.annotation.RestController;
|
|
|
|
|
|
|
|
import javax.servlet.http.HttpServletRequest;
|
|
import javax.servlet.http.HttpServletRequest;
|
|
|
import java.io.*;
|
|
import java.io.*;
|
|
|
-import java.net.HttpURLConnection;
|
|
|
|
|
-import java.net.URL;
|
|
|
|
|
import java.nio.charset.StandardCharsets;
|
|
import java.nio.charset.StandardCharsets;
|
|
|
import java.util.Arrays;
|
|
import java.util.Arrays;
|
|
|
import java.util.List;
|
|
import java.util.List;
|
|
@@ -22,7 +20,7 @@ import java.util.regex.Pattern;
|
|
|
/**
|
|
/**
|
|
|
* 内容安全检测控制器
|
|
* 内容安全检测控制器
|
|
|
* 用于检测用户发布的文字、图片是否包含违规内容
|
|
* 用于检测用户发布的文字、图片是否包含违规内容
|
|
|
- * 使用微信小程序内容安全API
|
|
|
|
|
|
|
+ * 使用网易易盾内容安全API
|
|
|
*/
|
|
*/
|
|
|
@RestController
|
|
@RestController
|
|
|
@RequestMapping("/api/content-security")
|
|
@RequestMapping("/api/content-security")
|
|
@@ -30,23 +28,13 @@ public class ContentSecurityController {
|
|
|
|
|
|
|
|
private static final Logger logger = LoggerFactory.getLogger(ContentSecurityController.class);
|
|
private static final Logger logger = LoggerFactory.getLogger(ContentSecurityController.class);
|
|
|
|
|
|
|
|
- // ====================== 常量配置 (统一维护,无硬编码) ======================
|
|
|
|
|
- private static final int MAX_IMAGE_SIZE = 2 * 1024 * 1024; // 图片最大下载大小 2MB,防止OOM
|
|
|
|
|
- private static final int WX_IMAGE_LIMIT_SIZE = 1 * 1024 * 1024; // 微信同步检测图片限制1MB
|
|
|
|
|
- private static final int TOKEN_EXPIRE_ADVANCE = 300; // token提前300秒刷新
|
|
|
|
|
- private static final int WX_ERRCODE_ILLEGAL_CONTENT = 87014; // 内容违规错误码
|
|
|
|
|
- private static final int WX_ERRCODE_TOKEN_EXPIRE_1 = 40001; // token过期错误码1
|
|
|
|
|
- private static final int WX_ERRCODE_TOKEN_EXPIRE_2 = 42001; // token过期错误码2
|
|
|
|
|
|
|
+ // 注入网易易盾内容安全服务
|
|
|
|
|
+ private final YidunSecurityService yidunSecurityService;
|
|
|
|
|
|
|
|
- @Value("${wechat.miniapp.appid:}")
|
|
|
|
|
- private String appId;
|
|
|
|
|
-
|
|
|
|
|
- @Value("${wechat.miniapp.secret:}")
|
|
|
|
|
- private String appSecret;
|
|
|
|
|
-
|
|
|
|
|
- // AccessToken缓存,volatile保证多线程可见性
|
|
|
|
|
- private volatile String cachedAccessToken;
|
|
|
|
|
- private volatile long tokenExpireTime = 0;
|
|
|
|
|
|
|
+ // 构造函数注入
|
|
|
|
|
+ public ContentSecurityController(YidunSecurityService yidunSecurityService) {
|
|
|
|
|
+ this.yidunSecurityService = yidunSecurityService;
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
private static final List<String> SENSITIVE_WORDS = Arrays.asList(
|
|
private static final List<String> SENSITIVE_WORDS = Arrays.asList(
|
|
|
"色情", "裸体", "性爱", "约炮", "一夜情", "援交", "卖淫", "嫖娼", "小姐服务",
|
|
"色情", "裸体", "性爱", "约炮", "一夜情", "援交", "卖淫", "嫖娼", "小姐服务",
|
|
@@ -79,31 +67,31 @@ public class ContentSecurityController {
|
|
|
|
|
|
|
|
logger.info("开始检测文字内容,长度: {}", content.length());
|
|
logger.info("开始检测文字内容,长度: {}", content.length());
|
|
|
|
|
|
|
|
- // 本地敏感词检测
|
|
|
|
|
|
|
+ // 第一层:本地敏感词快速检测
|
|
|
String localCheckResult = localTextCheck(content);
|
|
String localCheckResult = localTextCheck(content);
|
|
|
if (localCheckResult != null) {
|
|
if (localCheckResult != null) {
|
|
|
logger.info("本地敏感词检测不通过: {}", localCheckResult);
|
|
logger.info("本地敏感词检测不通过: {}", localCheckResult);
|
|
|
return buildResponse(false, "内容包含敏感词,请修改后重试");
|
|
return buildResponse(false, "内容包含敏感词,请修改后重试");
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // 微信内容安全检测
|
|
|
|
|
|
|
+ // 第二层:网易易盾内容安全检测(AI智能检测)
|
|
|
try {
|
|
try {
|
|
|
- String wxResult = wxMsgSecCheck(content);
|
|
|
|
|
- if (wxResult != null) {
|
|
|
|
|
- logger.info("微信内容安全检测不通过");
|
|
|
|
|
- return buildResponse(false, wxResult);
|
|
|
|
|
|
|
+ String yidunResult = yidunSecurityService.checkText(content);
|
|
|
|
|
+ if (yidunResult != null) {
|
|
|
|
|
+ logger.info("网易易盾内容安全检测不通过: {}", yidunResult);
|
|
|
|
|
+ return buildResponse(false, yidunResult);
|
|
|
}
|
|
}
|
|
|
} catch (Exception e) {
|
|
} catch (Exception e) {
|
|
|
- logger.error("微信文字内容安全检测异常", e);
|
|
|
|
|
|
|
+ logger.error("网易易盾文字内容安全检测异常", e);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- logger.info("文字内容检测通过");
|
|
|
|
|
|
|
+ logger.info("文字内容检测通过(已通过本地+网易易盾检测)");
|
|
|
return buildResponse(true, "");
|
|
return buildResponse(true, "");
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
* 检测图片是否安全
|
|
* 检测图片是否安全
|
|
|
- * 检测链路:同步接口(img_sec_check) → 异步接口(media_check_async)
|
|
|
|
|
|
|
+ * 使用网易易盾内容安全API进行检测
|
|
|
*/
|
|
*/
|
|
|
@PostMapping(value = "/check-image", produces = MediaType.APPLICATION_JSON_VALUE)
|
|
@PostMapping(value = "/check-image", produces = MediaType.APPLICATION_JSON_VALUE)
|
|
|
@ResponseBody
|
|
@ResponseBody
|
|
@@ -125,26 +113,15 @@ public class ContentSecurityController {
|
|
|
|
|
|
|
|
logger.info("开始检测图片: {}", imageUrl);
|
|
logger.info("开始检测图片: {}", imageUrl);
|
|
|
|
|
|
|
|
- // 1. 先尝试使用微信同步接口 img_sec_check (精度高,实时返回结果)
|
|
|
|
|
- try {
|
|
|
|
|
- String wxResult = wxImgSecCheck(imageUrl);
|
|
|
|
|
- if (wxResult != null) {
|
|
|
|
|
- logger.info("微信图片同步检测不通过: {}", imageUrl);
|
|
|
|
|
- return buildResponse(false, wxResult);
|
|
|
|
|
- }
|
|
|
|
|
- } catch (Exception e) {
|
|
|
|
|
- logger.error("微信图片同步检测异常: {}", imageUrl, e);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // 2. 如果同步接口无结果/图片过大,使用异步接口补充检测
|
|
|
|
|
|
|
+ // 使用网易易盾图片内容安全检测
|
|
|
try {
|
|
try {
|
|
|
- String asyncResult = wxMediaCheckAsync(imageUrl);
|
|
|
|
|
- if (asyncResult != null) {
|
|
|
|
|
- logger.info("微信图片异步检测不通过: {}", imageUrl);
|
|
|
|
|
- return buildResponse(false, asyncResult);
|
|
|
|
|
|
|
+ String yidunResult = yidunSecurityService.checkImage(imageUrl);
|
|
|
|
|
+ if (yidunResult != null) {
|
|
|
|
|
+ logger.info("网易易盾图片检测不通过: {}", yidunResult);
|
|
|
|
|
+ return buildResponse(false, yidunResult);
|
|
|
}
|
|
}
|
|
|
} catch (Exception e) {
|
|
} catch (Exception e) {
|
|
|
- logger.error("微信图片异步检测异常: {}", imageUrl, e);
|
|
|
|
|
|
|
+ logger.error("网易易盾图片内容安全检测异常", e);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
logger.info("图片检测通过: {}", imageUrl);
|
|
logger.info("图片检测通过: {}", imageUrl);
|
|
@@ -187,323 +164,7 @@ public class ContentSecurityController {
|
|
|
return null;
|
|
return null;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- /**
|
|
|
|
|
- * 调用微信文字内容安全检测API (msg_sec_check) - 同步接口
|
|
|
|
|
- */
|
|
|
|
|
- private String wxMsgSecCheck(String content) {
|
|
|
|
|
- HttpURLConnection conn = null;
|
|
|
|
|
- try {
|
|
|
|
|
- String accessToken = getAccessToken();
|
|
|
|
|
- if (accessToken == null || accessToken.isEmpty()) {
|
|
|
|
|
- logger.warn("获取access_token失败,跳过微信文字检测");
|
|
|
|
|
- return null;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- String urlStr = "https://api.weixin.qq.com/wxa/msg_sec_check?access_token=" + accessToken;
|
|
|
|
|
- URL url = new URL(urlStr);
|
|
|
|
|
- conn = (HttpURLConnection) url.openConnection();
|
|
|
|
|
- conn.setRequestMethod("POST");
|
|
|
|
|
- conn.setRequestProperty("Content-Type", "application/json; charset=UTF-8");
|
|
|
|
|
- conn.setDoOutput(true);
|
|
|
|
|
- conn.setConnectTimeout(10000);
|
|
|
|
|
- conn.setReadTimeout(10000);
|
|
|
|
|
-
|
|
|
|
|
- // 修复:openid传空字符串(无用户openid时的兼容写法,微信允许),不再传appId
|
|
|
|
|
- String jsonBody = "{\"content\":\"" + escapeJson(content) + "\",\"version\":2,\"scene\":2,\"openid\":\"\"}";
|
|
|
|
|
- logger.debug("微信文字检测请求体: {}", jsonBody);
|
|
|
|
|
-
|
|
|
|
|
- try (OutputStream os = conn.getOutputStream()) {
|
|
|
|
|
- os.write(jsonBody.getBytes(StandardCharsets.UTF_8));
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- int responseCode = conn.getResponseCode();
|
|
|
|
|
- String responseBody = readHttpResponse(conn);
|
|
|
|
|
- logger.info("微信文字检测响应: code={}, body={}", responseCode, responseBody);
|
|
|
|
|
-
|
|
|
|
|
- if (responseCode == 200 && responseBody != null) {
|
|
|
|
|
- int errcode = getJsonInt(responseBody, "errcode");
|
|
|
|
|
- // 全局处理token过期
|
|
|
|
|
- if (isTokenExpireCode(errcode)) {
|
|
|
|
|
- clearTokenCache();
|
|
|
|
|
- return null;
|
|
|
|
|
- }
|
|
|
|
|
- if (errcode == 0) {
|
|
|
|
|
- String suggest = getNestedJsonString(responseBody, "result", "suggest");
|
|
|
|
|
- if ("risky".equals(suggest)) {
|
|
|
|
|
- String label = getNestedJsonString(responseBody, "result", "label");
|
|
|
|
|
- return "内容包含" + getLabelText(label) + ",请修改后重试";
|
|
|
|
|
- }
|
|
|
|
|
- return null;
|
|
|
|
|
- } else if (errcode == WX_ERRCODE_ILLEGAL_CONTENT) {
|
|
|
|
|
- return "内容包含违规信息,请修改后重试";
|
|
|
|
|
- } else {
|
|
|
|
|
- logger.warn("微信文字检测返回错误码: {}, 描述: {}", errcode, getJsonString(responseBody, "errmsg"));
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- } catch (Exception e) {
|
|
|
|
|
- logger.error("调用微信文字安全检测API异常", e);
|
|
|
|
|
- } finally {
|
|
|
|
|
- if (conn != null) conn.disconnect();
|
|
|
|
|
- }
|
|
|
|
|
- return null;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- /**
|
|
|
|
|
- * 调用微信图片内容安全检测API (img_sec_check) - 同步接口
|
|
|
|
|
- * 限制:图片大小 <= 1MB,实时返回检测结果
|
|
|
|
|
- */
|
|
|
|
|
- private String wxImgSecCheck(String imageUrl) {
|
|
|
|
|
- HttpURLConnection conn = null;
|
|
|
|
|
- try {
|
|
|
|
|
- String accessToken = getAccessToken();
|
|
|
|
|
- if (accessToken == null || accessToken.isEmpty()) {
|
|
|
|
|
- logger.warn("获取access_token失败,跳过微信图片同步检测");
|
|
|
|
|
- return null;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- logger.info("开始下载图片: {}", imageUrl);
|
|
|
|
|
- byte[] imageBytes = downloadImage(imageUrl);
|
|
|
|
|
- if (imageBytes == null || imageBytes.length == 0) {
|
|
|
|
|
- logger.warn("下载图片失败: {}", imageUrl);
|
|
|
|
|
- return null;
|
|
|
|
|
- }
|
|
|
|
|
- logger.info("图片下载成功,大小: {} bytes", imageBytes.length);
|
|
|
|
|
-
|
|
|
|
|
- // 微信同步检测限制图片1MB,超过则跳过走异步
|
|
|
|
|
- if (imageBytes.length > WX_IMAGE_LIMIT_SIZE) {
|
|
|
|
|
- logger.warn("图片大小超过1MB({} bytes),跳过同步检测,走异步检测", imageBytes.length);
|
|
|
|
|
- return null;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- String boundary = "----WebKitFormBoundary" + System.currentTimeMillis();
|
|
|
|
|
- String urlStr = "https://api.weixin.qq.com/wxa/img_sec_check?access_token=" + accessToken;
|
|
|
|
|
-
|
|
|
|
|
- URL url = new URL(urlStr);
|
|
|
|
|
- conn = (HttpURLConnection) url.openConnection();
|
|
|
|
|
- conn.setRequestMethod("POST");
|
|
|
|
|
- conn.setRequestProperty("Content-Type", "multipart/form-data; boundary=" + boundary);
|
|
|
|
|
- conn.setDoOutput(true);
|
|
|
|
|
- conn.setConnectTimeout(15000);
|
|
|
|
|
- conn.setReadTimeout(30000);
|
|
|
|
|
-
|
|
|
|
|
- try (OutputStream os = conn.getOutputStream()) {
|
|
|
|
|
- String header = "--" + boundary + "\r\n" +
|
|
|
|
|
- "Content-Disposition: form-data; name=\"media\"; filename=\"image.jpg\"\r\n" +
|
|
|
|
|
- "Content-Type: application/octet-stream\r\n\r\n";
|
|
|
|
|
- os.write(header.getBytes(StandardCharsets.UTF_8));
|
|
|
|
|
- os.write(imageBytes);
|
|
|
|
|
- os.write(("\r\n--" + boundary + "--\r\n").getBytes(StandardCharsets.UTF_8));
|
|
|
|
|
- os.flush();
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- int responseCode = conn.getResponseCode();
|
|
|
|
|
- String responseBody = readHttpResponse(conn);
|
|
|
|
|
- logger.info("微信图片同步检测响应: code={}, body={}", responseCode, responseBody);
|
|
|
|
|
-
|
|
|
|
|
- if (responseCode == 200 && responseBody != null) {
|
|
|
|
|
- int errcode = getJsonInt(responseBody, "errcode");
|
|
|
|
|
- if (isTokenExpireCode(errcode)) {
|
|
|
|
|
- clearTokenCache();
|
|
|
|
|
- return null;
|
|
|
|
|
- }
|
|
|
|
|
- if (errcode == 0) {
|
|
|
|
|
- return null;
|
|
|
|
|
- } else if (errcode == WX_ERRCODE_ILLEGAL_CONTENT) {
|
|
|
|
|
- return "图片包含违规内容,请更换后重试";
|
|
|
|
|
- } else {
|
|
|
|
|
- logger.warn("微信图片同步检测错误码: {}, 描述: {}", errcode, getJsonString(responseBody, "errmsg"));
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- } catch (Exception e) {
|
|
|
|
|
- logger.error("调用微信图片同步检测API异常", e);
|
|
|
|
|
- } finally {
|
|
|
|
|
- if (conn != null) conn.disconnect();
|
|
|
|
|
- }
|
|
|
|
|
- return null;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- /**
|
|
|
|
|
- * 调用微信媒体内容安全检测API (media_check_async) - 异步接口
|
|
|
|
|
- * 适用:图片>1MB,提交检测后返回trace_id,结果需回调获取
|
|
|
|
|
- */
|
|
|
|
|
- private String wxMediaCheckAsync(String imageUrl) {
|
|
|
|
|
- HttpURLConnection conn = null;
|
|
|
|
|
- try {
|
|
|
|
|
- String accessToken = getAccessToken();
|
|
|
|
|
- if (accessToken == null || accessToken.isEmpty()) {
|
|
|
|
|
- logger.warn("获取access_token失败,跳过微信图片异步检测");
|
|
|
|
|
- return null;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- String urlStr = "https://api.weixin.qq.com/wxa/media_check_async?access_token=" + accessToken;
|
|
|
|
|
- URL url = new URL(urlStr);
|
|
|
|
|
- conn = (HttpURLConnection) url.openConnection();
|
|
|
|
|
- conn.setRequestMethod("POST");
|
|
|
|
|
- conn.setRequestProperty("Content-Type", "application/json; charset=UTF-8");
|
|
|
|
|
- conn.setDoOutput(true);
|
|
|
|
|
- conn.setConnectTimeout(10000);
|
|
|
|
|
- conn.setReadTimeout(10000);
|
|
|
|
|
-
|
|
|
|
|
- // 修复:openid传空字符串,不再传appId
|
|
|
|
|
- String jsonBody = "{\"media_url\":\"" + escapeJson(imageUrl) + "\",\"media_type\":2,\"version\":2,\"scene\":2,\"openid\":\"\"}";
|
|
|
|
|
- logger.info("微信图片异步检测请求体: {}", jsonBody);
|
|
|
|
|
-
|
|
|
|
|
- try (OutputStream os = conn.getOutputStream()) {
|
|
|
|
|
- os.write(jsonBody.getBytes(StandardCharsets.UTF_8));
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- int responseCode = conn.getResponseCode();
|
|
|
|
|
- String responseBody = readHttpResponse(conn);
|
|
|
|
|
- logger.info("微信图片异步检测响应: code={}, body={}", responseCode, responseBody);
|
|
|
|
|
-
|
|
|
|
|
- if (responseCode == 200 && responseBody != null) {
|
|
|
|
|
- int errcode = getJsonInt(responseBody, "errcode");
|
|
|
|
|
- if (isTokenExpireCode(errcode)) {
|
|
|
|
|
- clearTokenCache();
|
|
|
|
|
- return null;
|
|
|
|
|
- }
|
|
|
|
|
- if (errcode == 0) {
|
|
|
|
|
- String traceId = getJsonString(responseBody, "trace_id");
|
|
|
|
|
- logger.info("微信异步检测已提交,trace_id: {}", traceId);
|
|
|
|
|
- return null;
|
|
|
|
|
- } else if (errcode == WX_ERRCODE_ILLEGAL_CONTENT) {
|
|
|
|
|
- return "图片包含违规内容,请更换后重试";
|
|
|
|
|
- } else {
|
|
|
|
|
- logger.warn("微信异步检测错误码: {}, 描述: {}", errcode, getJsonString(responseBody, "errmsg"));
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- } catch (Exception e) {
|
|
|
|
|
- logger.error("调用微信图片异步检测API异常", e);
|
|
|
|
|
- } finally {
|
|
|
|
|
- if (conn != null) conn.disconnect();
|
|
|
|
|
- }
|
|
|
|
|
- return null;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- /**
|
|
|
|
|
- * 下载图片字节数组,增加大小限制防止OOM
|
|
|
|
|
- */
|
|
|
|
|
- private byte[] downloadImage(String imageUrl) {
|
|
|
|
|
- HttpURLConnection conn = null;
|
|
|
|
|
- try {
|
|
|
|
|
- URL url = new URL(imageUrl);
|
|
|
|
|
- conn = (HttpURLConnection) url.openConnection();
|
|
|
|
|
- conn.setRequestMethod("GET");
|
|
|
|
|
- conn.setConnectTimeout(10000);
|
|
|
|
|
- conn.setReadTimeout(30000);
|
|
|
|
|
- conn.setRequestProperty("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36");
|
|
|
|
|
- conn.setInstanceFollowRedirects(true);
|
|
|
|
|
-
|
|
|
|
|
- int responseCode = conn.getResponseCode();
|
|
|
|
|
- if (responseCode != 200) {
|
|
|
|
|
- logger.warn("下载图片失败,HTTP状态码: {}", responseCode);
|
|
|
|
|
- return null;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- try (InputStream is = conn.getInputStream();
|
|
|
|
|
- ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
|
|
|
|
|
- byte[] buffer = new byte[8192];
|
|
|
|
|
- int bytesRead;
|
|
|
|
|
- while ((bytesRead = is.read(buffer)) != -1) {
|
|
|
|
|
- // 超过最大限制,直接返回null,防止OOM
|
|
|
|
|
- if (baos.size() + bytesRead > MAX_IMAGE_SIZE) {
|
|
|
|
|
- logger.warn("图片大小超过{}MB,终止下载", MAX_IMAGE_SIZE / 1024 / 1024);
|
|
|
|
|
- return null;
|
|
|
|
|
- }
|
|
|
|
|
- baos.write(buffer, 0, bytesRead);
|
|
|
|
|
- }
|
|
|
|
|
- return baos.toByteArray();
|
|
|
|
|
- }
|
|
|
|
|
- } catch (Exception e) {
|
|
|
|
|
- logger.error("下载图片异常: {}", imageUrl, e);
|
|
|
|
|
- return null;
|
|
|
|
|
- } finally {
|
|
|
|
|
- if (conn != null) conn.disconnect();
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- /**
|
|
|
|
|
- * 获取微信AccessToken,双重检查锁保证并发安全+性能,缓存过期自动刷新
|
|
|
|
|
- */
|
|
|
|
|
- private String getAccessToken() {
|
|
|
|
|
- // 第一次无锁检查,提高并发性能
|
|
|
|
|
- if (cachedAccessToken != null && System.currentTimeMillis() < tokenExpireTime) {
|
|
|
|
|
- logger.debug("使用缓存的access_token,剩余有效期:{}ms", tokenExpireTime - System.currentTimeMillis());
|
|
|
|
|
- return cachedAccessToken;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- synchronized (this) {
|
|
|
|
|
- // 第二次加锁检查,防止多线程重复请求
|
|
|
|
|
- if (cachedAccessToken != null && System.currentTimeMillis() < tokenExpireTime) {
|
|
|
|
|
- return cachedAccessToken;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- if (appId == null || appId.isEmpty() || appSecret == null || appSecret.isEmpty()) {
|
|
|
|
|
- logger.error("微信小程序appId或appSecret未配置! appId={}", appId != null ? appId.substring(0, 4) + "***" : "null");
|
|
|
|
|
- return null;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- HttpURLConnection conn = null;
|
|
|
|
|
- try {
|
|
|
|
|
- String urlStr = String.format(
|
|
|
|
|
- "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=%s&secret=%s",
|
|
|
|
|
- appId, appSecret
|
|
|
|
|
- );
|
|
|
|
|
- logger.info("开始请求微信access_token,appId:{}***", appId.substring(0, 4));
|
|
|
|
|
-
|
|
|
|
|
- URL url = new URL(urlStr);
|
|
|
|
|
- conn = (HttpURLConnection) url.openConnection();
|
|
|
|
|
- conn.setRequestMethod("GET");
|
|
|
|
|
- conn.setConnectTimeout(10000);
|
|
|
|
|
- conn.setReadTimeout(10000);
|
|
|
|
|
-
|
|
|
|
|
- int responseCode = conn.getResponseCode();
|
|
|
|
|
- String responseBody = readHttpResponse(conn);
|
|
|
|
|
- logger.info("获取token响应: code={}, body={}", responseCode, responseBody);
|
|
|
|
|
-
|
|
|
|
|
- if (responseCode == 200 && responseBody != null) {
|
|
|
|
|
- String token = getJsonString(responseBody, "access_token");
|
|
|
|
|
- if (token != null && !token.isEmpty()) {
|
|
|
|
|
- cachedAccessToken = token;
|
|
|
|
|
- int expiresIn = getJsonInt(responseBody, "expires_in");
|
|
|
|
|
- if (expiresIn == 0) expiresIn = 7200;
|
|
|
|
|
- tokenExpireTime = System.currentTimeMillis() + (expiresIn - TOKEN_EXPIRE_ADVANCE) * 1000L;
|
|
|
|
|
- logger.info("获取access_token成功,有效期:{}秒", expiresIn);
|
|
|
|
|
- return cachedAccessToken;
|
|
|
|
|
- } else {
|
|
|
|
|
- int errcode = getJsonInt(responseBody, "errcode");
|
|
|
|
|
- String errmsg = getJsonString(responseBody, "errmsg");
|
|
|
|
|
- logger.error("获取token失败: errcode={}, errmsg={}", errcode, errmsg);
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- } catch (Exception e) {
|
|
|
|
|
- logger.error("获取微信access_token异常", e);
|
|
|
|
|
- } finally {
|
|
|
|
|
- if (conn != null) conn.disconnect();
|
|
|
|
|
- }
|
|
|
|
|
- return null;
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
// ====================== 通用工具方法 ======================
|
|
// ====================== 通用工具方法 ======================
|
|
|
- /**
|
|
|
|
|
- * 读取Http响应体,兼容成功流和错误流
|
|
|
|
|
- */
|
|
|
|
|
- private String readHttpResponse(HttpURLConnection conn) throws IOException {
|
|
|
|
|
- InputStream is = conn.getResponseCode() >= 400 ? conn.getErrorStream() : conn.getInputStream();
|
|
|
|
|
- if (is == null) return "";
|
|
|
|
|
- try (BufferedReader reader = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8))) {
|
|
|
|
|
- StringBuilder sb = new StringBuilder();
|
|
|
|
|
- String line;
|
|
|
|
|
- while ((line = reader.readLine()) != null) {
|
|
|
|
|
- sb.append(line);
|
|
|
|
|
- }
|
|
|
|
|
- return sb.toString();
|
|
|
|
|
- } finally {
|
|
|
|
|
- if (is != null) is.close();
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
/**
|
|
/**
|
|
|
* 标准JSON字符串转义,补全所有缺失的转义字符
|
|
* 标准JSON字符串转义,补全所有缺失的转义字符
|
|
|
*/
|
|
*/
|
|
@@ -535,71 +196,4 @@ public class ContentSecurityController {
|
|
|
Matcher matcher = pattern.matcher(json);
|
|
Matcher matcher = pattern.matcher(json);
|
|
|
return matcher.find() ? matcher.group(1) : null;
|
|
return matcher.find() ? matcher.group(1) : null;
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
- /**
|
|
|
|
|
- * 正则解析JSON字符串中的数字值
|
|
|
|
|
- */
|
|
|
|
|
- private int getJsonInt(String json, String key) {
|
|
|
|
|
- if (json == null || key == null) return 0;
|
|
|
|
|
- Pattern pattern = Pattern.compile("\"" + key + "\"\\s*:\\s*(-?\\d+)");
|
|
|
|
|
- Matcher matcher = pattern.matcher(json);
|
|
|
|
|
- if (matcher.find()) {
|
|
|
|
|
- try {
|
|
|
|
|
- return Integer.parseInt(matcher.group(1));
|
|
|
|
|
- } catch (NumberFormatException e) {
|
|
|
|
|
- return 0;
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- return 0;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- /**
|
|
|
|
|
- * 解析嵌套JSON中的字符串值
|
|
|
|
|
- */
|
|
|
|
|
- private String getNestedJsonString(String json, String parentKey, String childKey) {
|
|
|
|
|
- if (json == null || parentKey == null || childKey == null) return null;
|
|
|
|
|
- Pattern parentPattern = Pattern.compile("\"" + parentKey + "\"\\s*:\\s*\\{([^}]*)\\}");
|
|
|
|
|
- Matcher parentMatcher = parentPattern.matcher(json);
|
|
|
|
|
- if (parentMatcher.find()) {
|
|
|
|
|
- String parentContent = parentMatcher.group(1);
|
|
|
|
|
- return getJsonString("{" + parentContent + "}", childKey);
|
|
|
|
|
- }
|
|
|
|
|
- return null;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- /**
|
|
|
|
|
- * 新增:缺失的标签转换方法,解决运行报错
|
|
|
|
|
- */
|
|
|
|
|
- private String getLabelText(String label) {
|
|
|
|
|
- if (label == null) return "违规内容";
|
|
|
|
|
- switch (label) {
|
|
|
|
|
- case "100": return "违规内容";
|
|
|
|
|
- case "10001": return "广告推广内容";
|
|
|
|
|
- case "20001": return "时政敏感内容";
|
|
|
|
|
- case "20002": return "色情低俗内容";
|
|
|
|
|
- case "20003": return "辱骂攻击内容";
|
|
|
|
|
- case "20006": return "违法犯罪内容";
|
|
|
|
|
- case "20008": return "欺诈诱导内容";
|
|
|
|
|
- case "20012": return "低俗恶心内容";
|
|
|
|
|
- case "20013": return "侵权版权内容";
|
|
|
|
|
- case "21000": return "其他违规内容";
|
|
|
|
|
- default: return "违规内容";
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- /**
|
|
|
|
|
- * 判断是否为token过期错误码
|
|
|
|
|
- */
|
|
|
|
|
- private boolean isTokenExpireCode(int errcode) {
|
|
|
|
|
- return errcode == WX_ERRCODE_TOKEN_EXPIRE_1 || errcode == WX_ERRCODE_TOKEN_EXPIRE_2;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- /**
|
|
|
|
|
- * 清空token缓存
|
|
|
|
|
- */
|
|
|
|
|
- private void clearTokenCache() {
|
|
|
|
|
- cachedAccessToken = null;
|
|
|
|
|
- tokenExpireTime = 0;
|
|
|
|
|
- logger.info("检测到access_token过期,已清空缓存");
|
|
|
|
|
- }
|
|
|
|
|
}
|
|
}
|