diff --git a/web/src/main/java/com/example/web/aspect/ReadWriteSeparationAspect.java b/web/src/main/java/com/example/web/aspect/ReadWriteSeparationAspect.java new file mode 100644 index 0000000..e501121 --- /dev/null +++ b/web/src/main/java/com/example/web/aspect/ReadWriteSeparationAspect.java @@ -0,0 +1,66 @@ +package com.example.web.aspect; + +import com.example.web.config.DataSourceContextHolder; +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.annotation.After; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Before; +import org.aspectj.lang.annotation.Pointcut; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +@Aspect +@Component +public class ReadWriteSeparationAspect { + + private static final Logger logger = LoggerFactory.getLogger(ReadWriteSeparationAspect.class); + + // 定义切入点,拦截所有Mapper方法 + @Pointcut("execution(* com.example.web.mapper.*Mapper.*(..))") + public void mapperPointcut() { + } + + @Before("mapperPointcut()") + public void beforeMapperMethod(JoinPoint joinPoint) { + String methodName = joinPoint.getSignature().getName(); + String className = joinPoint.getTarget().getClass().getSimpleName(); + + // 获取当前使用的数据源 + String currentDataSource = DataSourceContextHolder.getDataSource(); + + // 根据方法名判断是读操作还是写操作 + if (isReadOperation(methodName)) { + // 读操作使用从库 + String slaveDataSource = currentDataSource.contains("wechat") ? "wechat-slave" : "primary-slave"; + DataSourceContextHolder.setDataSource(slaveDataSource); + logger.debug("切换到从库数据源: {},执行方法: {}.{}", slaveDataSource, className, methodName); + } else { + // 写操作使用主库 + String masterDataSource = currentDataSource.contains("wechat") ? "wechat" : "primary"; + DataSourceContextHolder.setDataSource(masterDataSource); + logger.debug("切换到主库数据源: {},执行方法: {}.{}", masterDataSource, className, methodName); + } + } + + @After("mapperPointcut()") + public void afterMapperMethod() { + // 方法执行完成后清除数据源上下文 + DataSourceContextHolder.clearDataSource(); + } + + /** + * 判断是否为读操作 + * @param methodName 方法名 + * @return 是否为读操作 + */ + private boolean isReadOperation(String methodName) { + // 通常查询方法以find、get、select、query、list、count等开头的方法为读操作 + return methodName.startsWith("find") || + methodName.startsWith("get") || + methodName.startsWith("select") || + methodName.startsWith("query") || + methodName.startsWith("list") || + methodName.startsWith("count"); + } +} \ No newline at end of file diff --git a/web/src/main/java/com/example/web/config/DataSourceConfig.java b/web/src/main/java/com/example/web/config/DataSourceConfig.java index d2f95d8..f484069 100644 --- a/web/src/main/java/com/example/web/config/DataSourceConfig.java +++ b/web/src/main/java/com/example/web/config/DataSourceConfig.java @@ -19,13 +19,27 @@ public class DataSourceConfig { public DataSource primaryDataSource() { return DataSourceBuilder.create().build(); } - + + // userlogin从库数据源 + @Bean(name = "primarySlaveDataSource") + @ConfigurationProperties(prefix = "spring.datasource.primary-slave") + public DataSource primarySlaveDataSource() { + return DataSourceBuilder.create().build(); + } + // 第二个数据源(wechat_app) @Bean(name = "wechatDataSource") @ConfigurationProperties(prefix = "spring.datasource.wechat") public DataSource wechatDataSource() { return DataSourceBuilder.create().build(); } + + // wechat_app从库数据源 + @Bean(name = "wechatSlaveDataSource") + @ConfigurationProperties(prefix = "spring.datasource.wechat-slave") + public DataSource wechatSlaveDataSource() { + return DataSourceBuilder.create().build(); + } // 动态数据源配置 @Primary @@ -35,7 +49,9 @@ public class DataSourceConfig { dynamicDataSource.setDefaultTargetDataSource(primaryDataSource()); Map dataSources = new HashMap<>(); dataSources.put("primary", primaryDataSource()); + dataSources.put("primary-slave", primarySlaveDataSource()); dataSources.put("wechat", wechatDataSource()); + dataSources.put("wechat-slave", wechatSlaveDataSource()); dynamicDataSource.setTargetDataSources(dataSources); return dynamicDataSource; } diff --git a/web/src/main/resources/application.yaml b/web/src/main/resources/application.yaml index 42681fd..bfac76f 100644 --- a/web/src/main/resources/application.yaml +++ b/web/src/main/resources/application.yaml @@ -11,6 +11,17 @@ spring: idle-timeout: 600000 minimum-idle: 5 maximum-pool-size: 20 + # userlogin从库数据库 + primary-slave: + jdbc-url: jdbc:mysql://1.95.162.61:3306/userlogin?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&useSSL=false&allowPublicKeyRetrieval=true + username: root + password: schl@2025 + driver-class-name: com.mysql.cj.jdbc.Driver + hikari: + max-lifetime: 1200000 + idle-timeout: 600000 + minimum-idle: 5 + maximum-pool-size: 30 # wechat_app数据库 wechat: jdbc-url: jdbc:mysql://1.95.162.61:3306/wechat_app?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&useSSL=false&allowPublicKeyRetrieval=true @@ -22,6 +33,17 @@ spring: idle-timeout: 600000 minimum-idle: 5 maximum-pool-size: 20 + # wechat_app从库数据库 + wechat-slave: + jdbc-url: jdbc:mysql://1.95.162.61:3306/wechat_app?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&useSSL=false&allowPublicKeyRetrieval=true + username: root + password: schl@2025 + driver-class-name: com.mysql.cj.jdbc.Driver + hikari: + max-lifetime: 1200000 + idle-timeout: 600000 + minimum-idle: 5 + maximum-pool-size: 30 server: port: 8083 diff --git a/web/src/main/resources/mapper/UsersMapper.xml b/web/src/main/resources/mapper/UsersMapper.xml index 04f2a37..9af5b17 100644 --- a/web/src/main/resources/mapper/UsersMapper.xml +++ b/web/src/main/resources/mapper/UsersMapper.xml @@ -181,7 +181,13 @@ SELECT COUNT(*) FROM users u + JOIN usermanagements um ON u.userId = um.userId WHERE u.type != 'Colleague' + AND (um.managercompany IS NULL OR um.managercompany = '') + AND (um.managerdepartment IS NULL OR um.managerdepartment = '') + AND (um.organization IS NULL OR um.organization = '') + AND (um.role IS NULL OR um.role = '') + AND (um.userName IS NULL OR um.userName = '') AND u.phoneNumber = #{phoneNumber} diff --git a/web/src/main/resources/static/index.html b/web/src/main/resources/static/index.html index 4d5d633..eb32af8 100644 --- a/web/src/main/resources/static/index.html +++ b/web/src/main/resources/static/index.html @@ -504,12 +504,13 @@ box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2); } - /* 表格容器样式,添加横向滚动 */ + /* 表格容器样式,添加横向滚动和虚拟滚动 */ .table-container { width: 100%; overflow-x: auto; margin: 0; padding: 0; + position: relative; } .table-container table { @@ -517,6 +518,138 @@ min-width: 100%; } + /* 虚拟滚动容器 */ + .virtual-scroll-container { + width: 100%; + overflow-x: auto; + overflow-y: auto; + max-height: 600px; /* 设置固定高度,根据需要调整 */ + position: relative; + } + + /* 虚拟滚动内容容器 */ + .virtual-scroll-content { + position: relative; + width: 100%; + } + + /* 虚拟滚动占位符 */ + .virtual-scroll-placeholder { + position: absolute; + left: 0; + top: 0; + width: 100%; + opacity: 0; + pointer-events: none; + } + + /* 虚拟滚动表格 */ + .virtual-scroll-table { + width: 100%; + border-collapse: collapse; + } + + /* 骨架屏样式 */ + .skeleton { + background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%); + background-size: 200% 100%; + animation: skeleton-loading 1.5s infinite; + } + + @keyframes skeleton-loading { + 0% { + background-position: 200% 0; + } + 100% { + background-position: -200% 0; + } + } + + .skeleton-row { + height: 60px; + border-bottom: 1px solid #e8e8e8; + } + + .skeleton-cell { + padding: 12px; + border-bottom: 1px solid #e8e8e8; + } + + .skeleton-text { + height: 16px; + border-radius: 4px; + } + + .skeleton-button { + height: 32px; + border-radius: 4px; + width: 80px; + } + + /* 加载状态样式 */ + .loading-overlay { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(255, 255, 255, 0.8); + display: flex; + justify-content: center; + align-items: center; + z-index: 1000; + opacity: 0; + visibility: hidden; + transition: opacity 0.3s ease, visibility 0.3s ease; + } + + .loading-overlay.active { + opacity: 1; + visibility: visible; + } + + .loading-spinner { + width: 50px; + height: 50px; + border: 5px solid #f3f3f3; + border-top: 5px solid #1890ff; + border-radius: 50%; + animation: spin 1s linear infinite; + } + + @keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } + } + + .loading-text { + margin-top: 10px; + color: #1890ff; + font-size: 14px; + } + + /* 页面加载骨架屏 */ + .page-skeleton { + padding: 20px; + } + + .skeleton-header { + height: 60px; + margin-bottom: 20px; + border-radius: 8px; + } + + .skeleton-user-info { + height: 120px; + margin-bottom: 20px; + border-radius: 8px; + } + + .skeleton-section { + height: 400px; + border-radius: 8px; + } + /* 确保表格列宽正确显示 */ .table-container th, .table-container td { @@ -880,6 +1013,19 @@ + +
+
+
加载中...
+
+ + + + \ No newline at end of file diff --git a/web/src/main/resources/static/login.html b/web/src/main/resources/static/login.html index 0256241..12cd723 100644 --- a/web/src/main/resources/static/login.html +++ b/web/src/main/resources/static/login.html @@ -292,12 +292,12 @@ loadingOverlay.appendChild(loadingText); document.body.appendChild(loadingOverlay); - // 延迟3秒后跳转到主页面 + // 延迟2秒后跳转到主页面 console.log('准备跳转到index.html...'); setTimeout(() => { // 优化:使用绝对路径跳转 window.location.href = '/KH/index.html'; - }, 3000); + }, 2000); } else { console.log('登录失败,错误信息:', data.message); errorMessage.textContent = data.message;