优化 json 格式处理
This commit is contained in:
@@ -69,7 +69,7 @@ offerpie/back-end
|
|||||||
│ ├─ aop/ # AOP 日志切面
|
│ ├─ aop/ # AOP 日志切面
|
||||||
│ ├─ exception/ # 业务异常统一处理
|
│ ├─ exception/ # 业务异常统一处理
|
||||||
│ ├─ email/ # 邮件发送抽象(EmailAbility)
|
│ ├─ email/ # 邮件发送抽象(EmailAbility)
|
||||||
│ ├─ ai/ # AI 对话能力封装(AiChatAbility、AiChatConfig)
|
│ ├─ ai/ # AI 对话能力封装(AiChatAbility、AiChatConfig、AiResponseCleanTool)
|
||||||
│ ├─ wxPay/ # 微信支付相关能力(Js、Native、Transfer 等)
|
│ ├─ wxPay/ # 微信支付相关能力(Js、Native、Transfer 等)
|
||||||
│ ├─ pojo/ # 公共 POJO(统一响应、登录/防重放 token 等)
|
│ ├─ pojo/ # 公共 POJO(统一响应、登录/防重放 token 等)
|
||||||
│ └─ web/ # Spring MVC 全局响应体 advice
|
│ └─ web/ # Spring MVC 全局响应体 advice
|
||||||
@@ -271,7 +271,7 @@ offerpie/back-end
|
|||||||
| **配置** | `OssConfig`, `RedissonConf`, `SecurityConfig`, `WxPayConfig`, `SmsConfig`, `AsyncConfig` | `common/config` |
|
| **配置** | `OssConfig`, `RedissonConf`, `SecurityConfig`, `WxPayConfig`, `SmsConfig`, `AsyncConfig` | `common/config` |
|
||||||
| **安全** | JWT 过滤器、登录令牌 (`RedisLoginTokenInfo`)、防重放 (`RedisPreventReplayInfo`) | `common/interceptor`、`common/pojo/interceptor` |
|
| **安全** | JWT 过滤器、登录令牌 (`RedisLoginTokenInfo`)、防重放 (`RedisPreventReplayInfo`) | `common/interceptor`、`common/pojo/interceptor` |
|
||||||
| **邮件** | `EmailAbility`(封装邮件发送) | `common/email` |
|
| **邮件** | `EmailAbility`(封装邮件发送) | `common/email` |
|
||||||
| **AI** | `AiChatAbility`(OpenAI 兼容多供应商对话)、`AiChatConfig`(供应商配置) | `common/ai` |
|
| **AI** | `AiChatAbility`(OpenAI 兼容多供应商对话)、`AiChatConfig`(供应商配置)、`AiResponseCleanTool`(AI响应文本清洗) | `common/ai` |
|
||||||
| **微信支付** | `WxJsPayAbility`, `WxNativePayAbility`, `WxTransferPayAbility`, `WxPayNotifyController` | `common/wxPay` |
|
| **微信支付** | `WxJsPayAbility`, `WxNativePayAbility`, `WxTransferPayAbility`, `WxPayNotifyController` | `common/wxPay` |
|
||||||
| **全局异常** | `GlobalExceptionAdvice`, `BusinessException`, `BusinessExpCodeEnum` | `common/exception` |
|
| **全局异常** | `GlobalExceptionAdvice`, `BusinessException`, `BusinessExpCodeEnum` | `common/exception` |
|
||||||
| **日志 & AOP** | `ControllerLogAspect`, `LoggingOriginalRequestFilter`, `SqlLoggerInterceptor` | `common/aop`, `common/interceptor` |
|
| **日志 & AOP** | `ControllerLogAspect`, `LoggingOriginalRequestFilter`, `SqlLoggerInterceptor` | `common/aop`, `common/interceptor` |
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package org.jiayunet.service;
|
package org.jiayunet.service;
|
||||||
|
|
||||||
import org.jiayunet.ai.AiChatAbility;
|
import org.jiayunet.ai.AiChatAbility;
|
||||||
|
import org.jiayunet.ai.AiResponseCleanTool;
|
||||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
@@ -651,7 +652,7 @@ public class JobService {
|
|||||||
|
|
||||||
// 8. 调用AI
|
// 8. 调用AI
|
||||||
String aiResponse = aiChatAbility.chat("job-recommend", systemPrompt, userMessage);
|
String aiResponse = aiChatAbility.chat("job-recommend", systemPrompt, userMessage);
|
||||||
String json = cleanAiResponse(aiResponse);
|
String json = AiResponseCleanTool.clean(aiResponse);
|
||||||
|
|
||||||
// 9. 解析AI返回,过滤出选中的岗位
|
// 9. 解析AI返回,过滤出选中的岗位
|
||||||
try {
|
try {
|
||||||
@@ -675,13 +676,4 @@ public class JobService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 清理AI返回的markdown代码块和控制字符 */
|
|
||||||
private String cleanAiResponse(String response) {
|
|
||||||
String json = response.trim();
|
|
||||||
if (json.startsWith("```")) {
|
|
||||||
json = json.replaceAll("^```\\w*\\n?", "").replaceAll("\\n?```$", "").trim();
|
|
||||||
}
|
|
||||||
json = json.replaceAll("[\\x00-\\x08\\x0B\\x0C\\x0E-\\x1F]", "");
|
|
||||||
return json;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
package org.jiayunet.ai;
|
||||||
|
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AI 响应文本清洗工具
|
||||||
|
* <p>从 AI 返回的原始文本中提取干净的 JSON 字符串,处理 markdown 代码块、前后说明文字、不可见控制字符等</p>
|
||||||
|
*
|
||||||
|
* @author zk
|
||||||
|
*/
|
||||||
|
public final class AiResponseCleanTool {
|
||||||
|
|
||||||
|
private AiResponseCleanTool() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 匹配 markdown 代码块:```可选语言标识\n 内容 \n```,DOTALL 让 . 匹配换行 */
|
||||||
|
private static final Pattern CODE_BLOCK_PATTERN = Pattern.compile("```\\w*\\s*\\n?(.*?)\\n?\\s*```", Pattern.DOTALL);
|
||||||
|
|
||||||
|
/** 匹配不可见控制字符(保留 \t \n \r) */
|
||||||
|
private static final Pattern CONTROL_CHAR_PATTERN = Pattern.compile("[\\x00-\\x08\\x0B\\x0C\\x0E-\\x1F]");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清理 AI 返回的文本,提取其中的 JSON
|
||||||
|
* <p>1. null/空白返回空串 2. 优先从 markdown 代码块提取 3. 无代码块则定位首个 JSON 起始符 4. 清除控制字符</p>
|
||||||
|
*/
|
||||||
|
public static String clean(String response) {
|
||||||
|
if (response == null || response.isBlank()) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
String result = response.trim();
|
||||||
|
|
||||||
|
// 尝试从 markdown 代码块中提取
|
||||||
|
Matcher matcher = CODE_BLOCK_PATTERN.matcher(result);
|
||||||
|
if (matcher.find()) {
|
||||||
|
result = matcher.group(1).trim();
|
||||||
|
} else {
|
||||||
|
// 没有代码块时,尝试提取第一个 JSON 对象 {} 或数组 []
|
||||||
|
int jsonStart = findJsonStart(result);
|
||||||
|
if (jsonStart > 0) {
|
||||||
|
result = result.substring(jsonStart).trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清除控制字符
|
||||||
|
result = CONTROL_CHAR_PATTERN.matcher(result).replaceAll("");
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 查找第一个 '{' 或 '[' 的位置,未找到返回 -1 */
|
||||||
|
private static int findJsonStart(String text) {
|
||||||
|
int objStart = text.indexOf('{');
|
||||||
|
int arrStart = text.indexOf('[');
|
||||||
|
if (objStart < 0) return arrStart;
|
||||||
|
if (arrStart < 0) return objStart;
|
||||||
|
return Math.min(objStart, arrStart);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
|||||||
import com.fasterxml.jackson.databind.JsonNode;
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.jiayunet.ai.AiChatAbility;
|
import org.jiayunet.ai.AiChatAbility;
|
||||||
|
import org.jiayunet.ai.AiResponseCleanTool;
|
||||||
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
|
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
|
||||||
import org.jiayunet.mapper.AppJobDataMapper;
|
import org.jiayunet.mapper.AppJobDataMapper;
|
||||||
import org.jiayunet.mapper.JobMapper;
|
import org.jiayunet.mapper.JobMapper;
|
||||||
@@ -128,7 +129,7 @@ public class JobCleanService {
|
|||||||
|
|
||||||
// 3. 解析JSON
|
// 3. 解析JSON
|
||||||
try {
|
try {
|
||||||
String json = cleanAiResponse(aiResponse);
|
String json = AiResponseCleanTool.clean(aiResponse);
|
||||||
JsonNode root = HttpTool.objectMapper.readTree(json);
|
JsonNode root = HttpTool.objectMapper.readTree(json);
|
||||||
|
|
||||||
// valid 校验
|
// valid 校验
|
||||||
@@ -221,7 +222,7 @@ public class JobCleanService {
|
|||||||
"\n\n【专业分类列表】\n" + dictCacheService.getMajorCategoryText();
|
"\n\n【专业分类列表】\n" + dictCacheService.getMajorCategoryText();
|
||||||
|
|
||||||
String aiResponse = aiChatAbility.chat(systemPrompt, userMessage);
|
String aiResponse = aiChatAbility.chat(systemPrompt, userMessage);
|
||||||
String json = cleanAiResponse(aiResponse);
|
String json = AiResponseCleanTool.clean(aiResponse);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
JsonNode root = HttpTool.objectMapper.readTree(json);
|
JsonNode root = HttpTool.objectMapper.readTree(json);
|
||||||
@@ -280,7 +281,7 @@ public class JobCleanService {
|
|||||||
String userMessage = "【岗位信息】\n标题: " + title + "\n职责: " + description + "\n要求: " + requirement;
|
String userMessage = "【岗位信息】\n标题: " + title + "\n职责: " + description + "\n要求: " + requirement;
|
||||||
|
|
||||||
String aiResponse = aiChatAbility.chat(systemPrompt, userMessage);
|
String aiResponse = aiChatAbility.chat(systemPrompt, userMessage);
|
||||||
String json = cleanAiResponse(aiResponse);
|
String json = AiResponseCleanTool.clean(aiResponse);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
JsonNode arrayNode = HttpTool.objectMapper.readTree(json);
|
JsonNode arrayNode = HttpTool.objectMapper.readTree(json);
|
||||||
@@ -377,16 +378,6 @@ public class JobCleanService {
|
|||||||
return sb.toString();
|
return sb.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 清理AI返回的markdown代码块和控制字符 */
|
|
||||||
private String cleanAiResponse(String response) {
|
|
||||||
String json = response.trim();
|
|
||||||
if (json.startsWith("```")) {
|
|
||||||
json = json.replaceAll("^```\\w*\\n?", "").replaceAll("\\n?```$", "").trim();
|
|
||||||
}
|
|
||||||
json = json.replaceAll("[\\x00-\\x08\\x0B\\x0C\\x0E-\\x1F]", "");
|
|
||||||
return json;
|
|
||||||
}
|
|
||||||
|
|
||||||
private String nullToEmpty(String s) {
|
private String nullToEmpty(String s) {
|
||||||
return s == null ? "" : s;
|
return s == null ? "" : s;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import com.baomidou.mybatisplus.core.toolkit.IdWorker;
|
|||||||
import com.fasterxml.jackson.databind.JsonNode;
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.jiayunet.ai.AiChatAbility;
|
import org.jiayunet.ai.AiChatAbility;
|
||||||
|
import org.jiayunet.ai.AiResponseCleanTool;
|
||||||
import org.jiayunet.mapper.*;
|
import org.jiayunet.mapper.*;
|
||||||
import org.jiayunet.pojo.po.*;
|
import org.jiayunet.pojo.po.*;
|
||||||
import org.jiayunet.pojo.vo.UserHonorsVo;
|
import org.jiayunet.pojo.vo.UserHonorsVo;
|
||||||
@@ -145,7 +146,7 @@ public class UserProfileAnalyzeService {
|
|||||||
|
|
||||||
String userMessage = "【用户简历】\n" + profileJson;
|
String userMessage = "【用户简历】\n" + profileJson;
|
||||||
String aiResponse = aiChatAbility.chat(systemPrompt, userMessage);
|
String aiResponse = aiChatAbility.chat(systemPrompt, userMessage);
|
||||||
String json = cleanAiResponse(aiResponse);
|
String json = AiResponseCleanTool.clean(aiResponse);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
JsonNode root = HttpTool.objectMapper.readTree(json);
|
JsonNode root = HttpTool.objectMapper.readTree(json);
|
||||||
@@ -193,7 +194,7 @@ public class UserProfileAnalyzeService {
|
|||||||
String userMessage = "【用户简历】\n" + profileJson + "\n\n【专业分类列表】\n" + dictCacheService.getMajorCategoryText();
|
String userMessage = "【用户简历】\n" + profileJson + "\n\n【专业分类列表】\n" + dictCacheService.getMajorCategoryText();
|
||||||
|
|
||||||
String aiResponse = aiChatAbility.chat(systemPrompt, userMessage);
|
String aiResponse = aiChatAbility.chat(systemPrompt, userMessage);
|
||||||
String json = cleanAiResponse(aiResponse);
|
String json = AiResponseCleanTool.clean(aiResponse);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
JsonNode root = HttpTool.objectMapper.readTree(json);
|
JsonNode root = HttpTool.objectMapper.readTree(json);
|
||||||
@@ -253,7 +254,7 @@ public class UserProfileAnalyzeService {
|
|||||||
|
|
||||||
String userMessage = "【用户简历】\n" + profileJson;
|
String userMessage = "【用户简历】\n" + profileJson;
|
||||||
String aiResponse = aiChatAbility.chat(systemPrompt, userMessage);
|
String aiResponse = aiChatAbility.chat(systemPrompt, userMessage);
|
||||||
String json = cleanAiResponse(aiResponse);
|
String json = AiResponseCleanTool.clean(aiResponse);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
JsonNode arrayNode = HttpTool.objectMapper.readTree(json);
|
JsonNode arrayNode = HttpTool.objectMapper.readTree(json);
|
||||||
@@ -332,13 +333,4 @@ public class UserProfileAnalyzeService {
|
|||||||
relationMapper.batchInsert(relations);
|
relationMapper.batchInsert(relations);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 清理AI返回的markdown代码块和控制字符 */
|
|
||||||
private String cleanAiResponse(String response) {
|
|
||||||
String json = response.trim();
|
|
||||||
if (json.startsWith("```")) {
|
|
||||||
json = json.replaceAll("^```\\w*\\n?", "").replaceAll("\\n?```$", "").trim();
|
|
||||||
}
|
|
||||||
json = json.replaceAll("[\\x00-\\x08\\x0B\\x0C\\x0E-\\x1F]", "");
|
|
||||||
return json;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user