Compare commits

...

10 Commits

Author SHA1 Message Date
75681 7898dbebe3 更新Readme 2025-08-13 09:12:20 +08:00
75681 2ca07a12aa 移除了YAML中的重复组配置,保持Java Bean配置 2025-08-12 20:34:57 +08:00
75681 26445b86f7 修复MyBatis字段映射冲突和数据库连接问题
问题修复:
- 修复Engineer实体类字段映射冲突:统一使用小写下划线字段名
- 解决"字段user_id不存在"的MyBatis映射错误
- 修改所有@TableField注解从大写改为小写格式
- 表名从ENGINEER改为engineer以匹配数据库约定

功能增强:
- 添加应用启动时数据库连接信息打印功能
- 增强数据库连接诊断和ENGINEER表访问测试
- 新增DatabaseTestController用于数据库连接和表结构测试

技术改进:
- 解决mapUnderscoreToCamelCase配置与@TableField注解冲突
- 优化错误日志记录和诊断信息
- 增强应用启动时的数据源验证功能

修改文件:
- Engineer.java: 字段映射注解全部改为小写下划线
- DevOpsApplication.java: 新增数据库连接信息打印
- DatabaseTestController.java: 新增数据库测试接口
- 配置文件优化和错误处理增强
2025-08-12 19:26:43 +08:00
75681 a12d628b8f 实现可配置错误日志捕获和管理系统
新增功能:
- 全局错误日志捕获系统,支持启动、运行时、数据库、业务错误分类
- 完整的配置开关控制(error-log.*配置项)
- Web管理接口(/api/error-logs/*)支持日志查看和管理
- 异步日志写入支持,提升性能
- 数据库健康检查组件
- 全局异常处理器集成

技术改进:
- Java版本兼容性处理(支持Java 8和Java 9+)
- 依赖更新:Lombok 1.18.30、Spring Boot Web starter
- 本地开发环境配置和Mock API实现
- 端口配置优化避免冲突

文件变更:
- 新增:ErrorLogUtils、ErrorLogProperties、ErrorLogController等核心组件
- 新增:本地开发配置application-local.yml
- 更新:应用主类、拦截器配置、依赖管理
- 新增:错误捕获使用指南和文档
2025-08-12 14:49:55 +08:00
黎润豪 fbcd766e36 调整来源的用户查询判断 2024-11-07 17:22:06 +08:00
黎润豪 45d8d0679e 修正表定义 2024-10-29 15:11:56 +08:00
黎润豪 c42364cd9b 空指针bug修复 2024-10-24 11:38:28 +08:00
黎润豪 dca0e3201c SQL修正,系统来源判断调整 2024-10-24 11:13:39 +08:00
黎润豪 4d62fa66b4 bug修复 2024-10-24 10:04:55 +08:00
黎润豪 702600e3c6 bug修复 2024-10-24 09:53:29 +08:00
27 changed files with 3546 additions and 535 deletions

12
.gitignore vendored
View File

@ -4,3 +4,15 @@
*-backup-*.chnr.json
.back_devops
.smarttomcat
/logs/
/.claude/
/CLAUDE.md
/*.bat
/setup_database.sql
/PowerShell启动说明.md
/DEPLOYMENT.md
/ERROR_CAPTURE_GUIDE.md
/*.sql
/JDK21_UPGRADE_NOTES.md
/start-jdk21.sh
/PROJECT_README.md

228
ERROR_CAPTURE_GUIDE.md Normal file
View File

@ -0,0 +1,228 @@
# 错误捕获和日志系统使用指南
## 🎯 系统功能概述
本系统已集成了完整的错误捕获和日志记录功能,能够自动捕获、分类保存和查看各种类型的错误信息。
## 📁 错误日志分类
### 自动分类保存
所有错误信息会自动保存到 `logs/errors/` 目录,按类型分类:
1. **启动错误** (`startup-error-*.log`)
- 应用启动失败
- Spring Boot初始化异常
- 配置加载错误
2. **运行时错误** (`runtime-error-*.log`)
- 空指针异常
- 非法参数异常
- Java模块访问异常
3. **数据库错误** (`database-error-*.log`)
- 数据库连接失败
- SQL执行异常
- MyBatis持久化错误
4. **业务错误** (`business-error-*.log`)
- 监控服务异常
- 微信通知发送失败
- 业务逻辑处理错误
5. **启动信息** (`startup-info.log`)
- 应用启动过程记录
- 数据库健康检查结果
- 系统配置信息
## 🚀 启动和查看错误
### 启动应用
```cmd
# 使用增强的调试脚本启动(推荐)
start_debug.bat
# 或使用简单启动脚本
start_with_logging.bat
```
### 查看错误信息
#### 1. 文件系统方式
```cmd
# 查看所有错误日志文件
dir logs\errors\*.log
# 查看启动错误(最重要)
type logs\errors\startup-error-*.log
# 查看启动信息
type logs\errors\startup-info.log
# 查看最新的运行时错误
type logs\errors\runtime-error-*.log
```
#### 2. Web接口方式
应用启动后,访问以下接口:
- **错误状态概览**: http://localhost:8080/api/error-logs/status
- **错误文件列表**: http://localhost:8080/api/error-logs/files
- **读取具体文件**: http://localhost:8080/api/error-logs/content?fileName=startup-error-2025-08-12.log
## 🔍 错误诊断流程
### 1. 应用启动失败
```cmd
# 运行调试脚本
start_debug.bat
# 检查启动错误日志
type logs\errors\startup-error-*.log
# 检查启动信息
type logs\errors\startup-info.log
# 检查数据库连接
netstat -an | findstr 5432
```
### 2. 运行时错误
```cmd
# 查看运行时错误日志
type logs\errors\runtime-error-*.log
# 查看业务错误日志
type logs\errors\business-error-*.log
# 通过Web接口查看
# 访问: http://localhost:8080/api/error-logs/status
```
### 3. 数据库相关错误
```cmd
# 查看数据库错误日志
type logs\errors\database-error-*.log
# 查看数据库健康检查结果
type logs\errors\startup-info.log | findstr "数据库\|健康检查"
```
## 📊 Web错误监控界面
### API接口说明
1. **GET /api/error-logs/status**
- 获取错误统计信息
- 返回各类错误数量和日志文件大小
2. **GET /api/error-logs/files**
- 获取所有错误日志文件列表
- 包含文件大小、修改时间等信息
3. **GET /api/error-logs/content?fileName=xxx&lines=100**
- 读取指定日志文件内容
- 默认显示最后100行
4. **DELETE /api/error-logs/cleanup?keepDays=7**
- 清理超过指定天数的旧日志文件
- 默认保留7天
### 使用示例
```bash
# 获取错误状态
curl http://localhost:8080/api/error-logs/status
# 获取文件列表
curl http://localhost:8080/api/error-logs/files
# 读取启动错误日志
curl "http://localhost:8080/api/error-logs/content?fileName=startup-error-2025-08-12.log&lines=50"
```
## 🛠️ 故障排除
### 常见问题及解决方案
#### 1. Java模块系统兼容性错误
```
错误: java.lang.reflect.InaccessibleObjectException
解决: 应用已自动设置所需的JVM参数无需手动处理
```
#### 2. 数据库连接失败
```
错误: 数据库连接异常
检查:
- PostgreSQL是否启动 (netstat -an | findstr 5432)
- 数据库是否已创建 (devops_gd)
- 用户名密码是否正确 (postgres/123456)
```
#### 3. 端口占用
```
错误: 端口8080已被占用
解决: netstat -ano | findstr 8080 找到进程并结束
```
#### 4. Maven参数解析错误
```
错误: Unknown lifecycle phase
解决: 使用提供的批处理脚本启动避免PowerShell参数解析问题
```
## 📝 日志维护
### 自动清理
```bash
# 通过Web接口清理7天前的日志
curl -X DELETE "http://localhost:8080/api/error-logs/cleanup?keepDays=7"
```
### 手动清理
```cmd
# 删除所有错误日志(谨慎操作)
del logs\errors\*.log
# 删除超过7天的日志文件
forfiles /p logs\errors /s /m *.log /d -7 /c "cmd /c del @path"
```
## 🔧 系统配置
### 错误日志配置
- **位置**: `src/main/resources/logback-spring.xml`
- **错误工具类**: `com.chinaweal.youfool.devops.util.ErrorLogUtils`
- **全局异常处理**: `com.chinaweal.youfool.devops.config.GlobalExceptionHandler`
### 数据库健康检查
- **组件**: `com.chinaweal.youfool.devops.config.DatabaseHealthChecker`
- **执行时机**: 应用启动完成后自动执行
- **检查内容**: 数据库连接、基本信息获取
## 📋 最佳实践
1. **定期检查错误状态**
- 访问 http://localhost:8080/api/error-logs/status
- 关注错误数量变化趋势
2. **及时处理启动错误**
- 启动失败时优先查看 `startup-error-*.log`
- 根据错误信息进行针对性修复
3. **监控数据库健康**
- 关注 `database-error-*.log` 中的连接失败记录
- 定期检查数据库健康检查结果
4. **日志文件管理**
- 定期清理旧日志文件避免磁盘空间不足
- 保留重要的错误日志用于问题分析
## 🆘 紧急情况处理
如果遇到严重错误无法启动:
1. **查看最新的启动错误日志**
2. **检查数据库服务状态**
3. **确认Java环境和Maven配置**
4. **使用Web接口如果应用部分可用获取详细错误信息**
5. **将错误日志文件提供给开发人员进行进一步分析**

153
README.md
View File

@ -1,3 +1,152 @@
**运维管理系统CW**
**相关文档**
# YouFool DevOps 广东运维管理系统 (CW)
[![Spring Boot](https://img.shields.io/badge/Spring%20Boot-2.7.18-brightgreen.svg)](https://spring.io/projects/spring-boot)
[![Java](https://img.shields.io/badge/Java-21-orange.svg)](https://openjdk.org/)
[![PostgreSQL](https://img.shields.io/badge/PostgreSQL-11.7-blue.svg)](https://www.postgresql.org/)
[![SpringDoc OpenAPI](https://img.shields.io/badge/SpringDoc-1.8.0-green.svg)](https://springdoc.org/)
## 🎯 项目概览
YouFool DevOps 运维管理系统(广东版)是基于 Spring Boot 框架开发的企业级运维管理平台。
### 核心特性
- 🔧 **运维报障管理**: 完整的故障报告、处理流程和状态跟踪
- 👥 **组织架构管理**: 用户、工程师信息管理和权限控制
- 📋 **任务分配系统**: 领导任务分配和工作流管理
- 📊 **数据统计分析**: 多维度数据统计和报表生成
- 🔄 **实时通信**: WebSocket 支持的实时消息推送
- 📝 **错误日志管理**: 可配置的错误捕获和日志管理系统
## 🏗️ 技术架构
### 运行环境
- **Java**: JDK 21 (OpenJDK)
- **框架**: Spring Boot 2.7.18
- **构建工具**: Maven 3.x
- **容器**: Apache Tomcat 9.0.83
### 数据层
- **数据库**: PostgreSQL 11.7
- **连接池**: Druid 1.2.23
- **ORM**: MyBatis-Plus 3.5.7
- **数据源**: 双数据源架构 (devops + youfool)
### 核心依赖
- **企业框架**: youfool-framework-springboot 1.1.1-SNAPSHOT
- **安全框架**: Apache Shiro 1.12.0
- **API文档**: SpringDoc OpenAPI 1.8.0
- **HTTP客户端**: Forest 1.5.16
- **Excel处理**: EasyExcel 2.2.6 + Spire.XLS 3.9.1
### 技术亮点
- ✅ **JDK 21 现代化升级**: 从 JDK 1.8 升级至 JDK 21支持最新 Java 特性
- ✅ **SpringDoc OpenAPI**: 从 SpringFox 迁移至现代化 API 文档解决方案
- ✅ **双数据源架构**: 支持 devops 和 youfool 两个数据源的事务管理
- ✅ **异步错误日志**: 高性能的错误日志捕获和管理系统
## 📂 项目结构
```
src/main/java/com/chinaweal/youfool/devops/
├── base/ # 基础环境模块
│ ├── controller/ # 基础数据控制器 (字典、任务、错误日志等)
│ ├── entity/ # 基础实体类
│ └── service/ # 基础服务层
├── repair/ # 运维报障模块 (核心业务)
│ ├── controller/ # 报障管理控制器
│ ├── entity/ # 报障相关实体类
│ ├── excel/ # Excel 导入导出
│ ├── scheduled/ # 定时任务
│ └── service/ # 业务服务层
├── org/ # 组织架构模块
│ ├── controller/ # 用户和工程师管理
│ ├── entity/ # 用户实体类
│ └── business/ # 业务系统集成
├── leaderassign/ # 领导分配模块
│ ├── controller/ # 任务分配控制器
│ └── entity/ # 分配任务实体
├── websocket/ # WebSocket 模块
│ └── server/ # 实时通信服务
├── config/ # 配置类
├── util/ # 工具类
└── DevOpsApplication.java # 应用主类
```
## 🚀 快速开始
### 环境要求
- JDK 21+
- PostgreSQL 11+
- Maven 3.6+
### 启动应用
```bash
# 开发环境启动
mvn spring-boot:run -Dspring-boot.run.profiles=dev
# 或使用优化的JDK 21启动脚本
./start-jdk21.sh # Linux/Mac
start-jdk21.bat # Windows
```
### 访问地址
- **应用主页**: http://localhost:8080
- **API文档**: http://localhost:8080/swagger-ui.html
- **Druid监控**: http://localhost:8080/druid (admin/123456)
- **错误日志管理**: http://localhost:8080/api/error-logs/
### API文档分组
- **组织架构**: 用户和工程师管理接口
- **运维报障**: 核心业务功能接口
- **基础环境**: 字典和基础数据接口
- **WebSocket测试环境**: 实时通信测试接口
- **领导分配**: 任务分配相关接口
## 📊 配置说明
### 环境配置
- `application-dev.yml`: 开发环境配置
- `application-prod.yml`: 生产环境配置
- `application-local.yml`: 本地开发配置
### 错误日志配置
系统支持完整的错误日志管理,可通过 `error-log.*` 配置项控制:
```yaml
error-log:
enabled: true # 全局开关
startup-enabled: true # 启动错误日志
runtime-enabled: true # 运行时错误日志
database-enabled: true # 数据库错误日志
business-enabled: true # 业务错误日志
log-directory: logs/errors # 日志目录
async-write: true # 异步写入
```
## 🛡️ 系统监控
### 错误日志管理
- **Web界面**: `/api/error-logs/` 提供完整的日志管理功能
- **日志分类**: 启动、运行时、数据库、业务错误分类存储
- **自动清理**: 支持按天数自动清理历史日志
- **性能优化**: 异步写入,不影响业务性能
### 数据库健康检查
- **双数据源监控**: devops 和 youfool 数据源健康状态检查
- **连接信息展示**: 启动时显示数据库连接详情
- **表访问测试**: ENGINEER 表数据访问验证
### Druid 连接池监控
- **访问地址**: http://localhost:8080/druid
- **监控内容**: SQL执行统计、连接池状态、慢SQL分析
- **登录信息**: admin / 123456
## 🔒 安全特性
- **Apache Shiro**: 基于角色的访问控制 (RBAC)
- **JWT Token**: 10小时后端令牌生命周期
- **RSA 加密**: 敏感数据加密传输
- **SM3 哈希**: 密码安全哈希存储
- **请求拦截**: 全局登录状态验证

File diff suppressed because it is too large Load Diff

131
pom.xml
View File

@ -14,17 +14,32 @@
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.6.RELEASE</version>
<version>2.7.18</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<java.version>1.8</java.version>
<java.version>21</java.version>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
<maven.compiler.release>21</maven.compiler.release>
<skipTests>true</skipTests>
<log4j.version>2.17.1</log4j.version>
<logback.version>1.2.9</logback.version>
<shiro.version>1.12.0</shiro.version>
<!-- JDK 21兼容版本 -->
<lombok.version>1.18.34</lombok.version>
<mybatis.version>3.5.16</mybatis.version>
<druid.version>1.2.23</druid.version>
<mybatis-plus.version>3.5.7</mybatis-plus.version>
<!-- SpringDoc OpenAPI for Swagger替换SpringFox -->
<springdoc.version>1.8.0</springdoc.version>
</properties>
<dependencies>
<!--Spring Boot Web Starter-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--基础框架youfool-framework-boot-->
<dependency>
<groupId>com.chinaweal.youfool</groupId>
@ -43,19 +58,36 @@
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
</exclusion>
<!-- 排除SpringFox相关依赖使用SpringDoc OpenAPI替代 -->
<exclusion>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-spring-boot-starter</artifactId>
</exclusion>
<exclusion>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
</exclusion>
<exclusion>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
</exclusion>
<exclusion>
<groupId>io.springfox</groupId>
<artifactId>springfox-boot-starter</artifactId>
</exclusion>
</exclusions>
</dependency>
<!--druid-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.21</version>
<version>${druid.version}</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.12</version>
<version>${lombok.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
@ -130,7 +162,7 @@
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.6</version>
<version>${mybatis.version}</version>
</dependency>
<!-- shiro -->
<dependency>
@ -138,13 +170,100 @@
<artifactId>shiro-spring</artifactId>
<version>${shiro.version}</version>
</dependency>
<!-- Spring Boot validation starter (required for @Valid annotations) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<!-- SpringDoc OpenAPI 3 (替换SpringFox/Knife4j) -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-ui</artifactId>
<version>${springdoc.version}</version>
</dependency>
<!-- SpringDoc OpenAPI WebMVC支持 -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-webmvc-core</artifactId>
<version>${springdoc.version}</version>
</dependency>
<!-- 临时依赖SpringFox注解兼容仅用于编译将逐步迁移到SpringDoc -->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.9.2</version>
<scope>compile</scope>
</dependency>
<!-- 临时依赖Knife4j注解兼容 -->
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-annotations</artifactId>
<version>2.0.9</version>
<scope>compile</scope>
</dependency>
</dependencies>
<build>
<finalName>devops-api</finalName>
<finalName>devops-api-gd</finalName>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<jvmArguments>
--add-opens java.base/java.util=ALL-UNNAMED
--add-opens java.base/java.lang=ALL-UNNAMED
--add-opens java.base/java.lang.reflect=ALL-UNNAMED
--add-opens java.base/java.time=ALL-UNNAMED
--add-opens java.desktop/java.beans=ALL-UNNAMED
</jvmArguments>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.12.1</version>
<configuration>
<source>21</source>
<target>21</target>
<release>21</release>
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</path>
</annotationProcessorPaths>
<compilerArgs>
<arg>--add-opens</arg>
<arg>java.base/java.util=ALL-UNNAMED</arg>
<arg>--add-opens</arg>
<arg>java.base/java.lang=ALL-UNNAMED</arg>
<arg>--add-opens</arg>
<arg>java.base/java.lang.reflect=ALL-UNNAMED</arg>
<arg>-Xlint:deprecation</arg>
<arg>-Xlint:unchecked</arg>
<arg>-Xlint:-options</arg>
</compilerArgs>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.2.5</version>
<configuration>
<argLine>
--add-opens java.base/java.util=ALL-UNNAMED
--add-opens java.base/java.lang=ALL-UNNAMED
--add-opens java.base/java.lang.reflect=ALL-UNNAMED
--add-opens java.base/java.time=ALL-UNNAMED
</argLine>
</configuration>
</plugin>
</plugins>
</build>

View File

@ -1,7 +1,14 @@
package com.chinaweal.youfool.devops;
import com.chinaweal.youfool.devops.config.ErrorLogProperties;
import com.chinaweal.youfool.devops.util.ErrorLogUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import javax.sql.DataSource;
import java.sql.Connection;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
@ -10,17 +17,30 @@ import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.scheduling.annotation.EnableScheduling;
import java.util.Arrays;
@SpringBootApplication(scanBasePackages = {"com.chinaweal"})
@Slf4j
@EnableScheduling
public class DevOpsApplication extends SpringBootServletInitializer implements ApplicationListener<ContextRefreshedEvent> {
@Value("${applicationName}")
@Value("${applicationName:devOps}")
private String applicationName;
@Value("${version}")
@Value("${version:1.0.0}")
private String version;
@Value("${description}")
@Value("${description:运维管理系统}")
private String description;
@Autowired
private ErrorLogProperties errorLogProperties;
@Autowired
@Qualifier("devopsDS")
private DataSource devopsDataSource;
@Autowired
@Qualifier("youfoolDS")
private DataSource youfoolDataSource;
@Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder applicationBuilder) {
return applicationBuilder.sources(DevOpsApplication.class);
@ -29,17 +49,229 @@ public class DevOpsApplication extends SpringBootServletInitializer implements A
@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
if (event.getApplicationContext().getParent() == null) {
try {
log.info("========================== 程序启动成功! ==========================");
log.info("====== 程 序:{} ", applicationName);
log.info("====== 版本号:{} ", version);
log.info("====== 描 述:{} ", description);
log.info("====== Java环境{}", getJavaVersionInfo());
log.info("====== 接口文档路径:/doc.html账号admin、密码123456。注如果乱码请指定VM -Dfile.encoding=UTF-8");
log.info("====== Druid Monitor路径/druid账号admin、密码123456");
// 显示错误日志配置状态
if (errorLogProperties != null) {
log.info("====== 错误日志配置:启用={}, 目录={}",
errorLogProperties.isEnabled(), errorLogProperties.getLogDirectory());
log.info("====== 错误日志类型:启动={}, 运行时={}, 数据库={}, 业务={}",
errorLogProperties.isStartupEnabled(),
errorLogProperties.isRuntimeEnabled(),
errorLogProperties.isDatabaseEnabled(),
errorLogProperties.isBusinessEnabled());
}
// 打印数据库连接信息
printDatabaseConnectionInfo();
log.info("====================================================================");
// 记录启动成功信息
ErrorLogUtils.logStartupInfo("应用启动成功 - " + applicationName + " v" + version);
} catch (Exception e) {
log.error("启动成功回调处理异常", e);
ErrorLogUtils.saveStartupError("启动成功回调异常", e);
}
}
}
public static void main(String[] args) {
SpringApplication.run(DevOpsApplication.class, args);
try {
// 记录启动开始
ErrorLogUtils.logStartupInfo("开始启动应用,参数: " + Arrays.toString(args));
// 设置默认的JVM参数以解决Java模块系统兼容性问题
setJavaModuleOptions();
// 启动Spring Boot应用
SpringApplication app = new SpringApplication(DevOpsApplication.class);
// 添加启动失败监听器
app.addListeners(event -> {
if (event instanceof org.springframework.boot.context.event.ApplicationFailedEvent) {
org.springframework.boot.context.event.ApplicationFailedEvent failedEvent =
(org.springframework.boot.context.event.ApplicationFailedEvent) event;
Throwable exception = failedEvent.getException();
log.error("应用启动失败", exception);
ErrorLogUtils.saveStartupError("应用启动失败", exception);
// 输出友好的错误信息
System.err.println("\n==================== 应用启动失败 ====================");
System.err.println("错误信息已保存到: logs/errors/startup-error-*.log");
System.err.println("详细错误信息: " + ErrorLogUtils.formatErrorInfo("启动失败", exception));
System.err.println("=================================================\n");
}
});
app.run(args);
// 添加JVM关闭钩子以确保异步日志写入器正确关闭
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
log.info("应用正在关闭,清理错误日志资源...");
ErrorLogUtils.shutdown();
}));
} catch (Exception e) {
log.error("应用启动异常", e);
ErrorLogUtils.saveStartupError("主方法启动异常", e);
// 输出友好的错误信息到控制台
System.err.println("\n==================== 应用启动异常 ====================");
System.err.println("错误信息已保存到: logs/errors/startup-error-*.log");
System.err.println("详细错误信息: " + ErrorLogUtils.formatErrorInfo("启动异常", e));
System.err.println("=================================================\n");
// 重新抛出异常以确保程序正确退出
throw new RuntimeException("应用启动失败", e);
}
}
/**
* 设置Java版本兼容性参数
*/
private static void setJavaModuleOptions() {
try {
String javaVersion = System.getProperty("java.version");
ErrorLogUtils.logStartupInfo("当前Java版本: " + javaVersion);
// 只在Java 9+版本设置模块参数
if (isJava9OrHigher()) {
String javaToolOptions = System.getProperty("JAVA_TOOL_OPTIONS", "");
// 检查是否已经设置了必要的模块参数
if (!javaToolOptions.contains("--add-opens java.base/java.util=ALL-UNNAMED")) {
String newOptions = javaToolOptions +
" --add-opens java.base/java.util=ALL-UNNAMED" +
" --add-opens java.base/java.lang=ALL-UNNAMED" +
" --add-opens java.base/java.lang.reflect=ALL-UNNAMED" +
" --add-opens java.base/java.time=ALL-UNNAMED";
System.setProperty("JAVA_TOOL_OPTIONS", newOptions.trim());
ErrorLogUtils.logStartupInfo("已设置Java模块兼容性参数: " + newOptions.trim());
}
} else {
ErrorLogUtils.logStartupInfo("检测到Java 8跳过模块系统参数设置");
}
} catch (Exception e) {
log.warn("检查Java版本失败", e);
ErrorLogUtils.saveStartupError("Java版本检查失败", e);
}
}
/**
* 检查是否为Java 9或更高版本
*/
private static boolean isJava9OrHigher() {
try {
String version = System.getProperty("java.version");
// Java 8: 1.8.x, Java 9+: 9.x, 10.x, 11.x, 21.x, etc.
if (version.startsWith("1.8")) {
return false;
}
// 尝试解析主版本号
String[] parts = version.split("\\.");
if (parts.length > 0) {
int majorVersion = Integer.parseInt(parts[0]);
return majorVersion >= 9;
}
} catch (Exception e) {
log.warn("解析Java版本失败: " + System.getProperty("java.version"), e);
}
return true; // JDK 21环境下默认为true
}
/**
* 获取当前Java版本信息
*/
private static String getJavaVersionInfo() {
try {
String version = System.getProperty("java.version");
String vendor = System.getProperty("java.vendor");
String vmName = System.getProperty("java.vm.name");
return String.format("Java %s (%s - %s)", version, vendor, vmName);
} catch (Exception e) {
return "Java version unknown";
}
}
/**
* 打印数据库连接信息
*/
private void printDatabaseConnectionInfo() {
try {
log.info("========== 数据库连接信息 ==========");
// 打印devops数据源信息
try (Connection conn = devopsDataSource.getConnection()) {
String url = conn.getMetaData().getURL();
String username = conn.getMetaData().getUserName();
String databaseName = conn.getCatalog();
log.info("🔗 Devops数据源:");
log.info(" URL: {}", url);
log.info(" 用户: {}", username);
log.info(" 数据库: {}", databaseName);
ErrorLogUtils.logStartupInfo("Devops数据源连接: " + url + ", 用户: " + username + ", 数据库: " + databaseName);
} catch (Exception e) {
log.error("❌ 获取devops数据源信息失败", e);
ErrorLogUtils.saveStartupError("获取devops数据源信息失败", e);
}
// 打印youfool数据源信息
try (Connection conn = youfoolDataSource.getConnection()) {
String url = conn.getMetaData().getURL();
String username = conn.getMetaData().getUserName();
String databaseName = conn.getCatalog();
log.info("🔗 Youfool数据源:");
log.info(" URL: {}", url);
log.info(" 用户: {}", username);
log.info(" 数据库: {}", databaseName);
ErrorLogUtils.logStartupInfo("Youfool数据源连接: " + url + ", 用户: " + username + ", 数据库: " + databaseName);
} catch (Exception e) {
log.error("❌ 获取youfool数据源信息失败", e);
ErrorLogUtils.saveStartupError("获取youfool数据源信息失败", e);
}
// 测试ENGINEER表访问
try (Connection conn = devopsDataSource.getConnection()) {
// 简单测试查询
String testSql = "SELECT COUNT(*) as total FROM engineer";
try (java.sql.PreparedStatement ps = conn.prepareStatement(testSql);
java.sql.ResultSet rs = ps.executeQuery()) {
if (rs.next()) {
int total = rs.getInt("total");
log.info("✅ ENGINEER表测试: 共{}条记录", total);
ErrorLogUtils.logStartupInfo("ENGINEER表访问测试成功共" + total + "条记录");
}
} catch (Exception e) {
log.error("❌ ENGINEER表访问测试失败", e);
ErrorLogUtils.saveStartupError("ENGINEER表访问测试失败", e);
}
} catch (Exception e) {
log.error("❌ 数据库连接测试失败", e);
ErrorLogUtils.saveStartupError("数据库连接测试失败", e);
}
log.info("=====================================");
} catch (Exception e) {
log.error("❌ 打印数据库连接信息时发生异常", e);
ErrorLogUtils.saveStartupError("打印数据库连接信息异常", e);
}
}
}

View File

@ -0,0 +1,238 @@
package com.chinaweal.youfool.devops.base.controller;
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.beans.factory.annotation.Qualifier;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 数据库测试控制器
* 用于测试ENGINEER表的SQL查询问题
*/
@RestController
@RequestMapping("/api/test")
@Api(tags = "数据库测试接口")
@Slf4j
public class DatabaseTestController {
@Autowired
@Qualifier("devopsDS")
private DataSource devopsDataSource;
@Autowired
@Qualifier("youfoolDS")
private DataSource youfoolDataSource;
/**
* 测试数据库连接信息
*/
@GetMapping("/connection")
@ApiOperation("测试数据库连接信息")
public Map<String, Object> testConnection() {
Map<String, Object> result = new HashMap<>();
try {
// 测试devops数据源
try (Connection conn = devopsDataSource.getConnection()) {
result.put("devops_url", conn.getMetaData().getURL());
result.put("devops_username", conn.getMetaData().getUserName());
result.put("devops_database", conn.getCatalog());
}
// 测试youfool数据源
try (Connection conn = youfoolDataSource.getConnection()) {
result.put("youfool_url", conn.getMetaData().getURL());
result.put("youfool_username", conn.getMetaData().getUserName());
result.put("youfool_database", conn.getCatalog());
}
result.put("success", true);
result.put("message", "数据库连接测试成功");
} catch (Exception e) {
log.error("数据库连接测试失败", e);
result.put("success", false);
result.put("message", "连接测试失败: " + e.getMessage());
}
return result;
}
/**
* 测试ENGINEER表结构
*/
@GetMapping("/engineer-structure")
@ApiOperation("测试ENGINEER表结构")
public Map<String, Object> testEngineerStructure() {
Map<String, Object> result = new HashMap<>();
try {
// 使用devops数据源测试
try (Connection conn = devopsDataSource.getConnection()) {
// 检查表是否存在
String checkTableSql = "SELECT COUNT(*) as table_count FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'engineer'";
try (PreparedStatement ps = conn.prepareStatement(checkTableSql);
ResultSet rs = ps.executeQuery()) {
if (rs.next()) {
result.put("table_exists", rs.getInt("table_count") > 0);
}
}
// 获取字段信息
String fieldsSql = "SELECT column_name, data_type, is_nullable FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'engineer' ORDER BY ordinal_position";
List<Map<String, Object>> fields = new ArrayList<>();
try (PreparedStatement ps = conn.prepareStatement(fieldsSql);
ResultSet rs = ps.executeQuery()) {
while (rs.next()) {
Map<String, Object> field = new HashMap<>();
field.put("name", rs.getString("column_name"));
field.put("type", rs.getString("data_type"));
field.put("nullable", rs.getString("is_nullable"));
fields.add(field);
}
}
result.put("fields", fields);
result.put("datasource", "devops");
result.put("success", true);
}
} catch (Exception e) {
log.error("ENGINEER表结构测试失败", e);
result.put("success", false);
result.put("message", "测试失败: " + e.getMessage());
result.put("error", e.getClass().getSimpleName());
}
return result;
}
/**
* 测试问题SQL - 分步执行
*/
@GetMapping("/engineer-query")
@ApiOperation("测试ENGINEER表查询")
public Map<String, Object> testEngineerQuery() {
Map<String, Object> result = new HashMap<>();
try {
// 使用devops数据源测试
try (Connection conn = devopsDataSource.getConnection()) {
result.put("connection_url", conn.getMetaData().getURL());
result.put("connection_user", conn.getMetaData().getUserName());
// 测试1: 简单查询
String simpleSql = "SELECT COUNT(*) as total FROM ENGINEER";
try (PreparedStatement ps = conn.prepareStatement(simpleSql);
ResultSet rs = ps.executeQuery()) {
if (rs.next()) {
result.put("total_records", rs.getInt("total"));
}
result.put("simple_query", "✓ 成功");
} catch (Exception e) {
result.put("simple_query", "✗ 失败: " + e.getMessage());
}
// 测试2: 测试USER_ID字段
String userIdSql = "SELECT USER_ID FROM ENGINEER LIMIT 1";
try (PreparedStatement ps = conn.prepareStatement(userIdSql);
ResultSet rs = ps.executeQuery()) {
result.put("user_id_query", "✓ 成功");
if (rs.next()) {
result.put("sample_user_id", rs.getString("USER_ID"));
}
} catch (Exception e) {
result.put("user_id_query", "✗ 失败: " + e.getMessage());
}
// 测试3: 测试完整的问题SQL
String fullSql = "SELECT USER_ID, USERNAME, PASSWORD, NICKNAME, SEX, PHONE, EMAIL, STATUS, " +
"DESCRIPTION, SORT, ROLES, SOURCE, IS_DELETED, create_by, " +
"create_time, update_by, update_time FROM ENGINEER WHERE IS_DELETED='0' LIMIT 1";
try (PreparedStatement ps = conn.prepareStatement(fullSql);
ResultSet rs = ps.executeQuery()) {
result.put("full_query", "✓ 成功");
if (rs.next()) {
Map<String, Object> sampleData = new HashMap<>();
sampleData.put("USER_ID", rs.getString("USER_ID"));
sampleData.put("USERNAME", rs.getString("USERNAME"));
sampleData.put("IS_DELETED", rs.getString("IS_DELETED"));
result.put("sample_data", sampleData);
}
} catch (Exception e) {
result.put("full_query", "✗ 失败: " + e.getMessage());
result.put("full_query_error", e.getClass().getSimpleName());
}
// 测试4: 测试带参数的查询(模拟实际应用场景)
String paramSql = "SELECT USER_ID, USERNAME FROM ENGINEER WHERE IS_DELETED='0' AND USERNAME = ?";
try (PreparedStatement ps = conn.prepareStatement(paramSql)) {
ps.setString(1, "test");
try (ResultSet rs = ps.executeQuery()) {
result.put("param_query", "✓ 成功");
}
} catch (Exception e) {
result.put("param_query", "✗ 失败: " + e.getMessage());
result.put("param_query_error", e.getClass().getSimpleName());
}
result.put("success", true);
}
} catch (Exception e) {
log.error("ENGINEER查询测试失败", e);
result.put("success", false);
result.put("message", "查询测试失败: " + e.getMessage());
result.put("error_class", e.getClass().getSimpleName());
result.put("error_cause", e.getCause() != null ? e.getCause().getMessage() : null);
}
return result;
}
/**
* 测试MyBatis查询使用相同的数据源配置
*/
@GetMapping("/mybatis-test")
@ApiOperation("测试MyBatis数据源配置")
public Map<String, Object> testMyBatisDataSource() {
Map<String, Object> result = new HashMap<>();
try {
// 检查数据源配置
result.put("devops_datasource_class", devopsDataSource.getClass().getSimpleName());
result.put("youfool_datasource_class", youfoolDataSource.getClass().getSimpleName());
// 测试两个数据源是否指向同一个数据库
try (Connection devopsConn = devopsDataSource.getConnection();
Connection youfoolConn = youfoolDataSource.getConnection()) {
result.put("devops_url", devopsConn.getMetaData().getURL());
result.put("youfool_url", youfoolConn.getMetaData().getURL());
result.put("same_database", devopsConn.getMetaData().getURL().equals(youfoolConn.getMetaData().getURL()));
}
result.put("success", true);
} catch (Exception e) {
log.error("MyBatis数据源测试失败", e);
result.put("success", false);
result.put("message", "测试失败: " + e.getMessage());
}
return result;
}
}

