集成ai部分

This commit is contained in:
sh
2026-01-05 15:39:01 +08:00
parent deb775ff5c
commit a31fc4cc72
23 changed files with 1885 additions and 1 deletions

View File

@@ -184,3 +184,19 @@ oauth:
connect-timeout: 10
read-timeout: 30
write-timeout: 30
#ai
chat:
baseUrl: http://39.98.44.136:8082
chatUrl: /v1/chat/completions
chatDetailUrl: /core/chat/getPaginationRecords
chatHistoryUrl: /core/chat/getHistories
updateNameUrl: /core/chat/updateHistory
stickChatUrl: /core/chat/updateHistory
delChatUrl: /core/chat/delHistory
delAllChatUrl: /core/chat/clearHistories
guestUrl: /v1/chat/completions
praiseUrl: /core/chat/feedback/updateUserFeedback
appId: 67cd49095e947ae0ca7fadd8
apiKey: fastgpt-qMl63276wPZvKAxEkW77bur0sSJpmuC6Ngg9lzyEjufLhsBAurjT55j
model: qd-job-turbo

View File

@@ -0,0 +1,296 @@
package com.ruoyi.cms.config;
import com.alibaba.fastjson2.JSONArray;
import com.alibaba.fastjson2.JSONObject;
import com.ruoyi.cms.domain.chat.ChatRequest;
import com.ruoyi.common.utils.StringUtils;
import lombok.var;
import okhttp3.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
@Component
public class ChatClient {
// 超时设置(单位:秒)
private static final int CONNECT_TIMEOUT = 30;
private static final int WRITE_TIMEOUT = 30;
private static final int READ_TIMEOUT = 300; // 流式响应不设置读取超时
// 单例 OkHttp 客户端(复用连接池)
private static final OkHttpClient client = new OkHttpClient.Builder()
.connectTimeout(CONNECT_TIMEOUT, TimeUnit.SECONDS)
.writeTimeout(WRITE_TIMEOUT, TimeUnit.SECONDS)
.readTimeout(READ_TIMEOUT, TimeUnit.SECONDS)
.build();
private static final String CHAT_ENDPOINT = "chat";
private static final String CHAT_HISTORY = "history";
public static final List<String> TEXT_FILE_EXTENSIONS= Arrays.asList(".txt", ".md", ".html", ".doc", ".docx", ".pdf", ".ppt", ".pptx", ".csv", ".xls", ".xlsx");
public static final List<String> IMAGE_FILE_EXTENSIONS=Arrays.asList(".jpg", ".jpeg", ".png", ".gif", ".bmp", ".webp");
private ChatConfig chatConfig;
@Autowired
public void setChatConfig(ChatConfig chatConfig) {
this.chatConfig = chatConfig;
}
@Value("${spring.profiles.active}")
private String env;
/**
* 发送流式聊天请求
* @param chatRequest 查询请求体
* @param callback 流式响应回调接口
*/
public void sendStreamingChat(ChatRequest chatRequest, StreamCallback callback) {
String url=chatConfig.getBaseUrl()+chatConfig.getChatUrl();
// 构建请求体
String jsonBody = buildChatRequestBody(chatRequest, CHAT_ENDPOINT);
// 构建请求
Request request = null;
try {
RequestBody body = RequestBody.create(MediaType.parse("application/json; charset=utf-8"),jsonBody);
request = new Request.Builder()
.url(url)
.addHeader("Content-Type", "application/json")
.addHeader("Authorization", "Bearer " + chatConfig.getApiKey())
.post(body).build();
}catch (Exception e){
e.printStackTrace();
callback.onError(new RuntimeException("构建请求失败: " + e.getMessage(), e));
return;
}
// 发送异步请求
client.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
callback.onError(new RuntimeException("请求发送失败: " + e.getMessage(), e));
}
@Override
public void onResponse(Call call, Response response) throws IOException {
try {
if (!response.isSuccessful()) {
String errorBody = response.body() != null ? response.body().string() : "无错误信息";
String errorMsg = String.format("API 响应错误: 状态码=%d, 错误信息=%s",
response.code(), errorBody);
System.err.println(errorMsg); // 打印详细错误
callback.onError(new RuntimeException(errorMsg));
return;
}
// 处理流式响应
ResponseBody body = response.body();
if (body == null) {
callback.onError(new RuntimeException("响应体为空"));
return;
}
// 逐行读取 SSE 格式的响应
try (var bufferedSource = body.source()) {
while (!bufferedSource.exhausted()) {
String chunk = bufferedSource.readUtf8Line();
if (chunk != null && !chunk.trim().isEmpty()) {
callback.onData(chunk);
}
}
}
// 通知流结束
callback.onComplete();
} catch (Exception e) {
String errorMsg = "处理响应失败: " + e.getMessage();
System.err.println(errorMsg);
callback.onError(new RuntimeException(errorMsg, e));
} finally {
response.close();
}
}
});
}
/**
* 构建聊天请求的 JSON 体
*/
/**
* 构建聊天请求的 JSON 体
* 根据文档 [cite: 162-172] 修改为 Vision API 兼容格式
*/
private String buildChatRequestBody(ChatRequest chatRequest, String key) {
JSONObject chatObject = new JSONObject();
// 1. 设置会话ID (如果有)
if(StringUtils.isNotEmpty(chatRequest.getSessionId())){
chatObject.put("chatId", chatRequest.getSessionId());
}
if("chat".equals(key)){
// 基础参数设置
chatObject.put("stream", true);
chatObject.put("model", chatConfig.getModel()); // 需确保为 "qd-job-turbo"
chatObject.put("user", chatRequest.getSessionId());
// 2. 获取历史消息列表,如果为空则初始化
JSONArray messages = chatRequest.getMessages();
if (messages == null) {
messages = new JSONArray();
}
// 3. 构建当前用户的多模态消息内容 (Multimodal Content) [cite: 162]
JSONArray contentArray = new JSONArray();
// 3.1 添加文本内容 [cite: 166, 172]
if(StringUtils.isNotEmpty(chatRequest.getData())){
JSONObject textPart = new JSONObject();
textPart.put("type", "text");
textPart.put("text", chatRequest.getData());
contentArray.add(textPart);
}
// 3.2 添加文件内容 (图片、PDF、Excel等) [cite: 174, 180, 232]
// 文档说明image_url 字段兼容 PDF, Excel, PPT 等所有 OCR 支持的文件
if(!CollectionUtils.isEmpty(chatRequest.getFileUrl())){
for(String url : chatRequest.getFileUrl()){
String finalUrl = "";
if(Objects.equals(env, "pro")){
finalUrl = url.replace("https://fw.rc.qingdao.gov.cn/rgpp-api/api/ng", "http://10.213.6.207:19010");
}else {
finalUrl = url;
}
// 处理内网/外网地址映射 (保留你原有的逻辑)
JSONObject filePart = new JSONObject();
filePart.put("type", "image_url"); // 固定为 image_url [cite: 174]
JSONObject imageUrlObj = new JSONObject();
imageUrlObj.put("url", finalUrl); // 文件地址 [cite: 172]
filePart.put("image_url", imageUrlObj);
contentArray.add(filePart);
}
}
// 4. 将当前消息封装为 User Message 对象并加入消息列表 [cite: 151, 163]
// 只有当有内容(文本或文件)时才添加
if (!contentArray.isEmpty()) {
JSONObject currentUserMessage = new JSONObject();
currentUserMessage.put("role", "user");
currentUserMessage.put("content", contentArray); // content 为数组格式
messages.add(currentUserMessage);
}
// 5. 将完整的消息列表放入请求体 [cite: 135]
chatObject.put("messages", messages);
} else {
// 非 chat 场景的逻辑保留
chatObject.put("appId", chatConfig.getAppId());
}
return chatObject.toJSONString();
}
/**
* 简单的 JSON 转义处理(防止特殊字符破坏 JSON 格式)
*/
private String escapeJson(String value) {
if (value == null) return "";
return value
.replace("\\", "\\\\")
.replace("\"", "\\\"")
.replace("\b", "\\b")
.replace("\f", "\\f")
.replace("\n", "\\n")
.replace("\r", "\\r")
.replace("\t", "\\t");
}
/**
* 发送聊天请求并返回完整JSON响应
* @param chatRequest 用户输入的查询内容
* @return 完整的JSON响应字符串
* @throws IOException 网络请求异常
*/
public String sendChatGuest(ChatRequest chatRequest) throws IOException {
String url=chatConfig.getBaseUrl()+chatConfig.getGuestUrl();
JSONArray array = chatRequest.getMessages();
if(array==null||array.isEmpty()||array.size()==0){
array = new JSONArray();
}
JSONObject contentObject = new JSONObject();
contentObject.put("content","你是一个岗位招聘专家请根据用户的问题生成用户下一步想要提出的问题。需要以用户的口吻进行生成。结合上下文中用户提出的问题以及助手回复的答案需要猜测用户更进一步的需求例如期望的薪资期望的工作地点掌握的技能。生成的问题举例有没有薪资在9000以上的工作我的学历是本科。我希望找国企。注意不仅限于这些还要根据上下文。其次所有的问题应该限定在青岛。并且只生成3到4个。");
contentObject.put("role","system");
array.add(contentObject);
JSONObject jsonBody = new JSONObject();
jsonBody.put("stream",false);
jsonBody.put("model",chatConfig.getModel());
jsonBody.put("messages",array);
// 构建请求(使用非流式响应模式)
RequestBody requestBody = RequestBody.create(MediaType.parse("application/json; charset=utf-8"), String.valueOf(jsonBody));
Request request = new Request.Builder()
.url(url)
.addHeader("Content-Type", "application/json")
.addHeader("Authorization", "Bearer " + chatConfig.getApiKey())
.post(requestBody).build();
// 发送同步请求
try (Response response = client.newCall(request).execute()) {
if (!response.isSuccessful()) {
String errorBody = response.body() != null ? response.body().string() : "无错误信息";
String errorMsg = String.format("API 响应错误: 状态码=%d, 错误信息=%s",
response.code(), errorBody);
throw new IOException(errorMsg);
}
ResponseBody body = response.body();
if (body == null) {
throw new IOException("响应体为空");
}
JSONObject object = JSONObject.parseObject(body.string());
String choices = object.getString("choices");
if (choices != null && !choices.trim().isEmpty()) {
JSONArray jsonArray = JSONArray.parseArray(choices);
object = JSONObject.parseObject(jsonArray.getString(0));
object = object.getJSONObject("message");
String content = object.getString("content");// 消息内容
return content;
}
return body.string();
}
}
/**
* 流式响应回调接口
*/
public interface StreamCallback {
/**
* 接收分片数据
* @param chunk SSE 格式的分片数据
*/
void onData(String chunk);
/**
* 响应结束
*/
void onComplete();
/**
* 发生错误
* @param e 异常信息
*/
void onError(Throwable e);
}
}

View File

@@ -0,0 +1,115 @@
package com.ruoyi.cms.config;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@Component
@ConfigurationProperties(prefix = "chat")
public class ChatConfig {
private String baseUrl;
private String chatUrl;
private String chatDetailUrl;
private String chatHistoryUrl;
private String updateNameUrl;
private String delChatUrl;
private String delAllChatUrl;
private String guestUrl;
private String praiseUrl;
private String apiKey;
private String appId;
private String model;
public String getBaseUrl() {
return baseUrl;
}
public void setBaseUrl(String baseUrl) {
this.baseUrl = baseUrl;
}
public String getChatUrl() {
return chatUrl;
}
public void setChatUrl(String chatUrl) {
this.chatUrl = chatUrl;
}
public String getChatDetailUrl() {
return chatDetailUrl;
}
public void setChatDetailUrl(String chatDetailUrl) {
this.chatDetailUrl = chatDetailUrl;
}
public String getChatHistoryUrl() {
return chatHistoryUrl;
}
public void setChatHistoryUrl(String chatHistoryUrl) {
this.chatHistoryUrl = chatHistoryUrl;
}
public String getUpdateNameUrl() {
return updateNameUrl;
}
public void setUpdateNameUrl(String updateNameUrl) {
this.updateNameUrl = updateNameUrl;
}
public String getDelChatUrl() {
return delChatUrl;
}
public void setDelChatUrl(String delChatUrl) {
this.delChatUrl = delChatUrl;
}
public String getDelAllChatUrl() {
return delAllChatUrl;
}
public void setDelAllChatUrl(String delAllChatUrl) {
this.delAllChatUrl = delAllChatUrl;
}
public String getGuestUrl() {
return guestUrl;
}
public void setGuestUrl(String guestUrl) {
this.guestUrl = guestUrl;
}
public String getPraiseUrl() {
return praiseUrl;
}
public void setPraiseUrl(String praiseUrl) {
this.praiseUrl = praiseUrl;
}
public String getApiKey() {
return apiKey;
}
public void setApiKey(String apiKey) {
this.apiKey = apiKey;
}
public String getAppId() {
return appId;
}
public void setAppId(String appId) {
this.appId = appId;
}
public String getModel() {
return model;
}
public void setModel(String model) {
this.model = model;
}
}

View File

@@ -0,0 +1,235 @@
package com.ruoyi.cms.controller.app;
import cn.hutool.core.util.ObjectUtil;
import com.alibaba.fastjson2.JSONArray;
import com.alibaba.fastjson2.JSONObject;
import com.ruoyi.cms.domain.ai.AiChatHistory;
import com.ruoyi.cms.domain.chat.ChatRequest;
import com.ruoyi.cms.service.AiChatHistoryService;
import com.ruoyi.common.annotation.BussinessLog;
import com.ruoyi.cms.config.ChatClient;
import com.ruoyi.cms.config.ChatConfig;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.utils.SiteSecurityUtils;
import com.ruoyi.common.utils.StringUtils;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import javax.annotation.PreDestroy;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import static com.ruoyi.common.enums.BusinessType.QUERY;
@RestController
@RequestMapping(value = "/app/chat")
@Slf4j
@Api(tags = "移动端:ai对话")
public class ChatController extends BaseController {
@Autowired
ChatClient chatClient;
@Autowired
ChatConfig chatConfig;
@Autowired
AiChatHistoryService aiChatHistoryService;
//private final ExecutorService executor = Executors.newCachedThreadPool();
// 可优化线程池配置,避免无限创建线程
private final ExecutorService executor = new ThreadPoolExecutor(
2, // 核心线程数
10, // 最大线程数
30, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(50), // 任务队列
new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略(避免任务丢失)
);
@BussinessLog(title = "查询用户的聊天历史记录",businessType = QUERY)
@ApiOperation("查询用户的聊天历史记录")
@GetMapping(value = "/getHistory")
public AjaxResult getChatHistoryList(AiChatHistory chatHistory) {
return AjaxResult.success(aiChatHistoryService.getList(chatHistory));
}
@BussinessLog(title = "查询用户的聊天详情",businessType = QUERY)
@ApiOperation("查询用户的聊天详情")
@GetMapping(value = "/detail")
public AjaxResult getChatDetail(ChatRequest request) {
return AjaxResult.success(aiChatHistoryService.getDetailList(request.getSessionId()));
}
// 处理前端聊天请求,返回 SSE 发射器
@ApiOperation("用户AI聊天")
@PostMapping("/chat")
@ResponseBody
@BussinessLog(title = "用户AI聊天",businessType = QUERY)
public SseEmitter chatStream(@RequestBody ChatRequest request) {
// 设置超时时间30分钟
SseEmitter emitter = new SseEmitter(1800000L);
long userId = 0;
if(ObjectUtil.isNotEmpty(request.getUserId())&&request.getUserId()!=0){
userId = request.getUserId();
}else{
try {
userId = SiteSecurityUtils.getLoginUser().getUserId();
}catch (Exception e) {
}
}
JSONObject contentObject = new JSONObject();
contentObject.put("content",request.getData());
contentObject.put("role","user");
JSONArray array = aiChatHistoryService.getChatHistoryData(request.getSessionId());
if(array==null||array.isEmpty()||array.size()==0){
array = new JSONArray();
}
array.add(contentObject);
request.setMessages(array);
List<String> answerList = new ArrayList<>();
long[] timeStart = {0};
// 异步处理请求并推送数据
executor.submit(() -> {
try {
// 2. 调用ChatClient的流式聊天方法正确传递参数
chatClient.sendStreamingChat(
request,
new ChatClient.StreamCallback() { // 使用内部类完整路径
@Override
public void onData(String chunk) {
try {
if(timeStart[0] == 0){
timeStart[0] = System.currentTimeMillis();
}
// 1. 处理SSE协议格式
String processedChunk = chunk.trim();
// 解析返回的chunk数据
String content = parseChatChunk(processedChunk);
if (StringUtils.isNotEmpty(content)) {
answerList.add(content);
}
emitter.send(processedChunk, MediaType.TEXT_EVENT_STREAM);
} catch (IOException e) {
emitter.completeWithError(e);
}
}
@Override
public void onComplete() {
emitter.complete();
}
@Override
public void onError(Throwable e) {
try {
emitter.send(SseEmitter.event()
.data("{\"status\":\"error\",\"message\":\"" + e.getMessage() + "\"}")
.name("error"));
} catch (IOException ex) {
// 记录日志
} finally {
emitter.completeWithError(e);
}
}
}
);
} catch (Exception e) {
emitter.completeWithError(e);
}
});
// 处理连接关闭
long finalUserId = userId;
emitter.onCompletion(() -> {
// 此处仅做资源清理,不关闭线程池
log.info("连接关闭,清理资源");
long timeEnd = System.currentTimeMillis();
double duration = (timeEnd - timeStart[0]) / 1000.0;
AiChatHistory chatHistory = new AiChatHistory();
chatHistory.setChatId(request.getSessionId());
chatHistory.setUserId((finalUserId==0)?null:finalUserId);
chatHistory.setAppId(chatConfig.getAppId());
chatHistory.setDataId(request.getDataId());
chatHistory.setTitle(request.getData());
chatHistory.setAnswerStringList(answerList);
chatHistory.setDurationSeconds(duration);
aiChatHistoryService.saveChatHistory(chatHistory);
});
return emitter;
}
@BussinessLog(title = "提供推测出的问询建议",businessType = QUERY)
@ApiOperation("提供推测出的问询建议")
@PostMapping(value = "/guest")
public AjaxResult getChatGuest(@RequestBody ChatRequest request) throws IOException {
JSONArray array = aiChatHistoryService.getChatHistoryData(request.getSessionId());
request.setMessages(array);
String result = chatClient.sendChatGuest(request);
try {
String[] strList = result.split("");
List<String> list = new ArrayList<>();
for(String str:strList){
if(StringUtils.isNotEmpty(str)){
str = str+"";
list.add(str);
}
}
return AjaxResult.success(list);
}catch (Exception e) {
return AjaxResult.error(e.getMessage());
}
}
// 解析返回的流式数据
private String parseChatChunk(String chunk) {
try {
String processed = chunk.trim();
if (processed.startsWith("data:")) {
processed = processed.substring("data:".length()).trim();
}
if (processed.isEmpty() || "[DONE]".equals(processed)) {
return null;
}
JSONObject json = JSONObject.parseObject(processed);
String choices = json.getString("choices");
if (choices != null && !choices.trim().isEmpty()) {
JSONArray jsonArray = JSONArray.parseArray(choices);
json = JSONObject.parseObject(jsonArray.getString(0));
json = json.getJSONObject("delta");
String content = json.getString("content");// 消息内容
return content;
}
return null;
} catch (Exception e) {
return null;
}
}
@PreDestroy
public void destroy() {
executor.shutdown(); // 应用退出前关闭线程池
try {
if (!executor.awaitTermination(5, TimeUnit.SECONDS)) {
executor.shutdownNow(); // 强制关闭未完成的任务
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
executor.shutdownNow();
log.error(e.getMessage());
}
}
}

View File

@@ -0,0 +1,37 @@
package com.ruoyi.cms.domain.ai;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.util.Date;
@Data
@ApiModel("ai聊天详情")
@TableName("AI_CHAT_DETAIL")
public class AiChatDetail {
@TableField(exist = false)
private static final long serialVersionUID = 1L;
@TableId(value = "id",type = IdType.AUTO)
@ApiModelProperty("id")
private Long id;
@ApiModelProperty("会话id")
private String chatId;
@ApiModelProperty("数据id")
private String dataId;
@ApiModelProperty("会话类型Human用户AI大模型")
private String obj;
@ApiModelProperty("会话内容")
private String content;
@ApiModelProperty("会话时间")
private Date time;
@ApiModelProperty("耗时")
private Double durationSeconds;
}

View File

@@ -0,0 +1,53 @@
package com.ruoyi.cms.domain.ai;
import com.alibaba.fastjson2.annotation.JSONField;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.util.Date;
import java.util.List;
@Data
@ApiModel("ai聊天历史记录")
@TableName("AI_CHAT_HISTORY")
public class AiChatHistory{
@TableField(exist = false)
private static final long serialVersionUID = 1L;
@TableId(value = "id",type = IdType.AUTO)
@ApiModelProperty("id")
private Long id;
@ApiModelProperty("用户id")
private Long userId;
@JSONField(name = "chatId")
@ApiModelProperty("会话id")
private String chatId;
@ApiModelProperty("应用id")
private String appId;
@JSONField(name = "title")
@ApiModelProperty("第一次的问题")
private String title;
@JSONField(name = "updateTime")
@ApiModelProperty("会话时间")
private Date updateTime;
@ApiModelProperty("是否删除")
private String delFlag;
@TableField(exist = false)
private List<String> answerStringList;
@TableField(exist = false)
private double durationSeconds;
@TableField(exist = false)
private String customTitle;
@TableField(exist = false)
private String dataId;
@TableField(exist = false)
private boolean top=false;
}

View File

@@ -0,0 +1,17 @@
package com.ruoyi.cms.domain.chat;
import com.alibaba.fastjson2.JSONArray;
import lombok.Data;
import java.util.List;
@Data
public class ChatRequest {
private String data;
private String dataId;
private String sessionId;
private long userId;
private List<String> fileUrl;
private JSONArray messages;
}

View File

@@ -0,0 +1,13 @@
package com.ruoyi.cms.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ruoyi.cms.domain.ai.AiChatDetail;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface AiChatDetailMapper extends BaseMapper<AiChatDetail> {
List<AiChatDetail> getList(AiChatDetail aiChatDetail);
}

View File

@@ -0,0 +1,13 @@
package com.ruoyi.cms.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ruoyi.cms.domain.ai.AiChatHistory;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface AiChatHistoryMapper extends BaseMapper<AiChatHistory> {
List<AiChatHistory> getList(AiChatHistory aiChatHistory);
}

View File

@@ -0,0 +1,18 @@
package com.ruoyi.cms.service;
import com.alibaba.fastjson2.JSONArray;
import com.alibaba.fastjson2.JSONObject;
import com.baomidou.mybatisplus.extension.service.IService;
import com.ruoyi.cms.domain.ai.AiChatHistory;
public interface AiChatHistoryService extends IService<AiChatHistory> {
JSONObject getList(AiChatHistory aiChatHistory);
void saveChatHistory(AiChatHistory aiChatHistory);
JSONObject getDetailList(String chatId);
JSONArray getChatHistoryData(String chatId);
}

View File

@@ -0,0 +1,191 @@
package com.ruoyi.cms.service.impl;
import com.alibaba.fastjson2.JSONArray;
import com.alibaba.fastjson2.JSONObject;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.ruoyi.cms.domain.ai.AiChatDetail;
import com.ruoyi.cms.domain.ai.AiChatHistory;
import com.ruoyi.cms.mapper.AiChatDetailMapper;
import com.ruoyi.cms.mapper.AiChatHistoryMapper;
import com.ruoyi.cms.service.AiChatHistoryService;
import com.ruoyi.common.utils.SiteSecurityUtils;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.uuid.IdUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
@Service
public class AiChatHistoryServiceImpl extends ServiceImpl<AiChatHistoryMapper, AiChatHistory> implements AiChatHistoryService {
@Autowired
private AiChatHistoryMapper aiChatHistoryMapper;
@Autowired
private AiChatDetailMapper aiChatDetailMapper;
@Override
public JSONObject getList(AiChatHistory aiChatHistory) {
JSONObject object = new JSONObject();
JSONArray jsonArray = new JSONArray();
if(Objects.isNull(aiChatHistory.getUserId())){
try {
aiChatHistory.setUserId(SiteSecurityUtils.getUserId());
}catch (Exception e){
object.put("list", jsonArray);
return object;
}
}
List<AiChatHistory> list = aiChatHistoryMapper.getList(aiChatHistory);
if(list!=null&&!list.isEmpty()){
JSONObject jsonObject1 = new JSONObject();
for(AiChatHistory history:list){
jsonObject1 = JSONObject.parseObject(JSONObject.toJSONString(history));
jsonArray.add(jsonObject1);
}
}
object.put("list", jsonArray);
return object;
}
@Override
public void saveChatHistory(AiChatHistory aiChatHistory) {
try {
AiChatHistory history = new AiChatHistory();
history.setChatId(aiChatHistory.getChatId());
List<AiChatHistory> list = aiChatHistoryMapper.getList(history);
if(list!=null&&!list.isEmpty()){
history = list.get(0);
history.setUpdateTime(new Date());
aiChatHistoryMapper.updateById(history);
}else{
history.setTitle(aiChatHistory.getTitle());
history.setUserId(aiChatHistory.getUserId());
history.setAppId(aiChatHistory.getAppId());
history.setDelFlag("0");
history.setUpdateTime(new Date());
aiChatHistoryMapper.insert(history);
}
AiChatDetail chatDetail = new AiChatDetail();
chatDetail.setChatId(aiChatHistory.getChatId());
chatDetail.setObj("Human");
chatDetail.setTime(new Date());
chatDetail.setDataId(IdUtils.fastSimpleUUID());
chatDetail.setContent(aiChatHistory.getTitle());
aiChatDetailMapper.insert(chatDetail);
List<String> answerList = aiChatHistory.getAnswerStringList();
if(answerList!=null&&!answerList.isEmpty()){
chatDetail = new AiChatDetail();
chatDetail.setChatId(aiChatHistory.getChatId());
chatDetail.setObj("AI");
chatDetail.setTime(new Date());
chatDetail.setDataId(aiChatHistory.getDataId());
chatDetail.setContent(String.join("",answerList));
chatDetail.setDurationSeconds(aiChatHistory.getDurationSeconds());
aiChatDetailMapper.insert(chatDetail);
}
}catch (Exception e){
e.printStackTrace();
}
}
@Override
public JSONObject getDetailList(String chatId) {
JSONObject dataList = new JSONObject();
JSONArray array = new JSONArray();
AiChatDetail detail = new AiChatDetail();
detail.setChatId(chatId);
List<AiChatDetail> list = aiChatDetailMapper.getList(detail);
if(list!=null&&!list.isEmpty()){
JSONObject data = new JSONObject();
JSONArray value = new JSONArray();
JSONObject valueObject = new JSONObject();
String dataId = "";
List<String> contentList = new ArrayList<>();
List<AiChatDetail> details = new ArrayList<>();
for(AiChatDetail detail1:list){
details = list.stream().filter(fi->fi.getDataId().equals(detail1.getDataId())).collect(Collectors.toList());
if(details.size() > 1){
contentList = new ArrayList<>();
if(StringUtils.isNotEmpty(dataId)){
continue;
}
data = new JSONObject();
data.put("dataId", detail1.getDataId());
data.put("obj", detail1.getObj());
data.put("hideInUI",false);
data.put("customFeedbacks",new JSONArray());
data.put("time",detail1.getTime());
data.put("durationSeconds",detail1.getDurationSeconds());
value = new JSONArray();
dataId = detail1.getDataId();
for(AiChatDetail detail2:details){
if(detail2.getContent().contains("```")){
contentList.add(detail2.getContent());
}else{
if(contentList!=null&&!contentList.isEmpty()){
valueObject = new JSONObject();
valueObject.put("type","text");
valueObject.put("text",new JSONObject().fluentPut("content", String.join("",contentList)));
value.add(valueObject);
}
valueObject = new JSONObject();
valueObject.put("type","text");
valueObject.put("text",new JSONObject().fluentPut("content", detail2.getContent()));
value.add(valueObject);
}
}
data.put("value",value);
array.add(data);
}else{
dataId = "";
data = new JSONObject();
data.put("dataId", detail1.getDataId());
data.put("obj", detail1.getObj());
data.put("hideInUI",false);
data.put("customFeedbacks",new JSONArray());
data.put("time",detail1.getTime());
if("AI".equals(detail1.getObj())){
data.put("durationSeconds",detail1.getDurationSeconds());
}
valueObject = new JSONObject();
valueObject.put("type","text");
valueObject.put("text",new JSONObject().fluentPut("content", detail1.getContent()));
value = new JSONArray();
value.add(valueObject);
data.put("value",value);
array.add(data);
}
}
}
dataList.put("list",array);
return dataList;
}
@Override
public JSONArray getChatHistoryData(String chatId) {
JSONObject content = null;
JSONArray data = new JSONArray();
AiChatDetail detail = new AiChatDetail();
detail.setChatId(chatId);
List<AiChatDetail> list = aiChatDetailMapper.getList(detail);
if(list!=null&&!list.isEmpty()){
for(AiChatDetail d:list){
content = new JSONObject();
if("AI".equals(d.getObj())){
content.put("role","assistant");
}else{
content.put("role","user");
}
content.put("content",d.getContent());
data.add(content);
}
}
return data;
}
}

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ruoyi.cms.mapper.AiChatDetailMapper">
<select id="getList" parameterType="com.ruoyi.cms.domain.ai.AiChatDetail" resultType="com.ruoyi.cms.domain.ai.AiChatDetail">
select * from ai_chat_detail where chat_id=#{chatId} order by time
</select>
</mapper>

View File

@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ruoyi.cms.mapper.AiChatHistoryMapper">
<select id="getList" parameterType="com.ruoyi.cms.domain.ai.AiChatHistory" resultType="com.ruoyi.cms.domain.ai.AiChatHistory">
select * from ai_chat_history where del_flag=0
<if test="userId != null and userId != ''">
and user_id = #{userId}
</if>
<if test="chatId != null and chatId != ''">
and chat_id = #{chatId}
</if>
<if test="appId != null and appId != ''">
and app_id = #{appId}
</if>
<if test="title != null and title != ''">
and title like CONCAT('%',#{title},'%')
</if>
<if test="updateTime != null">
and to_char(update_time,'yyyy-mm-dd') = to_char(#{updateTime},'yyyy-mm-dd')
</if>
</select>
</mapper>

View File

@@ -120,6 +120,22 @@
<artifactId>UserAgentUtils</artifactId>
</dependency>
<!--奇安信密码机-->
<!--<dependency>
<groupId>org.quickssl</groupId>
<artifactId>quickapi-client-java</artifactId>
<version>1.5.11-SNAPSHOT</version>
<classifier>shaded</classifier>
<scope>compile</scope>
</dependency>-->
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk15on</artifactId>
<version>1.59</version>
<scope>compile</scope>
</dependency>
<!-- servlet包 -->
<dependency>
<groupId>javax.servlet</groupId>

View File

@@ -0,0 +1,36 @@
package com.ruoyi.common.constant;
import java.util.Arrays;
import java.util.List;
/**
* 加解密配置常量
*/
public class EncryptConstants {
/**
* 是否开启加解密功能
*/
public static final boolean ENCRYPT_ENABLED = true;
/**
* 需要加解密的URL路径模式
*/
public static final List<String> URL_PATTERNS = Arrays.asList(
/*"/app/login",
"/app/user/resume",
"/app/user/experience/edit",
"/app/user/experience/delete",
"/app/user/experience/getSingle/*",
"/app/user/experience/list",
"/login",
"/system/user/resetPwd",
"/system/user/list",
"/system/user",
"/cms/appUser/list",
"/cms/appUser/getResumeList",
"/cms/appUser/getResumeDetail/*",
"/app/alipay/scanLogin",
"/app/user/cert"*/
);
}

View File

@@ -0,0 +1,6 @@
package com.ruoyi.common.constant;
public class SM4Constants {
public static final String SM4_KET = "86C63180C1306ABC4D8F989E0A0BC9F3";
}

View File

@@ -56,4 +56,8 @@ public enum BusinessType
* 清空数据
*/
CLEAN,
/**
* 查询
*/
QUERY
}

View File

@@ -0,0 +1,123 @@
package com.ruoyi.common.filter;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.ruoyi.common.constant.EncryptConstants;
import com.ruoyi.common.constant.SM4Constants;
import com.ruoyi.common.utils.EncryptHttpServletResponseWrapper;
import com.ruoyi.common.utils.SM4Utils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.PathMatcher;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
public class EncryptResponseFilter implements Filter {
private static final ObjectMapper objectMapper = new ObjectMapper();
private final PathMatcher pathMatcher = new AntPathMatcher();
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpServletResponse httpResponse = (HttpServletResponse) response;
// 检查是否开启加解密功能
if (!EncryptConstants.ENCRYPT_ENABLED) {
chain.doFilter(request, response);
return;
}
// 检查是否是SSE流式响应 - 不处理
if (isSseResponse(httpRequest, httpResponse)) {
chain.doFilter(request, response);
return;
}
// 检查URL是否需要加密响应
if (!needProcess(httpRequest)) {
chain.doFilter(request, response);
return;
}
// 包装响应
EncryptHttpServletResponseWrapper responseWrapper = new EncryptHttpServletResponseWrapper(httpResponse);
chain.doFilter(request, responseWrapper);
// 获取原始响应内容
byte[] content = responseWrapper.getContent();
String originalResponse = new String(content, StandardCharsets.UTF_8);
// 加密响应内容
if (StringUtils.isNotBlank(originalResponse) && !originalResponse.trim().isEmpty()) {
String encryptedResponse = SM4Utils.encryptEcb(SM4Constants.SM4_KET, originalResponse);
// 构建加密响应
Map<String, Object> result = new HashMap<>();
result.put("encrypted", true);
result.put("encryptedData", encryptedResponse);
result.put("timestamp", System.currentTimeMillis());
String finalResponse = objectMapper.writeValueAsString(result);
// 设置响应
byte[] encryptedBytes = finalResponse.getBytes(StandardCharsets.UTF_8);
httpResponse.setContentLength(encryptedBytes.length);
httpResponse.setContentType("application/json;charset=UTF-8");
httpResponse.getOutputStream().write(encryptedBytes);
} else {
// 空响应直接返回空
httpResponse.getOutputStream().write(new byte[0]);
}
}
/**
* 判断请求是否需要处理
*/
private boolean needProcess(HttpServletRequest request) {
String requestURI = request.getRequestURI();
// 检查URL是否匹配需要加解密的模式
for (String pattern : EncryptConstants.URL_PATTERNS) {
if (pathMatcher.match(pattern, requestURI)) {
return true;
}
}
return false;
}
/**
* 判断是否是SSE流式响应
*/
private boolean isSseResponse(HttpServletRequest request, HttpServletResponse response) {
String path = request.getRequestURI();
String method = request.getMethod();
// 检查是否是聊天接口
if ("/app/chat/chat".equals(path) && "POST".equalsIgnoreCase(method)) {
return true;
}
// 检查Accept头是否包含text/event-stream
String acceptHeader = request.getHeader("Accept");
if (acceptHeader != null && acceptHeader.contains("text/event-stream")) {
return true;
}
// 检查响应头是否已经设置为text/event-stream
String contentType = response.getContentType();
if (contentType != null && contentType.contains("text/event-stream")) {
return true;
}
return false;
}
}

View File

@@ -0,0 +1,268 @@
package com.ruoyi.common.filter;
import com.alibaba.fastjson2.JSONObject;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.ruoyi.common.constant.EncryptConstants;
import com.ruoyi.common.constant.SM4Constants;
import com.ruoyi.common.utils.EncryptHttpServletRequestWrapper;
import com.ruoyi.common.utils.EncryptHttpServletResponseWrapper;
import com.ruoyi.common.utils.SM4Utils;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.PathMatcher;
import org.springframework.web.client.RestTemplate;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
/**
* 请求包装过滤器 - 处理请求解密和包装
*/
@Component
@Slf4j
public class RequestWrapperFilter implements Filter {
private static final ObjectMapper objectMapper = new ObjectMapper();
private final PathMatcher pathMatcher = new AntPathMatcher();
private final RestTemplate restTemplate = new RestTemplate();
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpServletResponse httpResponse = (HttpServletResponse) response;
// 检查是否开启加解密功能
if (!EncryptConstants.ENCRYPT_ENABLED) {
chain.doFilter(request, response);
return;
}
// 检查是否是SSE流式响应 - 不处理
if (isSseResponse(httpRequest, httpResponse)) {
chain.doFilter(request, response);
return;
}
// 检查URL是否需要解密
if (needProcess(httpRequest)) {
// 处理请求解密
String method = httpRequest.getMethod();
if("POST".equalsIgnoreCase(method)) {
HttpServletRequest processedRequest = processBodyRequest(httpRequest);
chain.doFilter(processedRequest, response);
}else{
EncryptHttpServletResponseWrapper responseWrapper = new EncryptHttpServletResponseWrapper(httpResponse);
String forwardUrl = buildGetRequestURI(httpRequest);
if (StringUtils.isNotBlank(forwardUrl)) {
log.info("GET请求解密后转发URL{}", forwardUrl);
// 服务器内部转发
request.setAttribute("ENCRYPT_PROCESSED", Boolean.TRUE);
RequestDispatcher dispatcher = request.getRequestDispatcher(forwardUrl);
dispatcher.forward(request, responseWrapper);
// 2. 监听/处理转发后的响应(核心逻辑:从包装器中获取响应数据)
byte[] content = responseWrapper.getContent();
String originalResponse = new String(content, StandardCharsets.UTF_8);
if (StringUtils.isNotBlank(originalResponse) && !originalResponse.trim().isEmpty()) {
String encryptedResponse = SM4Utils.encryptEcb(SM4Constants.SM4_KET, originalResponse);
// 构建加密响应
Map<String, Object> result = new HashMap<>();
result.put("encrypted", true);
result.put("encryptedData", encryptedResponse);
result.put("timestamp", System.currentTimeMillis());
String finalResponse = objectMapper.writeValueAsString(result);
// 设置响应
byte[] encryptedBytes = finalResponse.getBytes(StandardCharsets.UTF_8);
httpResponse.setContentLength(encryptedBytes.length);
httpResponse.setContentType("application/json;charset=UTF-8");
httpResponse.getOutputStream().write(encryptedBytes);
} else {
// 空响应直接返回空
httpResponse.getOutputStream().write(new byte[0]);
}
return;
} else {
chain.doFilter(request, response);
}
}
} else {
// 不需要解密,直接放行
chain.doFilter(request, response);
}
}
/**
* 判断请求是否需要处理
*/
private boolean needProcess(HttpServletRequest request) {
if (Boolean.TRUE.equals(request.getAttribute("ENCRYPT_PROCESSED"))) {
return false;
}
String requestURI = request.getRequestURI();
// 检查URL是否匹配需要加解密的模式
for (String pattern : EncryptConstants.URL_PATTERNS) {
if (pathMatcher.match(pattern, requestURI)) {
return true;
}
}
return false;
}
/**
* 判断是否是SSE流式响应
*/
private boolean isSseResponse(HttpServletRequest request, HttpServletResponse response) {
String path = request.getRequestURI();
String method = request.getMethod();
// 检查是否是聊天接口
if ("/app/chat/chat".equals(path) && "POST".equalsIgnoreCase(method)) {
return true;
}
// 检查Accept头是否包含text/event-stream
String acceptHeader = request.getHeader("Accept");
if (acceptHeader != null && acceptHeader.contains("text/event-stream")) {
return true;
}
// 检查响应头是否已经设置为text/event-stream
String contentType = response.getContentType();
if (contentType != null && contentType.contains("text/event-stream")) {
return true;
}
return false;
}
private HttpServletRequest processBodyRequest(HttpServletRequest request) throws IOException {
String body = getRequestBody(request);
log.info("过滤器 - 原始请求体: " + body);
if (StringUtils.isNotBlank(body)) {
try {
// 解析请求体提取encryptedData字段
JsonNode jsonNode = objectMapper.readTree(body);
JsonNode encryptedDataNode = jsonNode.get("encryptedData");
if (encryptedDataNode != null && encryptedDataNode.isTextual()) {
String encryptedData = encryptedDataNode.asText();
// 解密 encryptedData
String decryptedData = SM4Utils.decryptEcb(SM4Constants.SM4_KET, encryptedData);
// 使用解密后的数据创建包装请求
return new EncryptHttpServletRequestWrapper(request, decryptedData);
} else {
log.info("请求体中未找到encryptedData字段直接返回原始请求");
return request;
}
} catch (Exception e) {
log.error("POST请求体解密失败: " + e.getMessage());
// 解密失败时返回原始请求,让业务层处理
return request;
}
}
return request;
}
private String getRequestBody(HttpServletRequest request) throws IOException {
StringBuilder stringBuilder = new StringBuilder();
BufferedReader bufferedReader = null;
try {
InputStream inputStream = request.getInputStream();
if (inputStream != null) {
bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
char[] charBuffer = new char[128];
int bytesRead;
while ((bytesRead = bufferedReader.read(charBuffer)) > 0) {
stringBuilder.append(charBuffer, 0, bytesRead);
}
}
} finally {
if (bufferedReader != null) {
bufferedReader.close();
}
}
return stringBuilder.toString();
}
private String buildGetRequestURI(HttpServletRequest request) {
String baseUrl = request.getRequestURI(); // 完整基础URL
String encryptedData = request.getParameter("encryptedData");
if (StringUtils.isBlank(encryptedData)) {
log.info("GET请求未携带encryptedData参数返回原始URL");
return null; // 无加密参数,不处理
}
try {
// 解密参数增加异常捕获避免解密失败导致URL拼接异常
String decryptedData = SM4Utils.decryptEcb(SM4Constants.SM4_KET, encryptedData);
if (StringUtils.isBlank(decryptedData)) {
log.error("GET请求encryptedData解密后为空");
return null;
}
JSONObject object = JSONObject.parseObject(decryptedData);
if (object.isEmpty()) {
log.info("解密后的参数为空返回原始URL");
return null;
}
// 拼接参数用StringBuilder避免字符串频繁拼接
StringBuilder params = new StringBuilder();
for (Map.Entry<String, Object> entry : object.entrySet()) { // 用entrySet更高效
String key = entry.getKey();
Object value = entry.getValue();
// 跳过空值和空key
if (StringUtils.isBlank(key) || Objects.isNull(value)) {
continue;
}
// 处理参数值:数组/嵌套JSON转为JSON字符串普通类型直接转字符串
String paramValue = String.valueOf(value);
// URL编码处理中文、特殊字符如空格、&、=等)
String encodedValue = URLEncoder.encode(paramValue, "UTF-8");
if (params.length() > 0) {
params.append("&");
}
params.append(key).append("=").append(encodedValue);
}
// 拼接完整URL仅当有参数时才加?
if (params.length() > 0) {
return baseUrl + "?" + params.toString();
} else {
log.info("解密后的参数均为空返回原始URL");
return null;
}
} catch (Exception e) {
log.error("GET请求参数解密/拼接失败", e);
return null; // 异常时返回null走原始请求
}
}
}

