Commit 8c6ba975 authored by ligaowei's avatar ligaowei

refactor(backend): 重构用户上下文处理和工具管理逻辑

重构UserUtils使用ThreadLocal存储用户ID以支持异步线程
简化MetaObjectHandlerConfig的用户ID获取逻辑
移除SseTokenEmitter的@Component注解并重构构造函数
优化AgentToolManager的代码结构和日志记录
重构AsyncUserContextDecorator以增强线程上下文传播

refactor(frontend): 简化表单渲染组件并移除pangea-ui依赖
parent 0306580c
This diff is collapsed.
......@@ -29,7 +29,6 @@ public class AgentChatService {
private final AgentToolManager agentToolManager;
private final UserSseService userSseService;
private final pangea.hiagent.web.service.AgentService agentService;
private final SseTokenEmitter sseTokenEmitter;
public AgentChatService(
EventService eventService,
......@@ -37,14 +36,12 @@ public class AgentChatService {
AgentProcessorFactory agentProcessorFactory,
AgentToolManager agentToolManager,
UserSseService userSseService,
pangea.hiagent.web.service.AgentService agentService,
SseTokenEmitter sseTokenEmitter) {
pangea.hiagent.web.service.AgentService agentService) {
this.errorHandlerService = errorHandlerService;
this.agentProcessorFactory = agentProcessorFactory;
this.agentToolManager = agentToolManager;
this.userSseService = userSseService;
this.agentService = agentService;
this.sseTokenEmitter = sseTokenEmitter;
}
// /**
......@@ -182,7 +179,7 @@ public class AgentChatService {
AgentRequest request = chatRequest.toAgentRequest(agent.getId(), agent, agentToolManager);
// 创建新的SseTokenEmitter实例
SseTokenEmitter tokenEmitter = sseTokenEmitter.createNewInstance(emitter, agent, request, userId, this::handleCompletion);
SseTokenEmitter tokenEmitter = new SseTokenEmitter(userSseService, emitter, agent, request, userId, this::handleCompletion);
// 处理流式请求
processor.processStreamRequest(request, agent, userId, tokenEmitter);
......
package pangea.hiagent.agent.service;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import pangea.hiagent.model.Agent;
......@@ -13,7 +13,6 @@ import pangea.hiagent.web.dto.AgentRequest;
* 无状态设计,每次使用时创建新实例
*/
@Slf4j
@Component
public class SseTokenEmitter implements TokenConsumerWithCompletion {
private final UserSseService userSseService;
......@@ -47,6 +46,13 @@ public class SseTokenEmitter implements TokenConsumerWithCompletion {
/**
* 无参构造函数,用于Spring容器初始化
*/
public SseTokenEmitter() {
this(null, null, null, null, null, null);
}
/**
* 构造函数,用于Spring容器初始化(带UserSseService参数)
*/
public SseTokenEmitter(UserSseService userSseService) {
this(userSseService, null, null, null, null, null);
}
......
......@@ -104,30 +104,22 @@ public class MetaObjectHandlerConfig implements MetaObjectHandler {
/**
* 获取当前用户ID,支持异步线程上下文
* 该方法支持以下场景:
* 1. 同步请求:从SecurityContext获取用户ID
* 2. 异步任务:从AsyncUserContextDecorator传播的上下文获取用户ID
* 3. 故障转移:尝试直接解析Token获取用户ID
* 1. 优先从ThreadLocal获取(支持异步线程)
* 2. 从SecurityContext获取(支持同步请求和AsyncUserContextDecorator传播)
* 3. 从请求中解析Token获取用户ID
*
* @return 用户ID,如果无法获取则返回null
*/
private String getCurrentUserIdWithContext() {
try {
// 方式1:首先尝试从SecurityContext获取(支持同步请求和AsyncUserContextDecorator传播)
// 直接调用UserUtils.getCurrentUserId(),该方法已经包含了所有获取用户ID的方式
// 并且优先从ThreadLocal获取,支持异步线程
String userId = UserUtils.getCurrentUserId();
if (userId != null) {
log.debug("通过SecurityContext成功获取用户ID: {}", userId);
log.debug("成功获取用户ID: {}", userId);
return userId;
}
log.debug("无法从SecurityContext获取用户ID,可能是异步线程且未使用AsyncUserContextDecorator包装");
// 方式2:尝试直接从请求中解析Token(故障转移)
String asyncUserId = UserUtils.getCurrentUserIdInAsync();
if (asyncUserId != null) {
log.debug("通过直接解析Token成功获取用户ID: {}", asyncUserId);
return asyncUserId;
}
log.warn("无法通过任何方式获取当前用户ID,createdBy/updatedBy字段将不被填充");
return null;
} catch (Exception e) {
......
......@@ -98,17 +98,25 @@ public class AsyncUserContextDecorator {
public static Runnable wrapWithContext(Runnable runnable) {
// 捕获当前线程的用户上下文
UserContextHolder userContext = captureUserContext();
// 同时捕获当前线程的用户ID(用于ThreadLocal传播)
String currentUserId = UserUtils.getCurrentUserId();
return () -> {
try {
// 在异步线程中传播用户上下文
propagateUserContext(userContext);
// 将用户ID设置到ThreadLocal中,增强可靠性
if (currentUserId != null) {
UserUtils.setCurrentUserId(currentUserId);
}
// 执行原始任务
runnable.run();
} finally {
// 清理当前线程的用户上下文
clearUserContext();
// 清理ThreadLocal中的用户ID
UserUtils.clearCurrentUserId();
}
};
}
......@@ -122,17 +130,25 @@ public class AsyncUserContextDecorator {
public static <V> Callable<V> wrapWithContext(Callable<V> callable) {
// 捕获当前线程的用户上下文
UserContextHolder userContext = captureUserContext();
// 同时捕获当前线程的用户ID(用于ThreadLocal传播)
String currentUserId = UserUtils.getCurrentUserId();
return () -> {
try {
// 在异步线程中传播用户上下文
propagateUserContext(userContext);
// 将用户ID设置到ThreadLocal中,增强可靠性
if (currentUserId != null) {
UserUtils.setCurrentUserId(currentUserId);
}
// 执行原始任务
return callable.call();
} finally {
// 清理当前线程的用户上下文
clearUserContext();
// 清理ThreadLocal中的用户ID
UserUtils.clearCurrentUserId();
}
};
}
......
......@@ -21,19 +21,71 @@ public class UserUtils {
// 注入JwtUtil bean
private static JwtUtil jwtUtil;
// 使用InheritableThreadLocal存储用户ID,支持异步线程继承
private static final InheritableThreadLocal<String> USER_ID_THREAD_LOCAL = new InheritableThreadLocal<>();
public UserUtils(JwtUtil jwtUtil) {
UserUtils.jwtUtil = jwtUtil;
}
/**
* 设置当前线程的用户ID
* @param userId 用户ID
*/
public static void setCurrentUserId(String userId) {
if (StringUtils.hasText(userId)) {
USER_ID_THREAD_LOCAL.set(userId);
log.debug("设置当前线程的用户ID: {}", userId);
} else {
USER_ID_THREAD_LOCAL.remove();
log.debug("清除当前线程的用户ID");
}
}
/**
* 清除当前线程的用户ID
*/
public static void clearCurrentUserId() {
USER_ID_THREAD_LOCAL.remove();
log.debug("清除当前线程的用户ID");
}
/**
* 从ThreadLocal获取用户ID
* @return 用户ID,如果不存在则返回null
*/
public static String getCurrentUserIdFromThreadLocal() {
String userId = USER_ID_THREAD_LOCAL.get();
if (userId != null) {
log.debug("从ThreadLocal获取到用户ID: {}", userId);
}
return userId;
}
public static String getCurrentUserId() {
String username = getCurrentUserIdInSync();
if (username==null || username.isEmpty()) {
username = getCurrentUserIdInAsync();
// 优先从ThreadLocal获取(支持异步线程)
String userId = getCurrentUserIdFromThreadLocal();
if (userId != null) {
return userId;
}
return username;
// 从同步上下文获取
userId = getCurrentUserIdInSync();
if (userId != null) {
// 将获取到的用户ID存入ThreadLocal,供后续异步操作使用
setCurrentUserId(userId);
return userId;
}
// 从异步上下文获取
userId = getCurrentUserIdInAsync();
if (userId != null) {
// 将获取到的用户ID存入ThreadLocal,供后续异步操作使用
setCurrentUserId(userId);
}
return userId;
}
/**
......
This diff is collapsed.
......@@ -19,7 +19,7 @@
"highlight.js": "^11.9.0",
"marked": "^17.0.1",
"pako": "^2.1.0",
"pangea-ui": "^0.14.2-beta.9",
"pinia": "^2.1.7",
"snabbdom": "^3.6.3",
"vue": "^3.4.0",
......
<template>
<hi-page-template
ref="templateRef"
:json="json"
:open-intl="false"
></hi-page-template>
<div class="button-wrap">
<a-button type="primary" @click="submit">提交</a-button>
<div class="form-container">
<h2>表单渲染器(已简化)</h2>
<div class="form-content">
<div class="form-field">
<label>输入框</label>
<el-input v-model="formData.input" placeholder="请输入"></el-input>
</div>
<div class="form-field">
<label>日期</label>
<el-date-picker v-model="formData.date" type="date" placeholder="选择日期"></el-date-picker>
</div>
</div>
<div class="button-wrap">
<el-button type="primary" @click="submit">提交</el-button>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref } from "vue";
import HiPageTemplate from "pangea-ui/hi-page-template";
import { ref, reactive } from "vue";
import { ElInput, ElDatePicker, ElButton } from "element-plus";
const templateRef = ref();
const formData = reactive({
input: '',
date: null
});
const submit = () => {
templateRef.value?.ctx.validate(1, (res, data) => {
console.log(res, data);
});
};
const json = {
pages: [
{
key: 0,
type: "default",
name: "默认页",
code: "",
display: "",
props: {
margin: "16px",
padding: "12px",
backgroundColor: "white",
display: {},
},
bindProps: {},
coms: [
{
key: 1,
type: "node",
name: "表单容器",
code: "HiFormContainer",
display: "",
props: {
status: "default",
backgroundColor: "transparent",
layout: "horizontal",
size: "medium",
labelAlign: "right",
display: {},
borderRadius: {},
boxShadow: {},
loop: {
data: [],
},
},
bindProps: {},
coms: [
{
key: 1766473421208,
name: "输入框",
code: "HiInput",
props: {
title: "输入框",
status: "default",
placeholder: "请输入",
name: "INPUT_6CP8HIBK",
},
bindProps: {},
coms: [],
},
{
key: 1766476676439,
name: "日期",
code: "HiDatePicker",
props: {
title: "日期",
type: "date",
format: "YYYY-MM-DD",
status: "default",
name: "DATE_PA9TUPQQ",
},
bindProps: {},
},
],
},
],
},
],
params: [],
apis: [],
funcs: [],
pageTemplate: {},
console.log('表单数据:', formData);
// 这里可以添加表单验证逻辑
};
</script>
<style scoped>
.form-container {
background-color: white;
padding: 16px;
margin: 16px;
border-radius: 8px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
}
.form-container h2 {
margin-top: 0;
margin-bottom: 16px;
color: #333;
}
.form-content {
margin-bottom: 16px;
}
.form-field {
margin-bottom: 16px;
}
.form-field label {
display: block;
margin-bottom: 8px;
font-weight: 500;
color: #606266;
}
.button-wrap {
display: flex;
justify-content: center;
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment