pull/8091/head
JEECG 2025-04-09 13:45:29 +08:00
commit 47a2a6fbac
9 changed files with 443 additions and 60 deletions

View File

@ -41,6 +41,35 @@ JeecgBoot AI低代码平台可以应用在任何J2EE项目的开发中
项目说明
-----------------------------------
| 项目名 | 说明 |
|--------------------|------------------------|
| `jeecg-boot` | 后端源码JAVASpringBoot微服务架构 |
| `jeecgboot-vue3` | 前端源码VUE3vue3+vite6+ts最新技术栈 |
| `JeecgUniapp` | [配套APP框架](https://github.com/jeecgboot/JeecgUniapp) 适配多个终端支持APP、小程序、H5 |
技术文档
-----------------------------------
- 官方网站: [http://www.jeecg.com](http://www.jeecg.com)
- 在线演示 [平台演示](http://boot3.jeecg.com) | [APP演示](http://jeecg.com/appIndex) | [体验低代码](https://jeecg.blog.csdn.net/article/details/106079007) | [体验零代码](https://app.qiaoqiaoyun.com/myapps/index)
- 开发文档: [文档中心](https://help.jeecg.com) | [AIGC大模块](https://help.jeecg.com/aigc)
- 新手指南: [快速入门](http://www.jeecg.com/doc/quickstart) | [入门视频](http://jeecg.com/doc/video) | [如何反馈问题](https://github.com/jeecgboot/JeecgBoot/issues/new?template=bug_report.md)
- QQ交流群 ⑩716488839、⑨808791225(满)、其他(满)
启动项目
-----------------------------------
- [IDEA启动前后端项目](https://help.jeecg.com/java/setup/idea/startup)
- [Docker一键启动前后端](https://help.jeecg.com/java/docker/quick)
AIGC应用平台介绍
-----------------------------------
@ -91,34 +120,6 @@ AIGC应用平台介绍
| 等等。。 | √ |
项目说明
-----------------------------------
| 项目名 | 说明 |
|--------------------|------------------------|
| `jeecg-boot` | 后端源码JAVASpringBoot微服务架构 |
| `jeecgboot-vue3` | 前端源码VUE3vue3+vite6+ts最新技术栈 |
| `JeecgUniapp` | [配套APP框架](https://github.com/jeecgboot/JeecgUniapp) 适配多个终端支持APP、小程序、H5 |
技术文档
-----------------------------------
- 官方网站: [http://www.jeecg.com](http://www.jeecg.com)
- 在线演示 [平台演示](http://boot3.jeecg.com) | [APP演示](http://jeecg.com/appIndex) | [体验低代码](https://jeecg.blog.csdn.net/article/details/106079007) | [体验零代码](https://app.qiaoqiaoyun.com/myapps/index)
- 开发文档: [文档中心](https://help.jeecg.com) | [AIGC大模块](https://help.jeecg.com/aigc)
- 新手指南: [快速入门](http://www.jeecg.com/doc/quickstart) | [入门视频](http://jeecg.com/doc/video) | [如何反馈问题](https://github.com/jeecgboot/JeecgBoot/issues/new?template=bug_report.md)
- QQ交流群 ⑩716488839、⑨808791225(满)、其他(满)
启动项目
-----------------------------------
- [IDEA启动前后端项目](https://help.jeecg.com/java/setup/idea/startup)
- [Docker一键启动前后端](https://help.jeecg.com/java/docker/quick)
技术架构:

View File

@ -1,7 +1,10 @@
package org.jeecg.config.init;
import io.undertow.server.DefaultByteBufferPool;
import io.undertow.server.handlers.BlockingHandler;
import io.undertow.websockets.jsr.WebSocketDeploymentInfo;
import org.jeecg.modules.monitor.actuator.undertow.CustomUndertowMetricsHandler;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.web.embedded.undertow.UndertowServletWebServerFactory;
import org.springframework.boot.web.server.WebServerFactoryCustomizer;
import org.springframework.context.annotation.Configuration;
@ -12,7 +15,14 @@ import org.springframework.context.annotation.Configuration;
* WARN io.undertow.websockets.jsr:68 - UT026010: Buffer pool was not set on WebSocketDeploymentInfo, the default pool will be used
*/
@Configuration
public class UndertowConfiguration implements WebServerFactoryCustomizer<UndertowServletWebServerFactory>{
public class UndertowConfiguration implements WebServerFactoryCustomizer<UndertowServletWebServerFactory> {
/**
* undertow
* for [QQYUN-11902]tomcat undertow
*/
@Autowired
private CustomUndertowMetricsHandler customUndertowMetricsHandler;
@Override
public void customize(UndertowServletWebServerFactory factory) {
@ -24,6 +34,9 @@ public class UndertowConfiguration implements WebServerFactoryCustomizer<Underto
webSocketDeploymentInfo.setBuffers(new DefaultByteBufferPool(true, 8192));
deploymentInfo.addServletContextAttribute("io.undertow.websockets.jsr.WebSocketDeploymentInfo", webSocketDeploymentInfo);
// 添加自定义 监控 handler
deploymentInfo.addInitialHandlerChainWrapper(next -> new BlockingHandler(customUndertowMetricsHandler.wrap(next)));
});
}
}

View File

@ -0,0 +1,88 @@
package org.jeecg.modules.monitor.actuator.undertow;
import io.micrometer.core.instrument.MeterRegistry;
import io.undertow.server.HttpHandler;
import io.undertow.server.HttpServerExchange;
import io.undertow.server.session.*;
import org.springframework.stereotype.Component;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.LongAdder;
/**
* undertow
* for [QQYUN-11902]tomcat undertow
* @author chenrui
* @date 2025/4/8 19:06
*/
@Component("jeecgCustomUndertowMetricsHandler")
public class CustomUndertowMetricsHandler {
// 用于统计已创建的 session 数量
private final LongAdder sessionsCreated = new LongAdder();
// 用于统计已销毁的 session 数量
private final LongAdder sessionsExpired = new LongAdder();
// 当前活跃的 session 数量
private final AtomicInteger activeSessions = new AtomicInteger();
// 历史最大活跃 session 数
private final AtomicInteger maxActiveSessions = new AtomicInteger();
// Undertow 内存 session 管理器(用于创建与管理 session
private final InMemorySessionManager sessionManager = new InMemorySessionManager("undertow-session-manager");
// 使用 Cookie 存储 session ID
private final SessionConfig sessionConfig = new SessionCookieConfig();
/**
*
* @param meterRegistry
* @author chenrui
* @date 2025/4/8 19:07
*/
public CustomUndertowMetricsHandler(MeterRegistry meterRegistry) {
// 注册 Micrometer 指标
meterRegistry.gauge("undertow.sessions.created", sessionsCreated, LongAdder::longValue);
meterRegistry.gauge("undertow.sessions.expired", sessionsExpired, LongAdder::longValue);
meterRegistry.gauge("undertow.sessions.active.current", activeSessions, AtomicInteger::get);
meterRegistry.gauge("undertow.sessions.active.max", maxActiveSessions, AtomicInteger::get);
// 添加 session 生命周期监听器,统计 session 创建与销毁
sessionManager.registerSessionListener(new SessionListener() {
@Override
public void sessionCreated(Session session, HttpServerExchange exchange) {
sessionsCreated.increment();
int now = activeSessions.incrementAndGet();
maxActiveSessions.getAndUpdate(max -> Math.max(max, now));
}
@Override
public void sessionDestroyed(Session session, HttpServerExchange exchange, SessionDestroyedReason reason) {
sessionsExpired.increment();
activeSessions.decrementAndGet();
}
});
}
/**
* Undertow HttpHandler session
* @param next
* @return
* @author chenrui
* @date 2025/4/8 19:07
*/
public HttpHandler wrap(HttpHandler next) {
return exchange -> {
// 获取当前 session如果不存在则创建
Session session = sessionManager.getSession(exchange, sessionConfig);
if (session == null) {
sessionManager.createSession(exchange, sessionConfig);
}
// 执行下一个 Handler
next.handleRequest(exchange);
};
}
}

View File

@ -0,0 +1,177 @@
package org.jeecg.modules.system.test;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import org.jeecg.common.api.vo.Result;
import org.jeecg.common.modules.redis.client.JeecgRedisClient;
import org.jeecg.common.util.RedisUtil;
import org.jeecg.config.JeecgBaseConfig;
import org.jeecg.modules.base.service.BaseCommonService;
import org.jeecg.modules.system.controller.SysUserController;
import org.jeecg.modules.system.entity.SysUser;
import org.jeecg.modules.system.service.*;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import java.util.ArrayList;
import java.util.List;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
/**
*
*/
@WebMvcTest(SysUserController.class)
public class SysUserApiTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private ISysUserService sysUserService;
@MockBean
private ISysDepartService sysDepartService;
@MockBean
private ISysUserRoleService sysUserRoleService;
@MockBean
private ISysUserDepartService sysUserDepartService;
@MockBean
private ISysDepartRoleUserService departRoleUserService;
@MockBean
private ISysDepartRoleService departRoleService;
@MockBean
private RedisUtil redisUtil;
@Value("${jeecg.path.upload}")
private String upLoadPath;
@MockBean
private BaseCommonService baseCommonService;
@MockBean
private ISysUserAgentService sysUserAgentService;
@MockBean
private ISysPositionService sysPositionService;
@MockBean
private ISysUserTenantService userTenantService;
@MockBean
private JeecgRedisClient jeecgRedisClient;
@MockBean
private JeecgBaseConfig jeecgBaseConfig;
/**
* 使
*/
private final String BASE_URL = "/sys/user/";
/**
*
*/
@Test
public void testQuery() throws Exception{
// 请求地址
String url = BASE_URL + "list";
Page<SysUser> sysUserPage = new Page<>();
SysUser sysUser = new SysUser();
sysUser.setUsername("admin");
List<SysUser> users = new ArrayList<>();
users.add(sysUser);
sysUserPage.setRecords(users);
sysUserPage.setCurrent(1);
sysUserPage.setSize(10);
sysUserPage.setTotal(1);
given(this.sysUserService.queryPageList(any(), any(), any(), any())).willReturn(Result.OK(sysUserPage));
String result = mockMvc.perform(get(url)).andReturn().getResponse().getContentAsString();
JSONObject jsonObject = JSON.parseObject(result);
Assertions.assertEquals("admin", jsonObject.getJSONObject("result").getJSONArray("records").getJSONObject(0).getString("username"));
}
/**
*
*/
@Test
public void testAdd() throws Exception {
// 请求地址
String url = BASE_URL + "add" ;
JSONObject params = new JSONObject();
params.put("username", "wangwuTest");
params.put("password", "123456");
params.put("confirmpassword","123456");
params.put("realname", "单元测试");
params.put("activitiSync", "1");
params.put("userIdentity","1");
params.put("workNo","0025");
String result = mockMvc.perform(post(url).contentType(MediaType.APPLICATION_JSON_VALUE).content(params.toJSONString()))
.andReturn().getResponse().getContentAsString();
JSONObject jsonObject = JSON.parseObject(result);
Assertions.assertTrue(jsonObject.getBoolean("success"));
}
/**
*
*/
@Test
public void testEdit() throws Exception {
// 数据Id
String dataId = "1331795062924374018";
// 请求地址
String url = BASE_URL + "edit";
JSONObject params = new JSONObject();
params.put("username", "wangwuTest");
params.put("realname", "单元测试1111");
params.put("activitiSync", "1");
params.put("userIdentity","1");
params.put("workNo","0025");
params.put("id",dataId);
SysUser sysUser = new SysUser();
sysUser.setUsername("admin");
given(this.sysUserService.getById(any())).willReturn(sysUser);
String result = mockMvc.perform(put(url).contentType(MediaType.APPLICATION_JSON_VALUE).content(params.toJSONString()))
.andReturn().getResponse().getContentAsString();
JSONObject jsonObject = JSON.parseObject(result);
Assertions.assertTrue(jsonObject.getBoolean("success"));
}
/**
*
*/
@Test
public void testDelete() throws Exception {
// 数据Id
String dataId = "1331795062924374018";
// 请求地址
String url = BASE_URL + "delete" + "?id=" + dataId;
String result = mockMvc.perform(delete(url)).andReturn().getResponse().getContentAsString();
JSONObject jsonObject = JSON.parseObject(result);
Assertions.assertTrue(jsonObject.getBoolean("success"));
}
}

View File

@ -359,7 +359,7 @@
<dependency>
<groupId>org.jeecgframework</groupId>
<artifactId>weixin4j</artifactId>
<version>2.0.1</version>
<version>2.0.2</version>
<exclusions>
<exclusion>
<artifactId>commons-beanutils</artifactId>

View File

@ -1,5 +1,12 @@
<template>
<Select @dropdownVisibleChange="handleFetch" v-bind="attrs_" @change="handleChange" :options="getOptions" v-model:value="state">
<Select
v-bind="attrs_"
v-model:value="state"
:options="getOptions"
@change="handleChange"
@dropdownVisibleChange="handleFetch"
@popupScroll="handlePopupScroll"
>
<template #[item]="data" v-for="item in Object.keys($slots)">
<slot :name="item" v-bind="data || {}"></slot>
</template>
@ -24,9 +31,10 @@
import { LoadingOutlined } from '@ant-design/icons-vue';
import { useI18n } from '/@/hooks/web/useI18n';
import { propTypes } from '/@/utils/propTypes';
import { isNumber } from '/@/utils/is';
type OptionsItem = { label: string; value: string; disabled?: boolean };
// https://help.jeecg.com/ui/apiSelect#pageconfig%E5%8F%82%E6%95%B0%E9%85%8D%E7%BD%AE
export default defineComponent({
name: 'ApiSelect',
components: {
@ -35,7 +43,7 @@
},
inheritAttrs: false,
props: {
value: [Array, Object, String, Number],
value: [Array, String, Number],
numberToString: propTypes.bool,
api: {
type: Function as PropType<(arg?: Recordable) => Promise<OptionsItem[]>>,
@ -46,6 +54,11 @@
type: Object as PropType<Recordable>,
default: () => ({}),
},
//
pageConfig: {
type: Object as PropType<Recordable>,
default: () => ({ isPage: false }),
},
// support xxx.xxx.xx
resultField: propTypes.string.def(''),
labelField: propTypes.string.def('label'),
@ -60,7 +73,15 @@
const emitData = ref<any[]>([]);
const attrs = useAttrs();
const { t } = useI18n();
// update-begin--author:liusq---date:20250407---forQQYUN-11831ApiSelect #7883
const hasMore = ref(true);
const pagination = ref({
pageNo: 1,
pageSize: 10,
total: 0,
});
const defPageConfig = { isPage: false, pageField: 'pageNo', pageSizeField: 'pageSize', totalField: 'total', listField: 'records' };
// update-end--author:liusq---date:20250407---forQQYUN-11831ApiSelect #7883
// Embedded in the form, just use the hook binding to perform form verification
const [state, setState] = useRuleFormItem(props, 'value', 'change', emitData);
// update-begin--author:liaozhiyang---date:20230830---forQQYUN-6308
@ -114,7 +135,7 @@
},
{ deep: true }
);
//
//
watchEffect(() => {
props.value && handleFetch();
});
@ -122,17 +143,33 @@
async function fetch() {
const api = props.api;
if (!api || !isFunction(api)) return;
options.value = [];
// update-begin--author:liusq---date:20250407---forQQYUN-11831ApiSelect #7883
if (!props.pageConfig.isPage || pagination.value.pageNo == 1) {
options.value = [];
}
try {
loading.value = true;
const res = await api(props.params);
if (Array.isArray(res)) {
options.value = res;
emitChange();
return;
}
if (props.resultField) {
options.value = get(res, props.resultField) || [];
let { isPage, pageField, pageSizeField, totalField, listField } = { ...defPageConfig, ...props.pageConfig };
let params = isPage
? { ...props.params, [pageField]: pagination.value.pageNo, [pageSizeField]: pagination.value.pageSize }
: { ...props.params };
// update-end--author:liusq---date:20250407---forQQYUN-11831ApiSelect #7883
const res = await api(params);
if (isPage) {
// update-begin--author:liusq---date:20250407---forQQYUN-11831ApiSelect #7883
options.value = [...options.value, ...res[listField]];
pagination.value.total = res[totalField] || 0;
hasMore.value = res[totalField] ? options.value.length < res[totalField] : res[listField] < pagination.value.pageSize;
// update-end--author:liusq---date:20250407---forQQYUN-11831ApiSelect #7883
} else {
if (Array.isArray(res)) {
options.value = res;
emitChange();
return;
}
if (props.resultField) {
options.value = get(res, props.resultField) || [];
}
}
emitChange();
} catch (error) {
@ -151,9 +188,17 @@
function initValue() {
let value = props.value;
if (value && typeof value === 'string' && value != 'null' && value != 'undefined') {
state.value = value.split(',');
// update-begin--author:liaozhiyang---date:20250407---forissues/8037
if (unref(attrs).mode == 'multiple') {
if (value && typeof value === 'string' && value != 'null' && value != 'undefined') {
state.value = value.split(',');
} else if (isNumber(value)) {
state.value = [value];
}
} else {
state.value = value;
}
// update-end--author:liaozhiyang---date:20250407---forissues/8037
}
async function handleFetch() {
@ -171,8 +216,18 @@
vModalValue && vModalValue(_);
emitData.value = args;
}
return { state, attrs_, attrs, getOptions, loading, t, handleFetch, handleChange };
// update-begin--author:liusq---date:20250407---forQQYUN-11831ApiSelect #7883
//
function handlePopupScroll(e) {
const { scrollTop, scrollHeight, clientHeight } = e.target;
const isNearBottom = scrollHeight - scrollTop <= clientHeight + 20;
if (props.pageConfig.isPage && isNearBottom && hasMore.value && !loading.value) {
pagination.value.pageNo += 1;
fetch();
}
}
// update-end--author:liusq---date:20250407---forQQYUN-11831ApiSelect #7883
return { state, attrs_, attrs, getOptions, loading, t, handleFetch, handleChange, handlePopupScroll };
},
});
</script>

View File

@ -209,18 +209,11 @@ export const useUserStore = defineStore({
//update-begin---author:wangshuai ---date:20230424 forQQYUN-5195------------
if (redirect && goHome) {
//update-end---author:wangshuai ---date:20230424 forQQYUN-5195------------
// update-begin--author:liaozhiyang---date:20240104---forQQYUN-7804404
let publicPath = import.meta.env.VITE_PUBLIC_PATH;
if (publicPath && publicPath != '/') {
// update-begin--author:liaozhiyang---date:20240509---forissues/1147/404
if (publicPath.endsWith('/')) {
publicPath = publicPath.slice(0, -1);
}
redirect = publicPath + redirect;
}
// update-end--author:liaozhiyang---date:20240509---forissues/1147/404
// update-begin--author:liaozhiyang---date:20250407---forissues/8034hash退
// router.options.history.basepublicPath
//
window.open(redirect, '_self')
window.open(`${router.options.history.base}${redirect}`, '_self');
// update-end--author:liaozhiyang---date:20250407---forissues/8034hash退
return data;
}
// update-end-author:sunjianlei date:20230306 for:

View File

@ -4,7 +4,8 @@
<a-tabs v-model:activeKey="activeKey" @change="tabChange">
<a-tab-pane key="1" tab="服务器信息"></a-tab-pane>
<a-tab-pane key="2" tab="JVM信息" force-render></a-tab-pane>
<a-tab-pane key="3" tab="Tomcat信息"></a-tab-pane>
<!-- <a-tab-pane key="3" tab="Tomcat信息"></a-tab-pane> -->
<a-tab-pane key="6" tab="Undertow信息"></a-tab-pane>
<a-tab-pane key="4" tab="磁盘监控">
<DiskInfo v-if="activeKey == 4" style="height: 100%"></DiskInfo>
</a-tab-pane>

View File

@ -30,6 +30,11 @@ enum Api {
tomcatSessionsRejected = '/actuator/metrics/tomcat.sessions.rejected',
memoryInfo = '/sys/actuator/memory/info',
// undertow
undertowSessionsCreated = '/actuator/metrics/undertow.sessions.created',
undertowSessionsExpired = '/actuator/metrics/undertow.sessions.expired',
undertowSessionsActiveCurrent = '/actuator/metrics/undertow.sessions.active.current',
undertowSessionsActiveMax = '/actuator/metrics/undertow.sessions.active.max',
}
/**
@ -207,6 +212,34 @@ export const getTomcatSessionsRejected = () => {
return defHttp.get({ url: Api.tomcatSessionsRejected }, { isTransformResponse: false });
};
/**
*undertow 已创建 session
*/
export const getUndertowSessionsCreated = () => {
return defHttp.get({ url: Api.undertowSessionsCreated }, { isTransformResponse: false });
};
/**
*undertow 已过期 session
*/
export const getUndertowSessionsExpired = () => {
return defHttp.get({ url: Api.undertowSessionsExpired }, { isTransformResponse: false });
};
/**
*undertow 当前活跃 session
*/
export const getUndertowSessionsActiveCurrent = () => {
return defHttp.get({ url: Api.undertowSessionsActiveCurrent }, { isTransformResponse: false });
};
/**
*undertow 活跃 session 数峰值
*/
export const getUndertowSessionsActiveMax = () => {
return defHttp.get({ url: Api.undertowSessionsActiveMax }, { isTransformResponse: false });
};
/**
* 内存信息
*/
@ -230,6 +263,9 @@ export const getMoreInfo = (infoType) => {
if (infoType == '5') {
return {};
}
if (infoType == '6') {
return {};
}
};
export const getTextInfo = (infoType) => {
@ -293,6 +329,16 @@ export const getTextInfo = (infoType) => {
'memory.runtime.usage': { color: 'purple', text: 'JVM内存使用率', unit: '%', valueType: 'Number' },
};
}
if (infoType == '6') {
// undertow
return {
'undertow.sessions.created': { color: 'green', text: 'undertow 已创建 session 数', unit: '个' },
'undertow.sessions.expired': { color: 'green', text: 'undertow 已过期 session 数', unit: '个' },
'undertow.sessions.active.current': { color: 'green', text: 'undertow 当前活跃 session 数', unit: '个' },
'undertow.sessions.active.max': { color: 'green', text: 'undertow 活跃 session 数峰值', unit: '个' },
'undertow.sessions.rejected': { color: 'green', text: '超过session 最大配置后,拒绝的 session 个数', unit: '个' },
};
}
};
/**
@ -334,4 +380,13 @@ export const getServerInfo = (infoType) => {
if (infoType == '5') {
return Promise.all([getMemoryInfo()]);
}
// undertow
if (infoType == '6') {
return Promise.all([
getUndertowSessionsActiveCurrent(),
getUndertowSessionsActiveMax(),
getUndertowSessionsCreated(),
getUndertowSessionsExpired(),
]);
}
};