View File

@@ -0,0 +1,58 @@
package com.ruoyi.common.utils;
import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
/**
* 请求包装器 - 用于读取和修改请求体
*/
public class EncryptHttpServletRequestWrapper extends HttpServletRequestWrapper {
private final String body;
public EncryptHttpServletRequestWrapper(HttpServletRequest request, String body) {
super(request);
this.body = body;
}
@Override
public ServletInputStream getInputStream() throws IOException {
final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(body.getBytes(StandardCharsets.UTF_8));
return new ServletInputStream() {
@Override
public boolean isFinished() {
return false;
}
@Override
public boolean isReady() {
return false;
}
@Override
public void setReadListener(ReadListener readListener) {
}
@Override
public int read() throws IOException {
return byteArrayInputStream.read();
}
};
}
@Override
public BufferedReader getReader() throws IOException {
return new BufferedReader(new InputStreamReader(this.getInputStream()));
}
public String getBody() {
return this.body;
}
}

View File

@@ -0,0 +1,74 @@
package com.ruoyi.common.utils;
import javax.servlet.ServletOutputStream;
import javax.servlet.WriteListener;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.nio.charset.StandardCharsets;
/**
* 响应包装器 - 用于修改响应内容
*/
public class EncryptHttpServletResponseWrapper extends HttpServletResponseWrapper {
private ByteArrayOutputStream buffer;
private ServletOutputStream out;
private PrintWriter writer;
public EncryptHttpServletResponseWrapper(HttpServletResponse response) {
super(response);
buffer = new ByteArrayOutputStream();
out = new WrappedOutputStream(buffer);
writer = new PrintWriter(new OutputStreamWriter(buffer, StandardCharsets.UTF_8));
}
@Override
public ServletOutputStream getOutputStream() throws IOException {
return out;
}
@Override
public PrintWriter getWriter() throws IOException {
return writer;
}
@Override
public void flushBuffer() throws IOException {
if (out != null) {
out.flush();
}
if (writer != null) {
writer.flush();
}
}
public byte[] getContent() throws IOException {
flushBuffer();
return buffer.toByteArray();
}
private static class WrappedOutputStream extends ServletOutputStream {
private ByteArrayOutputStream buffer;
public WrappedOutputStream(ByteArrayOutputStream buffer) {
this.buffer = buffer;
}
@Override
public void write(int b) throws IOException {
buffer.write(b);
}
@Override
public boolean isReady() {
return false;
}
@Override
public void setWriteListener(WriteListener writeListener) {
}
}
}