View File

@ -0,0 +1,299 @@
package com.chinaweal.youfool.devops.base.controller;
import com.chinaweal.youfool.devops.config.ErrorLogProperties;
import com.chinaweal.youfool.devops.util.ErrorLogUtils;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.stream.Collectors;
/**
* 错误日志查看控制器
* 提供Web接口查看应用错误日志
*/
@RestController
@RequestMapping("/api/error-logs")
@Api(tags = "错误日志管理")
@Slf4j
public class ErrorLogController {
@Autowired
private ErrorLogProperties errorLogProperties;
private static final String MAIN_LOG_DIR = "logs";
/**
* 获取错误日志配置信息
*/
@GetMapping("/config")
@ApiOperation("获取错误日志配置信息")
public Map<String, Object> getErrorLogConfig() {
Map<String, Object> result = new HashMap<>();
try {
result.put("success", true);
result.put("configInfo", ErrorLogUtils.getConfigurationInfo());
result.put("properties", errorLogProperties);
result.put("timestamp", new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
} catch (Exception e) {
log.error("获取错误日志配置失败", e);
result.put("success", false);
result.put("message", "获取配置失败: " + e.getMessage());
}
return result;
}
/**
* 获取错误日志文件列表
*/
@GetMapping("/files")
@ApiOperation("获取错误日志文件列表")
public Map<String, Object> getErrorLogFiles() {
Map<String, Object> result = new HashMap<>();
try {
String errorLogDir = errorLogProperties != null ?
errorLogProperties.getLogDirectory() : "logs/errors";
File errorDir = new File(errorLogDir);
List<Map<String, Object>> errorFiles = new ArrayList<>();
if (errorDir.exists() && errorDir.isDirectory()) {
File[] files = errorDir.listFiles((dir, name) -> name.endsWith(".log"));
if (files != null) {
for (File file : files) {
Map<String, Object> fileInfo = new HashMap<>();
fileInfo.put("name", file.getName());
fileInfo.put("size", file.length());
fileInfo.put("lastModified", new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date(file.lastModified())));
fileInfo.put("path", file.getAbsolutePath());
errorFiles.add(fileInfo);
}
}
// 按修改时间倒序排列
errorFiles.sort((a, b) -> b.get("lastModified").toString().compareTo(a.get("lastModified").toString()));
}
// 同时获取主日志目录的错误相关文件
File mainLogDir = new File(MAIN_LOG_DIR);
List<Map<String, Object>> mainLogFiles = new ArrayList<>();
if (mainLogDir.exists() && mainLogDir.isDirectory()) {
File[] files = mainLogDir.listFiles((dir, name) ->
name.contains("error") || name.contains("startup") || name.contains("warn"));
if (files != null) {
for (File file : files) {
Map<String, Object> fileInfo = new HashMap<>();
fileInfo.put("name", file.getName());
fileInfo.put("size", file.length());
fileInfo.put("lastModified", new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date(file.lastModified())));
fileInfo.put("path", file.getAbsolutePath());
mainLogFiles.add(fileInfo);
}
}
// 按修改时间倒序排列
mainLogFiles.sort((a, b) -> b.get("lastModified").toString().compareTo(a.get("lastModified").toString()));
}
result.put("success", true);
result.put("errorLogFiles", errorFiles);
result.put("mainLogFiles", mainLogFiles);
result.put("errorLogDir", errorLogDir);
result.put("mainLogDir", MAIN_LOG_DIR);
result.put("errorLogEnabled", errorLogProperties != null && errorLogProperties.isEnabled());
} catch (Exception e) {
log.error("获取错误日志文件列表失败", e);
result.put("success", false);
result.put("message", "获取日志文件列表失败: " + e.getMessage());
}
return result;
}
/**
* 读取指定错误日志文件内容
*/
@GetMapping("/content")
@ApiOperation("读取错误日志文件内容")
public Map<String, Object> getErrorLogContent(
@ApiParam("文件名") @RequestParam String fileName,
@ApiParam("从末尾开始读取的行数默认100") @RequestParam(defaultValue = "100") int lines) {
Map<String, Object> result = new HashMap<>();
try {
// 安全检查:防止路径遍历攻击
if (fileName.contains("..") || fileName.contains("/") || fileName.contains("\\")) {
result.put("success", false);
result.put("message", "文件名包含非法字符");
return result;
}
String errorLogDir = errorLogProperties != null ?
errorLogProperties.getLogDirectory() : "logs/errors";
// 先在错误日志目录查找
Path filePath = Paths.get(errorLogDir, fileName);
if (!Files.exists(filePath)) {
// 如果错误日志目录没有,再在主日志目录查找
filePath = Paths.get(MAIN_LOG_DIR, fileName);
}
if (!Files.exists(filePath)) {
result.put("success", false);
result.put("message", "文件不存在: " + fileName);
return result;
}
// 读取文件内容
List<String> allLines = Files.readAllLines(filePath);
// 获取最后指定行数的内容
List<String> resultLines;
if (allLines.size() <= lines) {
resultLines = allLines;
} else {
resultLines = allLines.subList(allLines.size() - lines, allLines.size());
}
result.put("success", true);
result.put("fileName", fileName);
result.put("content", String.join("\n", resultLines));
result.put("totalLines", allLines.size());
result.put("displayedLines", resultLines.size());
result.put("filePath", filePath.toString());
} catch (IOException e) {
log.error("读取错误日志文件失败: {}", fileName, e);
result.put("success", false);
result.put("message", "读取文件失败: " + e.getMessage());
} catch (Exception e) {
log.error("处理错误日志请求失败: {}", fileName, e);
result.put("success", false);
result.put("message", "处理请求失败: " + e.getMessage());
}
return result;
}
/**
* 清理旧的错误日志文件
*/
@DeleteMapping("/cleanup")
@ApiOperation("清理旧的错误日志文件")
public Map<String, Object> cleanupOldLogs(
@ApiParam("保留最近几天的日志默认7天") @RequestParam(defaultValue = "7") int keepDays) {
Map<String, Object> result = new HashMap<>();
try {
String errorLogDir = errorLogProperties != null ?
errorLogProperties.getLogDirectory() : "logs/errors";
long cutoffTime = System.currentTimeMillis() - (keepDays * 24 * 60 * 60 * 1000L);
File errorDir = new File(errorLogDir);
int deletedCount = 0;
List<String> deletedFiles = new ArrayList<>();
if (errorDir.exists() && errorDir.isDirectory()) {
File[] files = errorDir.listFiles((dir, name) -> name.endsWith(".log"));
if (files != null) {
for (File file : files) {
if (file.lastModified() < cutoffTime) {
String fileName = file.getName();
if (file.delete()) {
deletedCount++;
deletedFiles.add(fileName);
log.info("删除旧的错误日志文件: {}", fileName);
} else {
log.warn("删除文件失败: {}", fileName);
}
}
}
}
}
result.put("success", true);
result.put("deletedCount", deletedCount);
result.put("deletedFiles", deletedFiles);
result.put("keepDays", keepDays);
result.put("message", "成功清理 " + deletedCount + " 个旧日志文件");
} catch (Exception e) {
log.error("清理错误日志文件失败", e);
result.put("success", false);
result.put("message", "清理失败: " + e.getMessage());
}
return result;
}
/**
* 获取系统状态和错误统计
*/
@GetMapping("/status")
@ApiOperation("获取系统错误状态统计")
public Map<String, Object> getErrorStatus() {
Map<String, Object> result = new HashMap<>();
try {
String errorLogDir = errorLogProperties != null ?
errorLogProperties.getLogDirectory() : "logs/errors";
File errorDir = new File(errorLogDir);
Map<String, Integer> errorCounts = new HashMap<>();
long totalErrorLogSize = 0;
if (errorDir.exists() && errorDir.isDirectory()) {
File[] files = errorDir.listFiles((dir, name) -> name.endsWith(".log"));
if (files != null) {
for (File file : files) {
totalErrorLogSize += file.length();
// 根据文件名统计错误类型
String fileName = file.getName();
if (fileName.contains("startup-error")) {
errorCounts.put("启动错误", errorCounts.getOrDefault("启动错误", 0) + 1);
} else if (fileName.contains("runtime-error")) {
errorCounts.put("运行时错误", errorCounts.getOrDefault("运行时错误", 0) + 1);
} else if (fileName.contains("database-error")) {
errorCounts.put("数据库错误", errorCounts.getOrDefault("数据库错误", 0) + 1);
} else if (fileName.contains("business-error")) {
errorCounts.put("业务错误", errorCounts.getOrDefault("业务错误", 0) + 1);
} else {
errorCounts.put("其他错误", errorCounts.getOrDefault("其他错误", 0) + 1);
}
}
}
}
result.put("success", true);
result.put("errorCounts", errorCounts);
result.put("totalErrorLogSize", totalErrorLogSize);
result.put("errorLogDir", errorLogDir);
result.put("errorLogEnabled", errorLogProperties != null && errorLogProperties.isEnabled());
result.put("timestamp", new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
} catch (Exception e) {
log.error("获取错误状态统计失败", e);
result.put("success", false);
result.put("message", "获取状态失败: " + e.getMessage());
}
return result;
}
}