File diff suppressed because one or more lines are too long

View File

@@ -3,6 +3,9 @@ package com.ruoyi.framework.config;
import java.util.HashMap;
import java.util.Map;
import javax.servlet.DispatcherType;
import com.ruoyi.common.filter.EncryptResponseFilter;
import com.ruoyi.common.filter.RequestWrapperFilter;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
@@ -55,4 +58,48 @@ public class FilterConfig
return registration;
}
/**
* 请求包装过滤器 - 处理请求体解密
*/
@SuppressWarnings({ "rawtypes", "unchecked" })
@Bean
public FilterRegistrationBean requestWrapperFilterRegistration()
{
FilterRegistrationBean registration = new FilterRegistrationBean();
registration.setDispatcherTypes(DispatcherType.REQUEST);
registration.setFilter(new RequestWrapperFilter());
registration.addUrlPatterns("/*");
registration.setName("requestWrapperFilter");
registration.setOrder(FilterRegistrationBean.HIGHEST_PRECEDENCE + 1);
// 设置排除的URL模式
Map<String, String> initParameters = new HashMap<>();
initParameters.put("excludes", "/app/chat/chat");
registration.setInitParameters(initParameters);
return registration;
}
/**
* 响应加密过滤器 - 处理响应内容加密
*/
@SuppressWarnings({ "rawtypes", "unchecked" })
@Bean
public FilterRegistrationBean encryptResponseFilterRegistration()
{
FilterRegistrationBean registration = new FilterRegistrationBean();
registration.setDispatcherTypes(DispatcherType.REQUEST);
registration.setFilter(new EncryptResponseFilter());
registration.addUrlPatterns("/*");
registration.setName("encryptResponseFilter");
registration.setOrder(FilterRegistrationBean.HIGHEST_PRECEDENCE + 2);
// 设置排除的URL模式
Map<String, String> initParameters = new HashMap<>();
initParameters.put("excludes", "/app/chat/chat");
registration.setInitParameters(initParameters);
return registration;
}
}