View File

@ -2,6 +2,7 @@ package com.chinaweal.youfool.devops.base.service.impl;
import com.chinaweal.youfool.devops.base.service.IMonitorService;
import com.chinaweal.youfool.devops.repair.api.RobotApi;
import com.chinaweal.youfool.devops.util.ErrorLogUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.time.DateFormatUtils;
@ -30,6 +31,9 @@ public class MonitorServiceImpl implements IMonitorService {
@Override
public void countDbPwdExpireAndSendWx() throws Exception {
try {
log.info("开始执行数据库密码过期检查和微信通知");
List<String> list = new ArrayList<>();
//145数据库
list = findExpireUserInfo("jdbc:oracle:thin:@19.130.241.145:1521:FSAMRDATA", "system", "Chinaweal", "19.130.241.145");
@ -40,8 +44,11 @@ public class MonitorServiceImpl implements IMonitorService {
//顺德133数据库
list.addAll(findExpireUserInfo("jdbc:oracle:thin:@19.202.179.133:1521:SDAMRDATA", "system", "ChinaWeal_2020", "19.202.179.133"));
log.info("数据库密码过期检查完成,发现 {} 个即将过期的账号", list.size());
if (list.size() == 0) {
//没有账号过期,则不需要提醒
log.info("没有发现即将过期的数据库账号");
return;
}
@ -59,8 +66,24 @@ public class MonitorServiceImpl implements IMonitorService {
query.put("msgtype", "text");
query.put("text", content);
log.info("准备发送微信通知目标webhook数量: {}", dbWebhookKeys.length);
for (String dbWebhookKey : dbWebhookKeys) {
try {
robotApi.webhookSend(query, dbWebhookKey);
log.debug("微信通知发送成功webhook: {}", dbWebhookKey);
} catch (Exception e) {
log.error("微信通知发送失败webhook: {}", dbWebhookKey, e);
ErrorLogUtils.saveBusinessError("监控服务", "微信通知发送", e);
}
}
log.info("数据库密码过期检查和微信通知执行完成");
} catch (Exception e) {
log.error("数据库密码过期检查过程中发生异常", e);
ErrorLogUtils.saveBusinessError("监控服务", "数据库密码过期检查", e);
throw e; // 重新抛出异常以保持原有的异常处理逻辑
}
}

View File

@ -0,0 +1,187 @@
package com.chinaweal.youfool.devops.config;
import com.chinaweal.youfool.devops.util.ErrorLogUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Component;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 数据库健康检查组件
* 在应用启动时检查数据库连接状态
* 支持配置开关控制
*/
@Component
@ConditionalOnProperty(name = "error-log.database-enabled", havingValue = "true", matchIfMissing = true)
@Slf4j
public class DatabaseHealthChecker implements ApplicationRunner {
@Autowired
private ErrorLogProperties errorLogProperties;
@Autowired
@Qualifier("devopsDS")
private DataSource devopsDataSource;
@Autowired
@Qualifier("youfoolDS")
private DataSource youfoolDataSource;
@Override
public void run(ApplicationArguments args) {
// 检查是否启用了数据库健康检查
if (errorLogProperties == null || !errorLogProperties.isEnabled() || !errorLogProperties.isDatabaseEnabled()) {
log.info("数据库健康检查已禁用");
return;
}
log.info("开始数据库健康检查...");
ErrorLogUtils.logStartupInfo("开始数据库健康检查");
List<String> checkResults = new ArrayList<>();
boolean allHealthy = true;
// 检查DevOps数据源
boolean devopsHealthy = checkDataSource("devopsDS", devopsDataSource);
checkResults.add("DevOps数据源: " + (devopsHealthy ? "正常" : "异常"));
if (!devopsHealthy) allHealthy = false;
// 检查YouFool数据源
boolean youfoolHealthy = checkDataSource("youfoolDS", youfoolDataSource);
checkResults.add("YouFool数据源: " + (youfoolHealthy ? "正常" : "异常"));
if (!youfoolHealthy) allHealthy = false;
// 记录检查结果
String resultSummary = "数据库健康检查完成 - " +
(allHealthy ? "所有数据源正常" : "存在异常数据源");
log.info("数据库健康检查结果:");
for (String result : checkResults) {
log.info(" - {}", result);
}
ErrorLogUtils.logStartupInfo(resultSummary + ": " + String.join(", ", checkResults));
if (!allHealthy) {
log.warn("数据库健康检查发现问题,请检查数据库配置和连接状态");
}
}
/**
* 检查单个数据源的健康状态
*/
private boolean checkDataSource(String dataSourceName, DataSource dataSource) {
try {
log.debug("检查数据源: {}", dataSourceName);
try (Connection connection = dataSource.getConnection()) {
// 检查连接是否有效
if (connection == null || connection.isClosed()) {
log.error("数据源 {} 连接无效", dataSourceName);
ErrorLogUtils.saveDatabaseError(dataSourceName + "连接无效",
new RuntimeException("数据库连接为null或已关闭"));
return false;
}
// 执行简单查询测试连接
try (PreparedStatement ps = connection.prepareStatement("SELECT 1");
ResultSet rs = ps.executeQuery()) {
if (rs.next() && rs.getInt(1) == 1) {
log.debug("数据源 {} 连接测试成功", dataSourceName);
// 获取数据库基本信息
String dbInfo = getDatabaseInfo(connection);
log.info("数据源 {} 信息: {}", dataSourceName, dbInfo);
ErrorLogUtils.logStartupInfo(dataSourceName + " 连接成功 - " + dbInfo);
return true;
} else {
log.error("数据源 {} 查询测试失败", dataSourceName);
ErrorLogUtils.saveDatabaseError(dataSourceName + "查询测试失败",
new RuntimeException("SELECT 1 查询返回异常结果"));
return false;
}
}
}
} catch (Exception e) {
log.error("数据源 {} 健康检查失败", dataSourceName, e);
ErrorLogUtils.saveDatabaseError(dataSourceName + "健康检查失败", e);
return false;
}
}
/**
* 获取数据库基本信息
*/
private String getDatabaseInfo(Connection connection) {
try {
String dbName = connection.getMetaData().getDatabaseProductName();
String dbVersion = connection.getMetaData().getDatabaseProductVersion();
String driverName = connection.getMetaData().getDriverName();
String driverVersion = connection.getMetaData().getDriverVersion();
String url = connection.getMetaData().getURL();
String userName = connection.getMetaData().getUserName();
return String.format("%s %s (Driver: %s %s, URL: %s, User: %s)",
dbName, dbVersion, driverName, driverVersion, url, userName);
} catch (Exception e) {
log.warn("获取数据库信息失败", e);
return "无法获取数据库信息";
}
}
/**
* 手动触发数据库健康检查供其他组件调用
*/
public boolean performHealthCheck() {
// 检查是否启用了数据库健康检查
if (errorLogProperties == null || !errorLogProperties.isEnabled() || !errorLogProperties.isDatabaseEnabled()) {
log.warn("数据库健康检查已禁用,跳过手动检查");
return true; // 返回true表示没有问题因为检查被禁用
}
log.info("手动触发数据库健康检查");
boolean devopsHealthy = checkDataSource("devopsDS", devopsDataSource);
boolean youfoolHealthy = checkDataSource("youfoolDS", youfoolDataSource);
boolean allHealthy = devopsHealthy && youfoolHealthy;
String result = "手动健康检查结果: DevOps=" +
(devopsHealthy ? "正常" : "异常") + ", YouFool=" +
(youfoolHealthy ? "正常" : "异常");
log.info(result);
ErrorLogUtils.logStartupInfo(result);
return allHealthy;
}
/**
* 获取健康检查配置状态
*/
public Map<String, Object> getHealthCheckStatus() {
Map<String, Object> status = new HashMap<>();
status.put("enabled", errorLogProperties != null &&
errorLogProperties.isEnabled() &&
errorLogProperties.isDatabaseEnabled());
status.put("errorLogEnabled", errorLogProperties != null && errorLogProperties.isEnabled());
status.put("databaseCheckEnabled", errorLogProperties != null && errorLogProperties.isDatabaseEnabled());
return status;
}
}

View File

@ -0,0 +1,109 @@
package com.chinaweal.youfool.devops.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
* 错误日志配置属性
*/
@Component
@ConfigurationProperties(prefix = "error-log")
@Data
public class ErrorLogProperties {
/**
* 是否启用错误日志文件写入功能
* 默认启用
*/
private boolean enabled = true;
/**
* 是否启用启动错误日志
* 默认启用
*/
private boolean startupEnabled = true;
/**
* 是否启用运行时错误日志
* 默认启用
*/
private boolean runtimeEnabled = true;
/**
* 是否启用数据库错误日志
* 默认启用
*/
private boolean databaseEnabled = true;
/**
* 是否启用业务错误日志
* 默认启用
*/
private boolean businessEnabled = true;
/**
* 是否启用启动信息日志
* 默认启用
*/
private boolean startupInfoEnabled = true;
/**
* 错误日志目录
* 默认为 logs/errors
*/
private String logDirectory = "logs/errors";
/**
* 日志文件最大大小
* 默认为 10MB
*/
private String maxFileSize = "10MB";
/**
* 保留日志文件的最大天数
* 默认为 30
*/
private int maxHistory = 30;
/**
* 是否在控制台同时输出错误信息
* 默认启用
*/
private boolean consoleOutput = true;
/**
* 错误日志的详细级别
* 0: 仅错误消息
* 1: 错误消息 + 异常类型
* 2: 错误消息 + 异常类型 + 简化堆栈
* 3: 完整详细信息默认
*/
private int detailLevel = 3;
/**
* 是否异步写入日志文件
* 默认启用以提高性能
*/
private boolean asyncWrite = true;
/**
* 异步写入的队列大小
* 默认 1000
*/
private int asyncQueueSize = 1000;
/**
* 检查是否启用了任何错误日志功能
*/
public boolean isAnyErrorLogEnabled() {
return enabled && (startupEnabled || runtimeEnabled || databaseEnabled || businessEnabled);
}
/**
* 检查是否启用了启动相关日志
*/
public boolean isStartupLogEnabled() {
return enabled && (startupEnabled || startupInfoEnabled);
}
}

View File

@ -0,0 +1,185 @@
package com.chinaweal.youfool.devops.config;
import com.chinaweal.youfool.devops.util.ErrorLogUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DataAccessException;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import javax.servlet.http.HttpServletRequest;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.Map;
/**
* 全局异常处理器
* 捕获所有未处理的异常并保存到错误日志
* 支持配置开关控制
*/
@ControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
@Autowired
private ErrorLogProperties errorLogProperties;
/**
* 创建标准错误响应
*/
private Map<String, Object> createErrorResponse(String message, String errorType,
HttpServletRequest request,
boolean includeErrorLogConfig) {
Map<String, Object> result = new HashMap<>();
result.put("success", false);
result.put("message", message);
result.put("error", errorType);
result.put("timestamp", System.currentTimeMillis());
result.put("path", request.getRequestURI());
if (includeErrorLogConfig && errorLogProperties != null) {
result.put("errorLogEnabled", errorLogProperties.isEnabled());
result.put("errorLogConfig", "配置详情请访问 /api/error-logs/config");
}
return result;
}
/**
* 处理数据库相关异常
*/
@ExceptionHandler({SQLException.class, DataAccessException.class})
@ResponseBody
public ResponseEntity<Map<String, Object>> handleDatabaseException(Exception e, HttpServletRequest request) {
log.error("数据库异常 - URL: {}", request.getRequestURI(), e);
ErrorLogUtils.saveDatabaseError("数据库操作异常", e);
Map<String, Object> result = createErrorResponse(
"数据库操作失败,请联系管理员",
"数据库异常",
request,
true
);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(result);
}
/**
* 处理空指针异常
*/
@ExceptionHandler(NullPointerException.class)
@ResponseBody
public ResponseEntity<Map<String, Object>> handleNullPointerException(NullPointerException e, HttpServletRequest request) {
log.error("空指针异常 - URL: {}", request.getRequestURI(), e);
ErrorLogUtils.saveRuntimeError("空指针异常", e, "请求URL: " + request.getRequestURI());
Map<String, Object> result = createErrorResponse(
"系统内部错误,请联系管理员",
"空指针异常",
request,
false
);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(result);
}
/**
* 处理非法参数异常
*/
@ExceptionHandler(IllegalArgumentException.class)
@ResponseBody
public ResponseEntity<Map<String, Object>> handleIllegalArgumentException(IllegalArgumentException e, HttpServletRequest request) {
log.error("非法参数异常 - URL: {}", request.getRequestURI(), e);
ErrorLogUtils.saveRuntimeError("非法参数异常", e, "请求URL: " + request.getRequestURI());
Map<String, Object> result = createErrorResponse(
e.getMessage() != null ? e.getMessage() : "参数错误",
"参数异常",
request,
false
);
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(result);
}
/**
* 处理运行时异常
*/
@ExceptionHandler(RuntimeException.class)
@ResponseBody
public ResponseEntity<Map<String, Object>> handleRuntimeException(RuntimeException e, HttpServletRequest request) {
log.error("运行时异常 - URL: {}", request.getRequestURI(), e);
ErrorLogUtils.saveRuntimeError("运行时异常", e, "请求URL: " + request.getRequestURI());
Map<String, Object> result = createErrorResponse(
"系统运行异常,请联系管理员",
"运行时异常",
request,
false
);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(result);
}
/**
* 处理所有其他异常
*/
@ExceptionHandler(Exception.class)
@ResponseBody
public ResponseEntity<Map<String, Object>> handleGenericException(Exception e, HttpServletRequest request) {
log.error("未知异常 - URL: {}", request.getRequestURI(), e);
ErrorLogUtils.saveRuntimeError("未知异常", e, "请求URL: " + request.getRequestURI());
Map<String, Object> result = createErrorResponse(
"系统异常,请联系管理员",
"系统异常",
request,
false
);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(result);
}
/**
* 处理MyBatis相关异常
*/
@ExceptionHandler(org.apache.ibatis.exceptions.PersistenceException.class)
@ResponseBody
public ResponseEntity<Map<String, Object>> handleMyBatisException(org.apache.ibatis.exceptions.PersistenceException e, HttpServletRequest request) {
log.error("MyBatis异常 - URL: {}", request.getRequestURI(), e);
ErrorLogUtils.saveDatabaseError("MyBatis持久化异常", e);
Map<String, Object> result = new HashMap<>();
result.put("success", false);
result.put("message", "数据访问异常,请联系管理员");
result.put("error", "MyBatis异常");
result.put("timestamp", System.currentTimeMillis());
result.put("path", request.getRequestURI());
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(result);
}
/**
* 处理反射相关异常Java模块系统相关
*/
@ExceptionHandler(java.lang.reflect.InaccessibleObjectException.class)
@ResponseBody
public ResponseEntity<Map<String, Object>> handleInaccessibleObjectException(java.lang.reflect.InaccessibleObjectException e, HttpServletRequest request) {
log.error("Java模块访问异常 - URL: {}", request.getRequestURI(), e);
ErrorLogUtils.saveRuntimeError("Java模块访问异常", e,
"请求URL: " + request.getRequestURI() + ", 建议添加JVM参数: --add-opens java.base/java.util=ALL-UNNAMED");
Map<String, Object> result = new HashMap<>();
result.put("success", false);
result.put("message", "Java版本兼容性问题请联系管理员");
result.put("error", "模块访问异常");
result.put("timestamp", System.currentTimeMillis());
result.put("path", request.getRequestURI());
result.put("suggestion", "需要添加JVM参数以解决Java模块系统兼容性问题");
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(result);
}
}

View File

@ -44,6 +44,8 @@ public class InterceptorConfig implements WebMvcConfigurer {
excludes.add("/statistic/countRepairStepByUserId");
excludes.add("/base/taskFile/uploadFile");
excludes.add("/base/taskFile/downloadFile");
// 错误日志管理接口免认证访问(用于系统监控)
excludes.add("/api/error-logs/**");
registration.excludePathPatterns(excludes);
}

View File

@ -0,0 +1,37 @@
package com.chinaweal.youfool.devops.config;
import com.chinaweal.youfool.devops.repair.api.RobotApi;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.HashMap;
import java.util.Map;
/**
* 本地开发环境RobotApi配置
* 当Forest被禁用时提供Mock实现
*/
@Configuration
@ConditionalOnProperty(name = "forest.enabled", havingValue = "false", matchIfMissing = true)
@Slf4j
public class LocalRobotApiConfig {
@Bean
public RobotApi robotApi() {
return new RobotApi() {
@Override
public Map<String, Object> webhookSend(Map<String, Object> query, String webhookKey) {
log.info("本地模拟机器人消息发送 - webhookKey: {}, 消息内容: {}", webhookKey, query);
Map<String, Object> result = new HashMap<>();
result.put("errcode", 0);
result.put("errmsg", "ok");
result.put("mock", true);
return result;
}
};
}
}

View File

@ -0,0 +1,114 @@
package com.chinaweal.youfool.devops.config;
import io.swagger.v3.oas.models.Components;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Contact;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.info.License;
import io.swagger.v3.oas.models.security.SecurityScheme;
import org.springdoc.core.GroupedOpenApi;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* SpringDoc OpenAPI 配置类 (替换SpringFox)
* @author itluck
*/
@Configuration
@ConditionalOnProperty(value = "swagger.enable", havingValue = "true")
public class SpringDocOpenApiConfig {
@Value("${applicationName}")
private String applicationName;
@Value("${version:1.0.0}")
private String version;
@Value("${description:DevOps运维管理系统}")
private String description;
@Value("${license:Apache 2.0}")
private String license;
/**
* 配置OpenAPI基本信息
*/
@Bean
public OpenAPI customOpenAPI() {
return new OpenAPI()
.info(new Info()
.title(applicationName)
.description(description)
.version(version)
.contact(new Contact()
.name("chinaweal")
.url("https://www.chinaweal.com.cn")
.email(""))
.license(new License()
.name(license)
.url("https://www.chinaweal.com.cn")))
.components(new Components()
.addSecuritySchemes("token", new SecurityScheme()
.type(SecurityScheme.Type.APIKEY)
.in(SecurityScheme.In.HEADER)
.name("token")
.description("令牌")));
}
/**
* 组织架构模块API分组
*/
@Bean
public GroupedOpenApi orgApi() {
return GroupedOpenApi.builder()
.group("组织架构")
.packagesToScan("com.chinaweal.youfool.devops.org")
.build();
}
/**
* 运维报障模块API分组
*/
@Bean
public GroupedOpenApi devopsApi() {
return GroupedOpenApi.builder()
.group("运维报障")
.packagesToScan("com.chinaweal.youfool.devops.repair")
.build();
}
/**
* 基础环境模块API分组
*/
@Bean
public GroupedOpenApi basisApi() {
return GroupedOpenApi.builder()
.group("基础环境")
.packagesToScan("com.chinaweal.youfool.devops.base")
.build();
}
/**
* WebSocket测试环境API分组
*/
@Bean
public GroupedOpenApi websocketApi() {
return GroupedOpenApi.builder()
.group("WebSocket测试环境")
.packagesToScan("com.chinaweal.youfool.devops.websocket")
.build();
}
/**
* 领导分配模块API分组
*/
@Bean
public GroupedOpenApi leaderAssignApi() {
return GroupedOpenApi.builder()
.group("领导分配")
.packagesToScan("com.chinaweal.youfool.devops.leaderassign")
.build();
}
}

View File

@ -1,123 +0,0 @@
package com.chinaweal.youfool.devops.config;
import com.github.xiaoymin.knife4j.spring.annotations.EnableKnife4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.ParameterBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.schema.ModelRef;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.Contact;
import springfox.documentation.service.Parameter;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;
import java.util.ArrayList;
import java.util.List;
/**
* @author itluck
*/
@Configuration
@EnableSwagger2
@EnableKnife4j
@ConditionalOnProperty(value = "swagger.enable", havingValue = "true")
public class SwaggerKnife4j {
@Value("${applicationName}")
private String applicationName;
@Value("${version}")
private String version;
@Value("${description}")
private String description;
@Value("${license}")
private String license;
@Bean("orgApi")
public Docket orgApi() {
ParameterBuilder tokenPar = new ParameterBuilder();
List<Parameter> headers = new ArrayList<>();
tokenPar.name("token").description("令牌").modelRef(new ModelRef("string")).parameterType("header").required(false).build();
headers.add(tokenPar.build());
return new Docket(DocumentationType.SWAGGER_2)
.enable(true)
.apiInfo(apiInfo())
.groupName("组织架构")
.select()
.apis(RequestHandlerSelectors.basePackage("com.chinaweal.youfool.devops.org"))
.paths(PathSelectors.any())
.build()
.globalOperationParameters(headers);
}
@Bean("devopsApi")
public Docket devopsApi() {
//添加head参数start
ParameterBuilder tokenPar = new ParameterBuilder();
List<Parameter> headers = new ArrayList<>();
tokenPar.name("token").description("令牌").modelRef(new ModelRef("string")).parameterType("header").required(false).build();
headers.add(tokenPar.build());
return new Docket(DocumentationType.SWAGGER_2)
.enable(true)
.apiInfo(apiInfo())
.groupName("运维报障")
.select()
.apis(RequestHandlerSelectors.basePackage("com.chinaweal.youfool.devops.repair"))
.paths(PathSelectors.any())
.build()
.globalOperationParameters(headers);
}
@Bean("basisApi")
public Docket basisApi() {
//添加head参数start
ParameterBuilder tokenPar = new ParameterBuilder();
List<Parameter> headers = new ArrayList<>();
tokenPar.name("token").description("令牌").modelRef(new ModelRef("string")).parameterType("header").required(false).build();
headers.add(tokenPar.build());
return new Docket(DocumentationType.SWAGGER_2)
.enable(true)
.apiInfo(apiInfo())
.groupName("基础环境")
.select()
.apis(RequestHandlerSelectors.basePackage("com.chinaweal.youfool.devops.base"))
.paths(PathSelectors.any())
.build()
.globalOperationParameters(headers);
}
@Bean("websocketApi")
public Docket websocketApi() {
//添加head参数start
ParameterBuilder tokenPar = new ParameterBuilder();
List<Parameter> headers = new ArrayList<>();
tokenPar.name("token").description("令牌").modelRef(new ModelRef("string")).parameterType("header").required(false).build();
headers.add(tokenPar.build());
return new Docket(DocumentationType.SWAGGER_2)
.enable(true)
.apiInfo(apiInfo())
.groupName("WebSocket测试环境")
.select()
.apis(RequestHandlerSelectors.basePackage("com.chinaweal.youfool.devops.websocket"))
.paths(PathSelectors.any())
.build()
.globalOperationParameters(headers);
}
private ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title(applicationName)
.description(description)
.termsOfServiceUrl("https://www.chinaweal.com.cn")
.version(version)
.contact(new Contact("chinaweal", "https://www.chinaweal.com.cn", ""))
.license(license)
.licenseUrl("https://www.chinaweal.com.cn")
.build();
}
}

View File

@ -26,7 +26,7 @@ import javax.validation.constraints.NotBlank;
@Data
@EqualsAndHashCode(callSuper = true)
@Accessors(chain = true)
@TableName("ENGINEER")
@TableName("engineer")
@ApiModel(value = "Engineer对象", description = "运维工程师")
public class Engineer extends SuperEntity {
@ -36,7 +36,7 @@ public class Engineer extends SuperEntity {
* 用户ID
*/
@ApiModelProperty(value = "用户ID")
@TableId(value = "USER_ID", type = IdType.ASSIGN_UUID)
@TableId(value = "user_id", type = IdType.ASSIGN_UUID)
private String userId;
/**
@ -44,7 +44,7 @@ public class Engineer extends SuperEntity {
*/
@NotBlank(message = "用户名不能为空")
@ApiModelProperty(value = "用户名 登陆账号")
@TableField("USERNAME")
@TableField("username")
private String username;
/**
@ -52,7 +52,7 @@ public class Engineer extends SuperEntity {
*/
@NotBlank(message = "密码不能为空")
@ApiModelProperty(value = "密码 采用国产SM3")
@TableField("PASSWORD")
@TableField("password")
private String password;
/**
@ -60,7 +60,7 @@ public class Engineer extends SuperEntity {
*/
@NotBlank(message = "昵称不能为空")
@ApiModelProperty(value = "昵称")
@TableField("NICKNAME")
@TableField("nickname")
private String nickname;
/**
@ -68,21 +68,21 @@ public class Engineer extends SuperEntity {
*/
@ApiModelProperty(value = "性别 12")
@NotBlank(message = "性别不能为空")
@TableField("SEX")
@TableField("sex")
private String sex;
/**
* 手机号
*/
@ApiModelProperty(value = "手机号")
@TableField("PHONE")
@TableField("phone")
private String phone;
/**
* 邮箱
*/
@ApiModelProperty(value = "邮箱")
@TableField("EMAIL")
@TableField("email")
private String email;
/**
@ -90,35 +90,35 @@ public class Engineer extends SuperEntity {
*/
@NotBlank(message = "状态不能为空")
@ApiModelProperty(value = "状态 1正常2冻结3离职")
@TableField("STATUS")
@TableField("status")
private String status;
/**
* 描述
*/
@ApiModelProperty(value = "描述")
@TableField("DESCRIPTION")
@TableField("description")
private String description;
/**
* 排序 越小则优先级更高
*/
@ApiModelProperty(value = "排序 越小则优先级更高")
@TableField("SORT")
@TableField("sort")
private BigDecimal sort;
/**
* 角色 角色代码集合
*/
@ApiModelProperty(value = "角色 角色代码集合")
@TableField("ROLES")
@TableField("roles")
private String roles;
/**
* 所负责的系统 集合
*/
@ApiModelProperty(value = "所负责的系统 集合")
@TableField("SOURCE")
@TableField("source")
private String source;
/**
@ -126,7 +126,7 @@ public class Engineer extends SuperEntity {
*/
@ApiModelProperty(value = "是否删除 0未删除、1已删除")
@TableLogic(value = ConstantsUtil.NOT_DELETED, delval = ConstantsUtil.DELETED)
@TableField("IS_DELETED")
@TableField("is_deleted")
private String isDeleted;

View File

@ -26,7 +26,7 @@ import java.util.List;
@Data
@EqualsAndHashCode(callSuper = true)
@Accessors(chain = true)
@TableName(schema = "DEVOPS", value = "REPAIR_TODO")
@TableName(value = "REPAIR_TODO")
@ApiModel(value = "RepairTodo对象", description = "运维报障待办")
@JsonIgnoreProperties(value = {"handler"})
public class RepairTodo extends SuperEntity {

View File

@ -9,8 +9,8 @@ import com.alibaba.excel.write.metadata.fill.FillWrapper;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.chinaweal.youfool.devops.base.entity.Dict;
import com.chinaweal.youfool.devops.base.service.IDictService;
import com.chinaweal.youfool.devops.org.business.entity.BusinessUser;
import com.chinaweal.youfool.devops.org.business.service.BusinessUserService;
import com.chinaweal.youfool.devops.repair.controller.query.RepairTodoListQuery;
import com.chinaweal.youfool.devops.repair.entity.*;
@ -20,7 +20,6 @@ import com.chinaweal.youfool.devops.repair.mapper.RepairMapper;
import com.chinaweal.youfool.devops.repair.mapper.RepairTodoMapper;
import com.chinaweal.youfool.devops.repair.service.*;
import com.chinaweal.youfool.framework.springboot.rest.RestResult;
import com.chinaweal.youfool.framework.springboot.rest.ResultCode;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.util.Strings;
@ -31,6 +30,7 @@ import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.OutputStream;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.nio.charset.StandardCharsets;
import java.time.LocalDate;
import java.time.LocalDateTime;
@ -71,42 +71,15 @@ public class RepairServiceImpl extends ServiceImpl<RepairMapper, Repair> impleme
@Override
public RestResult<String> saveRepair(Repair repair) {
if (StringUtils.isBlank(repair.getSource())) {
repair.setSource("1");
}
//判断来源
if ("2".equals(repair.getSource())) {
repair.setOrgId("特设网办系统");
repair.setOrg("特设网办系统");
} else if ("3".equals(repair.getSource())) {
repair.setOrgId("佛山年报系统");
repair.setOrg("佛山年报系统");
} else if ("4".equals(repair.getSource())) {
repair.setOrgId("佛山行政执法办案平台");
repair.setOrg("佛山行政执法办案平台");
} else if ("5".equals(repair.getSource())) {
repair.setOrgId("佛山消保系统");
repair.setOrg("佛山消保系统");
} else {
//内网发起,查询业务系统的用户信息
BusinessUser user;
if (StringUtils.isNotBlank(repair.getUserId())) {
user = businessUserService.getById(repair.getUserId());
} else {
user = businessUserService.getByUsername(repair.getUsername());
}
if (user == null) {
return RestResult.error(ResultCode.USCID_INVALID, "找不到申报账号信息");
}
repair.setUserId(user.getUserId());
repair.setUsername(user.getUsername());
repair.setDeptId(user.getUnitId());
repair.setDept(user.getUnitName());
repair.setOrgId(user.getOrgId());
repair.setOrg(user.getOrgName());
repair.setNickname(user.getNickname());
if (StringUtils.isBlank(repair.getPhone())) {
repair.setPhone(user.getMobile());
}
repair.setSource("1");//内网
Dict source = iDictService.getByTypeAndCode("source", repair.getSource());
if (source != null) {
repair.setOrg(source.getName());
repair.setOrgId(source.getName());
}
String oldRepairUUID = repair.getRepairId();
@ -277,7 +250,7 @@ public class RepairServiceImpl extends ServiceImpl<RepairMapper, Repair> impleme
processRaterDate.put("id", "processRate");
processRaterDate.put("name", "故障处理率(已确认)");
processRaterDate.put("value", total.compareTo(BigDecimal.valueOf(0)) > 0 ? (resolved.multiply(BigDecimal.valueOf(100))
.divide(total, 2, BigDecimal.ROUND_HALF_UP) + "%") : 0);
.divide(total, 2, RoundingMode.HALF_UP) + "%") : 0);
//已解决+待确认 故障处理率
@ -285,7 +258,7 @@ public class RepairServiceImpl extends ServiceImpl<RepairMapper, Repair> impleme
confirmedProcessRaterDate.put("id", "confirmedProcessRate");
confirmedProcessRaterDate.put("name", "故障处理率(已确认+待确认)");
confirmedProcessRaterDate.put("value", total.compareTo(BigDecimal.valueOf(0)) > 0 ? (resolved.add(confirmed).multiply(BigDecimal.valueOf(100))
.divide(total, 2, BigDecimal.ROUND_HALF_UP) + "%") : 0);
.divide(total, 2, RoundingMode.HALF_UP) + "%") : 0);
//处理故障数量
@ -303,7 +276,7 @@ public class RepairServiceImpl extends ServiceImpl<RepairMapper, Repair> impleme
oneRate.put("id", "oneSuccessRate");
oneRate.put("name", "处理故障1次成功率");
oneRate.put("value", handleTotal.compareTo(BigDecimal.valueOf(0)) > 0 ? (oneSuccess.multiply(BigDecimal.valueOf(100))
.divide(handleTotal, 2, BigDecimal.ROUND_HALF_UP) + "%") : 0);
.divide(handleTotal, 2, RoundingMode.HALF_UP) + "%") : 0);
Map<String, Object> two = new LinkedHashMap<>();
@ -315,7 +288,7 @@ public class RepairServiceImpl extends ServiceImpl<RepairMapper, Repair> impleme
twoRate.put("id", "twoSuccessRate");
twoRate.put("name", "处理故障2次成功率");
twoRate.put("value", handleTotal.compareTo(BigDecimal.valueOf(0)) > 0 ? (twoSuccess.multiply(BigDecimal.valueOf(100))
.divide(handleTotal, 2, BigDecimal.ROUND_HALF_UP) + "%") : 0);
.divide(handleTotal, 2, RoundingMode.HALF_UP) + "%") : 0);
Map<String, Object> three = new LinkedHashMap<>();
@ -327,23 +300,27 @@ public class RepairServiceImpl extends ServiceImpl<RepairMapper, Repair> impleme
threeRate.put("id", "threeSuccessRate");
threeRate.put("name", "处理故障3次及以上成功率");
threeRate.put("value", handleTotal.compareTo(BigDecimal.valueOf(0)) > 0 ? (threeSuccess.multiply(BigDecimal.valueOf(100))
.divide(handleTotal, 2, BigDecimal.ROUND_HALF_UP) + "%") : 0);
.divide(handleTotal, 2, RoundingMode.HALF_UP) + "%") : 0);
Map<String, Object> avgResolvedMap = repairHandleMapper.getAvgResolvedTime(query);
Map<String, Object> avgResolvedTime = new LinkedHashMap<>();
BigDecimal resolvedHour = (BigDecimal) avgResolvedMap.get("resolvedHour");
BigDecimal resolvedNumber = (BigDecimal) avgResolvedMap.get("resolvedNumber");
Object resolvedHourObj = avgResolvedMap.get("resolvedHour");
Object resolvedNumberObj = avgResolvedMap.get("resolvedNumber");
BigDecimal resolvedHour = new BigDecimal(resolvedHourObj != null ? resolvedHourObj.toString() : "0");
BigDecimal resolvedNumber = new BigDecimal(resolvedNumberObj != null ? resolvedNumberObj.toString() : "0");
avgResolvedTime.put("id", "avgResolvedTime");
avgResolvedTime.put("name", "故障平均解决时长");
avgResolvedTime.put("value", resolvedNumber.compareTo(BigDecimal.valueOf(0)) > 0 ? (resolvedHour.divide(resolvedNumber, 2, BigDecimal.ROUND_HALF_UP)) + " 小时" : 0);
avgResolvedTime.put("value", resolvedNumber.compareTo(BigDecimal.valueOf(0)) > 0 ? (resolvedHour.divide(resolvedNumber, 2, RoundingMode.HALF_UP)) + " 小时" : 0);
Map<String, Object> avgFeedbackMap = repairHandleMapper.getAvgFeedbackTime(query);
Map<String, Object> avgFeedbackTime = new LinkedHashMap<>();
BigDecimal feedbackHour = (BigDecimal) avgFeedbackMap.get("feedbackHour");
BigDecimal feedbackNumber = (BigDecimal) avgFeedbackMap.get("feedbackNumber");
Object feedbackHourObj = avgFeedbackMap.get("feedbackHour");
Object feedbackNumberObj = avgFeedbackMap.get("feedbackNumber");
BigDecimal feedbackHour = new BigDecimal(feedbackHourObj != null ? feedbackHourObj.toString() : "0");
BigDecimal feedbackNumber = new BigDecimal(feedbackNumberObj != null ? feedbackNumberObj.toString() : "0");
avgFeedbackTime.put("id", "avgFeedbackTime");
avgFeedbackTime.put("name", "故障平均处理时长");
avgFeedbackTime.put("value", feedbackNumber.compareTo(BigDecimal.valueOf(0)) > 0 ? (feedbackHour.divide(feedbackNumber, 2, BigDecimal.ROUND_HALF_UP)) + " 小时" : 0);
avgFeedbackTime.put("value", feedbackNumber.compareTo(BigDecimal.valueOf(0)) > 0 ? (feedbackHour.divide(feedbackNumber, 2, RoundingMode.HALF_UP)) + " 小时" : 0);
list.add(totalData);
list.add(feedbackData);
@ -375,7 +352,7 @@ public class RepairServiceImpl extends ServiceImpl<RepairMapper, Repair> impleme
}
for (Map<String, Object> map : labelList) {
String name = (String) map.get("name");
BigDecimal num = (BigDecimal) map.get("num");
BigDecimal num = new BigDecimal(map.get("num").toString());
Map<String, Object> data = new LinkedHashMap<>();
data.put("id", labelType + name);
data.put("name", name);

View File

@ -19,6 +19,7 @@ import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.OutputStream;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.nio.charset.StandardCharsets;
import java.time.LocalDate;
import java.time.LocalDateTime;
@ -51,26 +52,26 @@ public class StatisticServiceImpl {
Map<String, Object> map = new LinkedHashMap<>();
query.setOrg(org);
Map<String, Object> data = repairTodoMapper.getWhole(query);
BigDecimal total = (BigDecimal) data.get("total");
BigDecimal resolved = (BigDecimal) data.get("resolved");
BigDecimal feedback = (BigDecimal) data.get("feedback");
BigDecimal unresolved = (BigDecimal) data.get("unresolved");
BigDecimal handleAndDeclare = (BigDecimal) data.get("handleAndDeclare");
BigDecimal total = new BigDecimal(data.get("total").toString());
BigDecimal resolved = new BigDecimal(data.get("resolved").toString());
BigDecimal feedback = new BigDecimal(data.get("feedback").toString());
BigDecimal unresolved = new BigDecimal(data.get("unresolved").toString());
BigDecimal handleAndDeclare = new BigDecimal(data.get("handleAndDeclare").toString());
map.put("no", i + 1);
map.put("name", org);
map.put("total", total);
map.put("resolved", resolved);
map.put("resolvedRate", total.compareTo(BigDecimal.valueOf(0)) > 0 ? (resolved.multiply(BigDecimal.valueOf(100))
.divide(total, 1, BigDecimal.ROUND_HALF_UP) + "%") : 0);
.divide(total, 1, RoundingMode.HALF_UP) + "%") : 0);
map.put("feedback", feedback);
map.put("feedbackRate", total.compareTo(BigDecimal.valueOf(0)) > 0 ? (feedback.multiply(BigDecimal.valueOf(100))
.divide(total, 1, BigDecimal.ROUND_HALF_UP) + "%") : 0);
.divide(total, 1, RoundingMode.HALF_UP) + "%") : 0);
map.put("unresolved", unresolved);
map.put("unresolvedRate", total.compareTo(BigDecimal.valueOf(0)) > 0 ? (unresolved.multiply(BigDecimal.valueOf(100))
.divide(total, 1, BigDecimal.ROUND_HALF_UP) + "%") : 0);
.divide(total, 1, RoundingMode.HALF_UP) + "%") : 0);
map.put("handleAndDeclare", handleAndDeclare);
map.put("handleAndDeclareRate", total.compareTo(BigDecimal.valueOf(0)) > 0 ? (handleAndDeclare.multiply(BigDecimal.valueOf(100))
.divide(total, 1, BigDecimal.ROUND_HALF_UP) + "%") : 0);
.divide(total, 1, RoundingMode.HALF_UP) + "%") : 0);
list.add(map);
}
return list;

View File

@ -0,0 +1,395 @@
package com.chinaweal.youfool.devops.util;
import com.chinaweal.youfool.devops.config.ErrorLogProperties;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
/**
* 错误日志工具类
* 用于统一处理和保存各种错误信息
* 支持配置开关控制
*/
@Component
@Slf4j
public class ErrorLogUtils {
private static final String DATE_FORMAT = "yyyy-MM-dd HH:mm:ss";
private static final SimpleDateFormat SDF = new SimpleDateFormat(DATE_FORMAT);
private static ErrorLogProperties errorLogProperties;
private static ThreadPoolExecutor asyncExecutor;
@Autowired
public void setErrorLogProperties(ErrorLogProperties properties) {
ErrorLogUtils.errorLogProperties = properties;
// 如果启用异步写入,初始化线程池
if (properties.isAsyncWrite()) {
initAsyncExecutor();
}
}
/**
* 初始化异步执行器
*/
private static void initAsyncExecutor() {
if (asyncExecutor == null || asyncExecutor.isShutdown()) {
asyncExecutor = new ThreadPoolExecutor(
1, 2, 60L, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(errorLogProperties.getAsyncQueueSize()),
r -> {
Thread t = new Thread(r, "ErrorLog-Writer");
t.setDaemon(true);
return t;
},
new ThreadPoolExecutor.CallerRunsPolicy()
);
}
}
/**
* 保存启动错误信息
*/
public static void saveStartupError(String errorType, Throwable throwable) {
if (isEnabled() && isStartupErrorEnabled()) {
saveError("startup-error", errorType, throwable, null);
} else {
logToConsoleIfEnabled("启动错误", errorType, throwable);
}
}
/**
* 保存运行时错误信息
*/
public static void saveRuntimeError(String errorType, Throwable throwable, String additionalInfo) {
if (isEnabled() && isRuntimeErrorEnabled()) {
saveError("runtime-error", errorType, throwable, additionalInfo);
} else {
logToConsoleIfEnabled("运行时错误", errorType, throwable);
}
}
/**
* 保存数据库错误信息
*/
public static void saveDatabaseError(String operation, Throwable throwable) {
if (isEnabled() && isDatabaseErrorEnabled()) {
saveError("database-error", operation, throwable, null);
} else {
logToConsoleIfEnabled("数据库错误", operation, throwable);
}
}
/**
* 保存业务错误信息
*/
public static void saveBusinessError(String module, String operation, Throwable throwable) {
if (isEnabled() && isBusinessErrorEnabled()) {
saveError("business-error", module + ":" + operation, throwable, null);
} else {
logToConsoleIfEnabled("业务错误", module + ":" + operation, throwable);
}
}
/**
* 记录系统启动信息
*/
public static void logStartupInfo(String message) {
if (isEnabled() && isStartupInfoEnabled()) {
saveStartupInfoToFile(message);
} else {
logToConsoleIfEnabled("启动信息", message, null);
}
}
/**
* 配置开关检查方法
*/
private static boolean isEnabled() {
return errorLogProperties != null && errorLogProperties.isEnabled();
}
private static boolean isStartupErrorEnabled() {
return errorLogProperties != null && errorLogProperties.isStartupEnabled();
}
private static boolean isRuntimeErrorEnabled() {
return errorLogProperties != null && errorLogProperties.isRuntimeEnabled();
}
private static boolean isDatabaseErrorEnabled() {
return errorLogProperties != null && errorLogProperties.isDatabaseEnabled();
}
private static boolean isBusinessErrorEnabled() {
return errorLogProperties != null && errorLogProperties.isBusinessEnabled();
}
private static boolean isStartupInfoEnabled() {
return errorLogProperties != null && errorLogProperties.isStartupInfoEnabled();
}
private static boolean isConsoleOutputEnabled() {
return errorLogProperties == null || errorLogProperties.isConsoleOutput();
}
private static String getLogDirectory() {
return errorLogProperties != null ? errorLogProperties.getLogDirectory() : "logs/errors";
}
private static int getDetailLevel() {
return errorLogProperties != null ? errorLogProperties.getDetailLevel() : 3;
}
/**
* 仅输出到控制台当文件写入被禁用时
*/
private static void logToConsoleIfEnabled(String category, String errorType, Throwable throwable) {
if (isConsoleOutputEnabled()) {
if (throwable != null) {
log.error("[{}] {}: {}", category, errorType, throwable.getMessage(), throwable);
} else {
log.info("[{}] {}", category, errorType);
}
}
}
/**
* 保存启动信息到文件
*/
private static void saveStartupInfoToFile(String message) {
executeLogWrite(() -> {
try {
File errorDir = new File(getLogDirectory());
if (!errorDir.exists()) {
errorDir.mkdirs();
}
File startupFile = new File(errorDir, "startup-info.log");
try (FileWriter fw = new FileWriter(startupFile, true);
PrintWriter pw = new PrintWriter(fw)) {
pw.println(SDF.format(new Date()) + " - " + message);
pw.flush();
}
} catch (IOException e) {
log.error("记录启动信息失败", e);
}
});
}
/**
* 执行日志写入支持同步/异步
*/
private static void executeLogWrite(Runnable writeTask) {
if (errorLogProperties != null && errorLogProperties.isAsyncWrite() && asyncExecutor != null) {
asyncExecutor.execute(writeTask);
} else {
writeTask.run();
}
}
/**
* 通用错误保存方法
*/
private static void saveError(String category, String errorType, Throwable throwable, String additionalInfo) {
executeLogWrite(() -> saveErrorSync(category, errorType, throwable, additionalInfo));
}
/**
* 同步保存错误信息
*/
private static void saveErrorSync(String category, String errorType, Throwable throwable, String additionalInfo) {
try {
// 确保错误日志目录存在
File errorDir = new File(getLogDirectory());
if (!errorDir.exists()) {
errorDir.mkdirs();
}
// 创建错误日志文件
String fileName = String.format("%s-%s.log", category,
new SimpleDateFormat("yyyy-MM-dd").format(new Date()));
File errorFile = new File(errorDir, fileName);
// 写入错误信息
try (FileWriter fw = new FileWriter(errorFile, true);
PrintWriter pw = new PrintWriter(fw)) {
writeErrorDetails(pw, category, errorType, throwable, additionalInfo);
pw.flush();
}
if (isConsoleOutputEnabled()) {
log.error("错误信息已保存到: {}", errorFile.getAbsolutePath());
}
} catch (IOException e) {
log.error("保存错误日志失败", e);
}
}
/**
* 根据详细级别写入错误信息
*/
private static void writeErrorDetails(PrintWriter pw, String category, String errorType,
Throwable throwable, String additionalInfo) {
int detailLevel = getDetailLevel();
pw.println("==================== 错误记录 ====================");
pw.println("时间: " + SDF.format(new Date()));
pw.println("类别: " + category);
pw.println("类型: " + errorType);
if (additionalInfo != null && !additionalInfo.isEmpty()) {
pw.println("附加信息: " + additionalInfo);
}
if (throwable != null) {
if (detailLevel >= 1) {
pw.println("异常类型: " + throwable.getClass().getSimpleName());
pw.println("错误消息: " + throwable.getMessage());
}
if (detailLevel >= 2) {
// 获取根本原因
Throwable rootCause = getRootCause(throwable);
if (rootCause != throwable) {
pw.println("根本原因: " + rootCause.getClass().getSimpleName());
pw.println("根本消息: " + rootCause.getMessage());
}
}
if (detailLevel >= 3) {
pw.println("详细堆栈:");
throwable.printStackTrace(pw);
} else if (detailLevel == 2) {
pw.println("简化堆栈:");
writeSimplifiedStackTrace(pw, throwable);
}
}
pw.println("================================================");
pw.println();
}
/**
* 写入简化的堆栈跟踪
*/
private static void writeSimplifiedStackTrace(PrintWriter pw, Throwable throwable) {
StackTraceElement[] elements = throwable.getStackTrace();
int maxLines = 10; // 限制显示的堆栈行数
for (int i = 0; i < Math.min(elements.length, maxLines); i++) {
StackTraceElement element = elements[i];
// 只显示项目相关的堆栈
if (element.getClassName().startsWith("com.chinaweal.youfool.devops")) {
pw.println(" at " + element.toString());
}
}
if (elements.length > maxLines) {
pw.println(" ... " + (elements.length - maxLines) + " more");
}
}
/**
* 获取配置信息供外部查询
*/
public static String getConfigurationInfo() {
if (errorLogProperties == null) {
return "错误日志配置未初始化";
}
StringBuilder info = new StringBuilder();
info.append("错误日志配置信息:\n");
info.append("- 全局开关: ").append(errorLogProperties.isEnabled()).append("\n");
info.append("- 启动错误: ").append(errorLogProperties.isStartupEnabled()).append("\n");
info.append("- 运行时错误: ").append(errorLogProperties.isRuntimeEnabled()).append("\n");
info.append("- 数据库错误: ").append(errorLogProperties.isDatabaseEnabled()).append("\n");
info.append("- 业务错误: ").append(errorLogProperties.isBusinessEnabled()).append("\n");
info.append("- 启动信息: ").append(errorLogProperties.isStartupInfoEnabled()).append("\n");
info.append("- 日志目录: ").append(errorLogProperties.getLogDirectory()).append("\n");
info.append("- 详细级别: ").append(errorLogProperties.getDetailLevel()).append("\n");
info.append("- 异步写入: ").append(errorLogProperties.isAsyncWrite()).append("\n");
info.append("- 控制台输出: ").append(errorLogProperties.isConsoleOutput()).append("\n");
return info.toString();
}
/**
* 关闭异步执行器应用关闭时调用
*/
public static void shutdown() {
if (asyncExecutor != null && !asyncExecutor.isShutdown()) {
asyncExecutor.shutdown();
try {
if (!asyncExecutor.awaitTermination(5, TimeUnit.SECONDS)) {
asyncExecutor.shutdownNow();
}
} catch (InterruptedException e) {
asyncExecutor.shutdownNow();
Thread.currentThread().interrupt();
}
}
}
/**
* 获取异常的完整堆栈信息
*/
public static String getStackTrace(Throwable throwable) {
StringWriter sw = new StringWriter();
PrintWriter pw = new PrintWriter(sw);
throwable.printStackTrace(pw);
return sw.toString();
}
/**
* 格式化错误信息用于显示
*/
public static String formatErrorInfo(String errorType, Throwable throwable) {
StringBuilder sb = new StringBuilder();
sb.append("错误类型: ").append(errorType).append("\n");
sb.append("时间: ").append(SDF.format(new Date())).append("\n");
if (throwable != null) {
sb.append("异常: ").append(throwable.getClass().getSimpleName()).append("\n");
sb.append("消息: ").append(throwable.getMessage()).append("\n");
// 获取根本原因
Throwable rootCause = getRootCause(throwable);
if (rootCause != throwable) {
sb.append("根本原因: ").append(rootCause.getClass().getSimpleName()).append("\n");
sb.append("根本消息: ").append(rootCause.getMessage()).append("\n");
}
}
return sb.toString();
}
/**
* 获取异常的根本原因
*/
private static Throwable getRootCause(Throwable throwable) {
Throwable cause = throwable;
while (cause.getCause() != null && cause.getCause() != cause) {
cause = cause.getCause();
}
return cause;
}
}

View File

@ -14,6 +14,9 @@ spring:
url: jdbc:postgresql://172.22.80.157:5432/devops_gd
username: devops_gd
password: ChinaWeal@2024
main:
# Spring Boot 2.7+新增:允许循环依赖(临时修复,后续需重构代码消除循环依赖)
allow-circular-references: true
file:
devopsDir: D:\chinaweal\gitea-code\youfool-project\youfool-devops\upload
@ -32,3 +35,46 @@ applicationName: devOps
swagger:
enable: true
# SpringDoc OpenAPI 配置 (替换SpringFox)
springdoc:
api-docs:
enabled: true
path: /v3/api-docs
swagger-ui:
enabled: true
path: /swagger-ui.html
operations-sorter: alpha
tags-sorter: alpha
version: "1.0.0"
description: "DevOps运维管理系统API文档"
license: "Apache 2.0"
# 错误日志配置
error-log:
# 是否启用错误日志文件写入功能
enabled: true
# 启动错误日志
startup-enabled: true
# 运行时错误日志
runtime-enabled: true
# 数据库错误日志
database-enabled: true
# 业务错误日志
business-enabled: true
# 启动信息日志
startup-info-enabled: true
# 日志目录
log-directory: logs/errors
# 单个日志文件最大大小
max-file-size: 10MB
# 保留日志文件的最大天数
max-history: 30
# 是否在控制台同时输出
console-output: true
# 详细级别 (0-3, 3为最详细)
detail-level: 3
# 是否异步写入
async-write: true
# 异步队列大小
async-queue-size: 1000

View File

@ -0,0 +1,72 @@
server:
port: 8080
logging:
level:
dao: debug
youfool.dao: info
com.chinaweal.youfool.framework.springboot.log: debug
com.chinaweal.youfool.devops: debug
spring:
autoconfigure:
exclude: com.dtflys.forest.springboot.ForestAutoConfiguration
datasource:
youfool:
url: jdbc:postgresql://localhost:5432/devops_gd
username: postgres
password: 123456
devops:
url: jdbc:postgresql://localhost:5432/devops_gd
username: postgres
password: 123456
file:
devopsDir: ./upload
business:
fsLoginUrl: http://121.8.152.130:9888/tzrysb/user/loginDevops
sdLoginUrl: http://121.8.152.130:9888/tzrysb/user/loginDevops
fsUserInfoUrl: http://121.8.152.130:9888/tzrysb/user/searchUserDevops
sdUserInfoUrl: http://121.8.152.130:9888/tzrysb/user/searchUserDevops
# 本地测试用的webhook密钥
noticeWebhookKeys: test-webhook-key
# 数据账号密码过期机器人
dbWebhookKeys: test-db-webhook-key
applicationName: devOps-Local
swagger:
enable: true
# 禁用Forest相关配置
forest:
enabled: false
# 错误日志配置
error-log:
# 是否启用错误日志文件写入功能
enabled: true
# 启动错误日志
startup-enabled: true
# 运行时错误日志
runtime-enabled: true
# 数据库错误日志
database-enabled: true
# 业务错误日志
business-enabled: true
# 启动信息日志
startup-info-enabled: true
# 日志目录
log-directory: logs/errors
# 单个日志文件最大大小
max-file-size: 10MB
# 保留日志文件的最大天数
max-history: 30
# 是否在控制台同时输出
console-output: true
# 详细级别 (0-3, 3为最详细)
detail-level: 3
# 是否异步写入
async-write: true
# 异步队列大小
async-queue-size: 1000

View File

@ -17,7 +17,7 @@ spring:
password: ChinaWeal@2024
file:
devopsDir: ..\webapps\upload
devopsDir: D:\project\devops-gd\upload
business:
fsLoginUrl: http://121.8.152.130:9888/tzrysb/user/loginDevops
@ -32,3 +32,10 @@ dbWebhookKeys: 45d1bda2-c0b9-45c3-b640-be77f7a0726d,2eacc126-74d2-4360-a88e-369c
swagger:
enable: false
# SpringDoc OpenAPI 配置 (生产环境禁用)
springdoc:
api-docs:
enabled: false
swagger-ui:
enabled: false

View File

@ -3,7 +3,7 @@ logging:
root: info
spring:
profiles:
active: prod
active: dev
application:
name: youfoo-devops
datasource:
@ -17,7 +17,7 @@ spring:
max-wait: 30000
time-between-eviction-runs-millis: 60000
min-evictable-idle-time-millis: 300000
validation-query: select 1 from dual
validation-query: select 1
devops:
filters: stat
initial-size: 2
@ -26,7 +26,7 @@ spring:
max-wait: 30000
time-between-eviction-runs-millis: 60000
min-evictable-idle-time-millis: 300000
validation-query: select 1 from dual
validation-query: select 1
druid:
web-stat-filter:
enabled: true
@ -80,3 +80,22 @@ forest:
#数据库密码监控账号列表
dbUsernames: AICSCR,AICORG,SPEEQU,ZKRUSER,GOLDENGATE,DEVOPS,SYSTEM,SPEE_OS,DWDDATA,DIMDATA,AICBLACK,CRUSER,ZSJUSER,FJYUSER,CXAICSCR,XIAOBAO,FSREPORT
# 错误日志默认配置
error-log:
# 全局开关,默认启用
enabled: false
# 各类型日志开关
startup-enabled: true
runtime-enabled: true
database-enabled: true
business-enabled: true
startup-info-enabled: true
# 基础配置
log-directory: logs/errors
max-file-size: 5MB
max-history: 7
console-output: true
detail-level: 2
async-write: true
async-queue-size: 500

View File

@ -96,8 +96,22 @@
</filter>
</appender>
<!-- 启动错误专用日志 -->
<appender name="STARTUP_ERROR" class="ch.qos.logback.core.rolling.RollingFileAppender">
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %level %logger{36} - %msg%n</pattern>
</encoder>
<file>${logFilePath}/startup-error.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>${logFilePath}/startup-error.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<maxFileSize>10MB</maxFileSize>
<maxHistory>7</maxHistory>
</rollingPolicy>
</appender>
<root>
<appender-ref ref="CONSOLE"/>
<!-- 生产环境启用所有文件日志 -->
<springProfile name="prod">
<appender-ref ref="FILE"/>
<appender-ref ref="FILE_DEBUG"/>
@ -105,7 +119,20 @@
<appender-ref ref="FILE_WARN"/>
<appender-ref ref="FILE_ERROR"/>
</springProfile>
<!-- 本地环境启用错误和警告日志 -->
<springProfile name="local">
<appender-ref ref="FILE"/>
<appender-ref ref="FILE_WARN"/>
<appender-ref ref="FILE_ERROR"/>
</springProfile>
<!-- 开发环境启用调试日志 -->
<springProfile name="dev">
<appender-ref ref="FILE"/>
<appender-ref ref="FILE_DEBUG"/>
<appender-ref ref="FILE_INFO"/>
<appender-ref ref="FILE_WARN"/>
<appender-ref ref="FILE_ERROR"/>
</springProfile>
</root>
<logger name="com.baomidou.mybatisplus.generator" level="debug"/>

View File

@ -73,7 +73,7 @@
and count( case when rh.step = 'feedback' then 1 else null end ) >2
and count( case when rh.step = 'unresolved' then 1 else null end ) > 0
</if>
)
) a
</select>
<select id="listResultSecond"